@homeflare/alchemy 0.38.0 → 0.40.0

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 (353) hide show
  1. package/README.md +32 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/forgejo/index.js +4 -2
  27. package/dist/forgejo/index.js.map +3 -3
  28. package/dist/forgejo/team-member.d.ts +25 -0
  29. package/dist/forgejo/team-member.d.ts.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  33. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  34. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  35. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  36. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  37. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  38. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  39. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  40. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  41. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  42. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  43. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  44. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  45. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  46. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  47. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  48. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  49. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  50. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  51. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  52. package/dist/proxmox/index.d.ts +14 -3
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +574 -133
  55. package/dist/proxmox/index.js.map +33 -23
  56. package/dist/proxmox/qemu-form.d.ts +27 -18
  57. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  58. package/dist/proxmox/qemu-judge.d.ts +30 -0
  59. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  60. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  61. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  62. package/dist/proxmox/qemu-net.d.ts +9 -0
  63. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  64. package/dist/proxmox/qemu-props.d.ts +112 -0
  65. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  66. package/dist/proxmox/qemu-volume.d.ts +27 -0
  67. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  68. package/dist/proxmox/qemu.d.ts +14 -27
  69. package/dist/proxmox/qemu.d.ts.map +1 -1
  70. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  71. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  72. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  73. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  74. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  75. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  76. package/dist/proxmox/storage-download-props.d.ts +36 -0
  77. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  78. package/dist/proxmox/storage-download-task.d.ts +17 -0
  79. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  80. package/dist/proxmox/storage-download.d.ts +44 -0
  81. package/dist/proxmox/storage-download.d.ts.map +1 -0
  82. package/dist/talos/credentials-write.d.ts +46 -0
  83. package/dist/talos/credentials-write.d.ts.map +1 -0
  84. package/dist/talos/credentials.d.ts +51 -9
  85. package/dist/talos/credentials.d.ts.map +1 -1
  86. package/dist/talos/fake-process.d.ts +56 -0
  87. package/dist/talos/fake-process.d.ts.map +1 -0
  88. package/dist/talos/index.d.ts +19 -4
  89. package/dist/talos/index.d.ts.map +1 -1
  90. package/dist/talos/index.js +357 -173
  91. package/dist/talos/index.js.map +16 -10
  92. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  93. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  94. package/dist/talos/kubeconfig.d.ts +41 -23
  95. package/dist/talos/kubeconfig.d.ts.map +1 -1
  96. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  97. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  98. package/dist/talos/machine-config-poll.d.ts +43 -0
  99. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  100. package/dist/talos/machine-config-read.d.ts +49 -0
  101. package/dist/talos/machine-config-read.d.ts.map +1 -0
  102. package/dist/talos/resource.d.ts +18 -0
  103. package/dist/talos/resource.d.ts.map +1 -1
  104. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  105. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  106. package/dist/talos/talos-bootstrap.d.ts +87 -0
  107. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  108. package/dist/talos/talos-cluster-health.d.ts +43 -1
  109. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  110. package/dist/talos/talos-errors.d.ts +68 -0
  111. package/dist/talos/talos-errors.d.ts.map +1 -0
  112. package/dist/talos/talos-machine-config.d.ts +51 -15
  113. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  114. package/dist/talos/talosctl.d.ts +7 -0
  115. package/dist/talos/talosctl.d.ts.map +1 -1
  116. package/dist/talos/values.d.ts +23 -3
  117. package/dist/talos/values.d.ts.map +1 -1
  118. package/dist/telemetry/fake-collector.d.ts +36 -0
  119. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  120. package/dist/telemetry/index.d.ts +11 -0
  121. package/dist/telemetry/index.d.ts.map +1 -0
  122. package/dist/telemetry/index.js +223 -0
  123. package/dist/telemetry/index.js.map +12 -0
  124. package/dist/telemetry/layer.d.ts +86 -0
  125. package/dist/telemetry/layer.d.ts.map +1 -0
  126. package/dist/telemetry/redact-serialization.d.ts +21 -0
  127. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  128. package/dist/telemetry/redact.d.ts +46 -0
  129. package/dist/telemetry/redact.d.ts.map +1 -0
  130. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  131. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  132. package/dist/unifi/acl-rule-form.d.ts +90 -0
  133. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  134. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  135. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  136. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  137. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  138. package/dist/unifi/acl-rule.d.ts +49 -0
  139. package/dist/unifi/acl-rule.d.ts.map +1 -0
  140. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  141. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  142. package/dist/unifi/dns-policy-form.d.ts +97 -0
  143. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  144. package/dist/unifi/dns-policy.d.ts +55 -0
  145. package/dist/unifi/dns-policy.d.ts.map +1 -0
  146. package/dist/unifi/drift.d.ts +27 -0
  147. package/dist/unifi/drift.d.ts.map +1 -0
  148. package/dist/unifi/fake-unifi.d.ts +5 -1
  149. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  150. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  151. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  152. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  153. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  154. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  155. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  156. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  157. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  158. package/dist/unifi/firewall-policy.d.ts +57 -0
  159. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  160. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  161. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  162. package/dist/unifi/firewall-zone.d.ts +1 -1
  163. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  164. package/dist/unifi/index.d.ts +11 -3
  165. package/dist/unifi/index.d.ts.map +1 -1
  166. package/dist/unifi/index.js +725 -60
  167. package/dist/unifi/index.js.map +31 -9
  168. package/dist/unifi/network-drift.d.ts +26 -0
  169. package/dist/unifi/network-drift.d.ts.map +1 -0
  170. package/dist/unifi/network-form.d.ts +40 -7
  171. package/dist/unifi/network-form.d.ts.map +1 -1
  172. package/dist/unifi/network.d.ts +1 -0
  173. package/dist/unifi/network.d.ts.map +1 -1
  174. package/dist/unifi/paginate.d.ts +24 -0
  175. package/dist/unifi/paginate.d.ts.map +1 -0
  176. package/dist/unifi/providers.d.ts +1 -1
  177. package/dist/unifi/providers.d.ts.map +1 -1
  178. package/dist/unifi/resource.d.ts +36 -1
  179. package/dist/unifi/resource.d.ts.map +1 -1
  180. package/dist/unifi/scan-source.d.ts +11 -0
  181. package/dist/unifi/scan-source.d.ts.map +1 -0
  182. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  183. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  184. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  185. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  186. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  187. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  188. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  189. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  190. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  191. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  192. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  193. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  194. package/docs/telemetry-spike.md +94 -0
  195. package/docs/telemetry.md +109 -0
  196. package/docs/unifi-acl-rule.md +122 -0
  197. package/docs/unifi-dns-policy.md +75 -0
  198. package/docs/unifi-firewall-policy.md +109 -0
  199. package/docs/unifi-traffic-matching-list.md +82 -0
  200. package/docs/unifi-wifi.md +120 -0
  201. package/docs/unifi.md +64 -3
  202. package/package.json +10 -2
  203. package/src/ceph/auth-entity.ts +74 -0
  204. package/src/ceph/ceph-argv-config.ts +78 -0
  205. package/src/ceph/ceph-argv.test.ts +226 -0
  206. package/src/ceph/ceph-argv.ts +189 -0
  207. package/src/ceph/ceph-auth-form.test.ts +103 -0
  208. package/src/ceph/ceph-auth-form.ts +107 -0
  209. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  210. package/src/ceph/ceph-auth-parse.ts +121 -0
  211. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  212. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  213. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  214. package/src/ceph/ceph-config-options.test.ts +44 -0
  215. package/src/ceph/ceph-config-options.ts +36 -0
  216. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  217. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  218. package/src/ceph/ceph-transport.test.ts +125 -0
  219. package/src/ceph/ceph-transport.ts +117 -0
  220. package/src/ceph/fake-ceph-dial.ts +55 -0
  221. package/src/ceph/index.ts +12 -0
  222. package/src/forgejo/team-member.test.ts +14 -0
  223. package/src/forgejo/team-member.ts +28 -1
  224. package/src/index.ts +1 -0
  225. package/src/proxmox/constraints-required.test.ts +1 -0
  226. package/src/proxmox/generated/constraints/index.ts +3 -1
  227. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  228. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  229. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  230. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  231. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  232. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  233. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  234. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  235. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  236. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  237. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  238. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  239. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  240. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  241. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  242. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  243. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  244. package/src/proxmox/index.test.ts +11 -6
  245. package/src/proxmox/index.ts +19 -3
  246. package/src/proxmox/qemu-form.ts +49 -13
  247. package/src/proxmox/qemu-identity.test.ts +30 -0
  248. package/src/proxmox/qemu-judge.test.ts +65 -0
  249. package/src/proxmox/qemu-judge.ts +84 -0
  250. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  251. package/src/proxmox/qemu-lifecycle.ts +131 -32
  252. package/src/proxmox/qemu-net.test.ts +48 -0
  253. package/src/proxmox/qemu-net.ts +78 -0
  254. package/src/proxmox/qemu-ownership.test.ts +44 -0
  255. package/src/proxmox/qemu-props.test.ts +87 -0
  256. package/src/proxmox/qemu-props.ts +157 -0
  257. package/src/proxmox/qemu-volume.test.ts +229 -0
  258. package/src/proxmox/qemu-volume.ts +178 -0
  259. package/src/proxmox/qemu.ts +15 -29
  260. package/src/proxmox/storage-download-errors.ts +7 -0
  261. package/src/proxmox/storage-download-identity.test.ts +54 -0
  262. package/src/proxmox/storage-download-identity.ts +50 -0
  263. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  264. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  265. package/src/proxmox/storage-download-props.ts +38 -0
  266. package/src/proxmox/storage-download-task.ts +72 -0
  267. package/src/proxmox/storage-download.test.ts +216 -0
  268. package/src/proxmox/storage-download.ts +58 -0
  269. package/src/talos/credentials-write.test.ts +81 -0
  270. package/src/talos/credentials-write.ts +95 -0
  271. package/src/talos/credentials.test.ts +157 -0
  272. package/src/talos/credentials.ts +160 -90
  273. package/src/talos/fake-process.ts +94 -0
  274. package/src/talos/index.ts +39 -4
  275. package/src/talos/kubeconfig-attrs.ts +55 -0
  276. package/src/talos/kubeconfig.test.ts +256 -0
  277. package/src/talos/kubeconfig.ts +170 -90
  278. package/src/talos/machine-config-fixtures.ts +86 -0
  279. package/src/talos/machine-config-poll.test.ts +57 -0
  280. package/src/talos/machine-config-poll.ts +82 -0
  281. package/src/talos/machine-config-read.ts +118 -0
  282. package/src/talos/resource.ts +19 -0
  283. package/src/talos/secrets-gate.test.ts +63 -0
  284. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  285. package/src/talos/talos-bootstrap.test.ts +218 -0
  286. package/src/talos/talos-bootstrap.ts +151 -45
  287. package/src/talos/talos-cluster-health.test.ts +143 -0
  288. package/src/talos/talos-cluster-health.ts +91 -28
  289. package/src/talos/talos-errors.ts +85 -0
  290. package/src/talos/talos-machine-config.test.ts +189 -0
  291. package/src/talos/talos-machine-config.ts +102 -88
  292. package/src/talos/talosctl.ts +8 -0
  293. package/src/talos/values.test.ts +34 -6
  294. package/src/talos/values.ts +41 -7
  295. package/src/telemetry/fake-collector.ts +59 -0
  296. package/src/telemetry/http-client-sealing.test.ts +82 -0
  297. package/src/telemetry/index.ts +10 -0
  298. package/src/telemetry/layer.test.ts +204 -0
  299. package/src/telemetry/layer.ts +152 -0
  300. package/src/telemetry/redact-serialization.ts +178 -0
  301. package/src/telemetry/redact.test.ts +124 -0
  302. package/src/telemetry/redact.ts +147 -0
  303. package/src/unifi/acl-rule-drift.ts +57 -0
  304. package/src/unifi/acl-rule-form.ts +135 -0
  305. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  306. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  307. package/src/unifi/acl-rule-ordering.ts +70 -0
  308. package/src/unifi/acl-rule.test.ts +183 -0
  309. package/src/unifi/acl-rule.ts +61 -0
  310. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  311. package/src/unifi/dns-policy-drift.ts +48 -0
  312. package/src/unifi/dns-policy-form.ts +142 -0
  313. package/src/unifi/dns-policy.test.ts +154 -0
  314. package/src/unifi/dns-policy.ts +67 -0
  315. package/src/unifi/drift.test.ts +78 -0
  316. package/src/unifi/drift.ts +56 -0
  317. package/src/unifi/errors-and-secrets.test.ts +127 -0
  318. package/src/unifi/fake-unifi.ts +5 -1
  319. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  320. package/src/unifi/firewall-policy-drift.ts +55 -0
  321. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  322. package/src/unifi/firewall-policy-form.ts +146 -0
  323. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  324. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  325. package/src/unifi/firewall-policy-ordering.ts +87 -0
  326. package/src/unifi/firewall-policy.test.ts +183 -0
  327. package/src/unifi/firewall-policy.ts +73 -0
  328. package/src/unifi/firewall-zone-form.ts +31 -2
  329. package/src/unifi/firewall-zone.test.ts +21 -1
  330. package/src/unifi/firewall-zone.ts +1 -1
  331. package/src/unifi/get-only-guard.test.ts +165 -0
  332. package/src/unifi/index.ts +78 -1
  333. package/src/unifi/network-drift.test.ts +207 -0
  334. package/src/unifi/network-drift.ts +80 -0
  335. package/src/unifi/network-form.ts +11 -39
  336. package/src/unifi/network.test.ts +7 -82
  337. package/src/unifi/network.ts +6 -6
  338. package/src/unifi/paginate.test.ts +252 -0
  339. package/src/unifi/paginate.ts +141 -0
  340. package/src/unifi/providers.ts +19 -1
  341. package/src/unifi/resource.ts +73 -3
  342. package/src/unifi/scan-source.ts +64 -0
  343. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  344. package/src/unifi/traffic-matching-list-form.ts +77 -0
  345. package/src/unifi/traffic-matching-list.test.ts +202 -0
  346. package/src/unifi/traffic-matching-list.ts +78 -0
  347. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  348. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  349. package/src/unifi/wifi-broadcast-form.ts +211 -0
  350. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  351. package/src/unifi/wifi-broadcast.test.ts +155 -0
  352. package/src/unifi/wifi-broadcast.ts +90 -0
  353. package/src/unifi/write-op-reference.test.ts +190 -0
@@ -0,0 +1,1609 @@
1
+ // src/ceph/auth-entity.ts
2
+ import { Resource } from "alchemy";
3
+ import { isResolved } from "alchemy/Diff";
4
+ import * as Provider from "alchemy/Provider";
5
+ import * as Effect3 from "effect/Effect";
6
+
7
+ // src/openbao/mount-path.ts
8
+ var trimTrailing = (text, char) => {
9
+ let end = text.length;
10
+ while (end > 0 && text[end - 1] === char)
11
+ end -= 1;
12
+ return text.slice(0, end);
13
+ };
14
+ var trimRuns = (text, char) => {
15
+ let start = 0;
16
+ while (start < text.length && text[start] === char)
17
+ start += 1;
18
+ return trimTrailing(text.slice(start), char);
19
+ };
20
+ var trimTrailingSlashes = (text) => trimTrailing(text, "/");
21
+ var trimSlashes = (text) => trimRuns(text, "/");
22
+ var sshMountOf = (mount) => trimTrailingSlashes(mount ?? "ssh");
23
+
24
+ // src/ceph/ceph-auth-form.ts
25
+ var capsEqual = (a, b) => a.mon === b.mon && a.osd === b.osd && a.mgr === b.mgr;
26
+ var cephCliPath = (mount, entity) => `${trimTrailingSlashes(mount)}/ceph/${entity}`;
27
+ var cephDataPath = (mount, entity) => `${trimTrailingSlashes(mount)}/data/ceph/${entity}`;
28
+ var identityMoved = (news, output) => output !== undefined && (output.entity !== news.entity || output.baoPath !== cephCliPath(news.mount, news.entity));
29
+ var planCephAuthEntity = (news, output) => {
30
+ if (output === undefined)
31
+ return;
32
+ if (identityMoved(news, output))
33
+ return { action: "replace" };
34
+ return capsEqual(news.caps, output.caps) ? { action: "noop" } : { action: "update" };
35
+ };
36
+ var attributesOf = (props, fingerprint, node) => ({
37
+ baoPath: cephCliPath(props.mount, props.entity),
38
+ caps: props.caps,
39
+ entity: props.entity,
40
+ fingerprint,
41
+ node
42
+ });
43
+
44
+ // src/ceph/ceph-auth-reconcile.ts
45
+ import * as Effect2 from "effect/Effect";
46
+
47
+ // src/openbao/bao-http.ts
48
+ import * as Cause from "effect/Cause";
49
+ import * as Context from "effect/Context";
50
+ import * as Effect from "effect/Effect";
51
+ import * as Option from "effect/Option";
52
+ import * as Schedule from "effect/Schedule";
53
+ import * as Semaphore from "effect/Semaphore";
54
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
55
+ import * as Headers from "effect/unstable/http/Headers";
56
+ import * as HttpClient from "effect/unstable/http/HttpClient";
57
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
58
+
59
+ // src/openbao/bao-address.ts
60
+ var DEFAULT_ADDRESS = "https://127.0.0.1:8200";
61
+ var UNIX = "unix://";
62
+ var readBaoVariable = (env, name) => env[name] ?? env[`VAULT_${name.slice("BAO_".length)}`] ?? "";
63
+ var resolveAddress = (env) => {
64
+ const agent = readBaoVariable(env, "BAO_AGENT_ADDR");
65
+ const direct = readBaoVariable(env, "BAO_ADDR");
66
+ const raw = agent !== "" ? agent : direct !== "" ? direct : DEFAULT_ADDRESS;
67
+ const namespace = readBaoVariable(env, "BAO_NAMESPACE");
68
+ const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
69
+ return {
70
+ base: socket === undefined ? raw.replace(/\/+$/, "") : "http://localhost",
71
+ namespace: namespace === "" ? undefined : namespace,
72
+ socket
73
+ };
74
+ };
75
+ var headersFor = (address, env) => {
76
+ const headers = { "X-Vault-Request": "true" };
77
+ if (address.namespace !== undefined)
78
+ headers["X-Vault-Namespace"] = address.namespace;
79
+ const token = readBaoVariable(env, "BAO_TOKEN");
80
+ if (token !== "")
81
+ headers["X-Vault-Token"] = token;
82
+ return headers;
83
+ };
84
+
85
+ // src/openbao/bao-status.ts
86
+ class BaoError extends Error {
87
+ status;
88
+ method;
89
+ path;
90
+ errors;
91
+ constructor(status, method, path, errors) {
92
+ const code = status === 0 ? "no response" : String(status);
93
+ super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join("; ") || "(no errors given)"}`);
94
+ this.status = status;
95
+ this.method = method;
96
+ this.path = path;
97
+ this.errors = errors;
98
+ this.name = "BaoError";
99
+ }
100
+ }
101
+ var objectOf = (text) => {
102
+ try {
103
+ const parsed = JSON.parse(text);
104
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
105
+ } catch {
106
+ return;
107
+ }
108
+ };
109
+ var errorsOf = (text) => {
110
+ const raw = objectOf(text)?.["errors"];
111
+ return Array.isArray(raw) && raw.every((entry) => typeof entry === "string") ? raw : undefined;
112
+ };
113
+ var settle = (intent, method, path, status, text) => {
114
+ const fail = (errors) => ({
115
+ error: new BaoError(status, method, path, errors)
116
+ });
117
+ if (status < 200 || status >= 300) {
118
+ const errors = errorsOf(text);
119
+ if (errors === undefined) {
120
+ return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);
121
+ }
122
+ if (status === 404 && intent !== "write")
123
+ return { body: undefined };
124
+ return fail(errors);
125
+ }
126
+ if (text.trim() === "") {
127
+ return intent === "read" ? fail(["a successful read with an empty body"]) : { body: undefined };
128
+ }
129
+ const body = objectOf(text);
130
+ return body === undefined ? fail(["a success body that is not a JSON object"]) : { body };
131
+ };
132
+
133
+ // src/openbao/bao-http.ts
134
+ var BaoEnv = Context.Reference("homeflare/openbao/BaoEnv", {
135
+ defaultValue: () => process.env
136
+ });
137
+ var MAX_IN_FLIGHT = 8;
138
+ var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
139
+ defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
140
+ });
141
+ var BAO_TIMEOUT = "60 seconds";
142
+ var retryTransport = (effect) => Effect.retry(effect, {
143
+ schedule: Schedule.spaced("750 millis"),
144
+ times: 2,
145
+ while: (error) => error instanceof BaoError && error.status === 0 || Cause.isTimeoutError(error)
146
+ });
147
+ var redactingToken = (effect) => Effect.gen(function* () {
148
+ const names = yield* Headers.CurrentRedactedNames;
149
+ return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
150
+ ...names,
151
+ "x-vault-token"
152
+ ]);
153
+ });
154
+ var overSocket = (socket, method, path) => (effect) => {
155
+ if (socket === undefined)
156
+ return effect;
157
+ if (typeof Bun === "undefined") {
158
+ return Effect.fail(new BaoError(0, method, path, ["a unix:// address needs Bun fetch; this is not Bun"]));
159
+ }
160
+ return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
161
+ const init = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
162
+ return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
163
+ });
164
+ };
165
+ var baoCall = (intent, method, path, body) => Effect.gen(function* () {
166
+ const env = yield* BaoEnv;
167
+ const address = resolveAddress(env);
168
+ const client = yield* HttpClient.HttpClient;
169
+ const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(HttpClientRequest.setHeaders(headersFor(address, env)), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
170
+ const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text2) => ({ status: response.status, text: text2 }))), Effect.timeout(BAO_TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
171
+ const gate = yield* BaoGate;
172
+ const { status, text } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
173
+ const outcome = settle(intent, method, path, status, text);
174
+ return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
175
+ });
176
+ var baoRead = (path) => Effect.map(baoCall("read", "GET", path), (body) => {
177
+ if (body === undefined)
178
+ return;
179
+ const data = body["data"];
180
+ return typeof data === "object" && data !== null ? data : body;
181
+ });
182
+ var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, path, body));
183
+ var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
184
+
185
+ // src/openbao/digest.ts
186
+ var canonical = (text) => text.trimEnd();
187
+ var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
188
+
189
+ // src/ceph/ceph-config-options.ts
190
+ var NAMED_CONFIG_OPTIONS = new Set([]);
191
+ var LOCKOUT_EXACT = new Set([
192
+ "mon_host",
193
+ "public_network",
194
+ "cluster_network",
195
+ "public_addr",
196
+ "cluster_addr"
197
+ ]);
198
+ var LOCKOUT_PATTERN = /^auth_\w*_required$|(^|_)key(ring)?($|_)/;
199
+ var isLockoutConfigOption = (name) => LOCKOUT_EXACT.has(name) || LOCKOUT_PATTERN.test(name);
200
+
201
+ // src/ceph/ceph-argv-config.ts
202
+ var WHO = /^(global|mon|osd|mgr|mds|client)(\.[A-Za-z0-9_-]{1,32})?$/;
203
+ var OPTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
204
+ var CONFIG_VALUE = /^(?:[A-Za-z0-9][A-Za-z0-9 ._:/=,-]{0,255}|-[0-9]{1,255})$/;
205
+ var configValueProblem = (value) => CONFIG_VALUE.test(value) ? undefined : `config value ${JSON.stringify(value)} contains characters outside the safe set`;
206
+ var namedOptionProblem = (name) => {
207
+ if (!OPTION_NAME.test(name))
208
+ return `${JSON.stringify(name)} is not a plain ceph option name`;
209
+ if (isLockoutConfigOption(name))
210
+ return `${name} is a lockout-class option and can never be declared`;
211
+ return NAMED_CONFIG_OPTIONS.has(name) ? undefined : `${name} is not on the named option list (it starts empty — see ceph-config-options.ts)`;
212
+ };
213
+ var configProblem = (rest) => {
214
+ const [verb, who, name, ...extra] = rest;
215
+ if (who === undefined || !WHO.test(who)) {
216
+ return "config takes a bounded `<who>` (global/mon/osd/mgr/mds/client[.id])";
217
+ }
218
+ if (name === undefined)
219
+ return "config takes a `<name>` from the named option list";
220
+ if (verb === "get" || verb === "rm") {
221
+ return extra.length === 0 ? namedOptionProblem(name) : `config ${String(verb)} takes exactly \`<who> <name>\``;
222
+ }
223
+ if (verb === "set") {
224
+ const [value, ...more] = extra;
225
+ if (value === undefined || more.length !== 0)
226
+ return "config set takes exactly `<who> <name> <value>`";
227
+ const named = namedOptionProblem(name);
228
+ return named !== undefined ? named : configValueProblem(value);
229
+ }
230
+ return `ceph config ${String(verb)} is not on the allowlist`;
231
+ };
232
+
233
+ // src/ceph/ceph-argv.ts
234
+ var CEPH_BIN = "/usr/bin/ceph";
235
+ var ENTITY_PREFIX = "client.k8s-";
236
+ var ENTITY = /^client\.k8s-[a-z0-9][a-z0-9-]{0,61}$/;
237
+ var boundedEntityProblem = (entity) => ENTITY.test(entity) ? undefined : `entity must match \`${ENTITY_PREFIX}<name>\` (lowercase, digits, hyphens), got ${JSON.stringify(entity)}`;
238
+ var MON_CAP = /^profile rbd$/;
239
+ var POOL_SCOPED_CAP = /^profile rbd pool=[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
240
+ var DAEMONS = ["mon", "osd", "mgr"];
241
+ var capProblem = (daemon, value) => {
242
+ if (value === undefined)
243
+ return `${daemon} cap is missing`;
244
+ const shape = daemon === "mon" ? MON_CAP : POOL_SCOPED_CAP;
245
+ return shape.test(value) ? undefined : `${daemon} cap ${JSON.stringify(value)} is not the design's plain pool-scoped rbd profile`;
246
+ };
247
+ var authGetProblem = (ops) => {
248
+ const [entity, ...extra] = ops;
249
+ if (entity === undefined || extra.length !== 0)
250
+ return "auth get takes exactly `<entity>`";
251
+ return boundedEntityProblem(entity);
252
+ };
253
+ var capsTripleProblem = (ops) => {
254
+ const [entity, ...pairs] = ops;
255
+ if (entity === undefined)
256
+ return `takes exactly \`<entity> ${DAEMONS.map((d) => `${d} <cap>`).join(" ")}\``;
257
+ const entityProblem = boundedEntityProblem(entity);
258
+ if (entityProblem !== undefined)
259
+ return entityProblem;
260
+ if (pairs.length !== DAEMONS.length * 2) {
261
+ return `takes exactly \`<entity> ${DAEMONS.map((d) => `${d} <cap>`).join(" ")}\``;
262
+ }
263
+ for (const [index, daemon] of DAEMONS.entries()) {
264
+ const key = pairs[index * 2];
265
+ const value = pairs[index * 2 + 1];
266
+ if (key !== daemon)
267
+ return `expected \`${daemon}\` in position ${String(index + 1)}, got ${JSON.stringify(key)}`;
268
+ const problem = capProblem(daemon, value);
269
+ if (problem !== undefined)
270
+ return problem;
271
+ }
272
+ return;
273
+ };
274
+ var authProblem = (rest) => {
275
+ const [verb, ...ops] = rest;
276
+ if (verb === "ls") {
277
+ return "auth ls prints every key on the cluster; use the committed read-only inventory " + "script instead, never this transport";
278
+ }
279
+ if (verb === "del")
280
+ return "auth del is refused entirely — lockout safety, never auto-delete (D3)";
281
+ if (verb === "get")
282
+ return authGetProblem(ops);
283
+ if (verb === "get-or-create")
284
+ return capsTripleProblem(ops);
285
+ if (verb === "caps")
286
+ return capsTripleProblem(ops);
287
+ return `ceph auth ${String(verb)} is not on the allowlist`;
288
+ };
289
+ var JSON_FORMAT = ["-f", "json"];
290
+ var withJsonSuffix = (rest, check) => {
291
+ const suffix = rest.slice(-2);
292
+ if (suffix[0] !== JSON_FORMAT[0] || suffix[1] !== JSON_FORMAT[1]) {
293
+ return "every ceph call in this allowlist ends `-f json`";
294
+ }
295
+ return check(rest.slice(0, -2));
296
+ };
297
+ var cephCommandProblem = (argv) => {
298
+ const [bin, group, ...rest] = argv;
299
+ if (bin !== CEPH_BIN)
300
+ return `${JSON.stringify(bin)} is not the allowlisted ceph binary`;
301
+ if (group === "quorum_status") {
302
+ return rest.length === 2 && rest[0] === JSON_FORMAT[0] && rest[1] === JSON_FORMAT[1] ? undefined : "quorum_status takes exactly `-f json`";
303
+ }
304
+ if (group === "auth")
305
+ return withJsonSuffix(rest, authProblem);
306
+ if (group === "config")
307
+ return withJsonSuffix(rest, configProblem);
308
+ return `ceph ${String(group)} is not on the allowlist`;
309
+ };
310
+ var authGetArgv = (entity) => [
311
+ CEPH_BIN,
312
+ "auth",
313
+ "get",
314
+ entity,
315
+ ...JSON_FORMAT
316
+ ];
317
+ var authGetOrCreateArgv = (entity, caps) => [
318
+ CEPH_BIN,
319
+ "auth",
320
+ "get-or-create",
321
+ entity,
322
+ ...DAEMONS.flatMap((daemon) => [daemon, caps[daemon]]),
323
+ ...JSON_FORMAT
324
+ ];
325
+ var authCapsArgv = (entity, caps) => [
326
+ CEPH_BIN,
327
+ "auth",
328
+ "caps",
329
+ entity,
330
+ ...DAEMONS.flatMap((daemon) => [daemon, caps[daemon]]),
331
+ ...JSON_FORMAT
332
+ ];
333
+ var quorumStatusArgv = () => [
334
+ CEPH_BIN,
335
+ "quorum_status",
336
+ ...JSON_FORMAT
337
+ ];
338
+
339
+ // src/ceph/ceph-auth-parse.ts
340
+ var isRecord = (value) => typeof value === "object" && value !== null;
341
+ var parseCaps = (value, entity) => {
342
+ if (!isRecord(value))
343
+ throw new Error(`ceph auth: ${entity}'s entry has no caps object`);
344
+ const { mon, osd, mgr } = value;
345
+ if (typeof mon !== "string" || typeof osd !== "string" || typeof mgr !== "string") {
346
+ throw new Error(`ceph auth: ${entity}'s caps are missing a mon, osd or mgr string`);
347
+ }
348
+ return { mgr, mon, osd };
349
+ };
350
+ var parseKeyringEntry = (stdout, entity) => {
351
+ let parsed;
352
+ try {
353
+ parsed = JSON.parse(stdout);
354
+ } catch {
355
+ throw new Error(`ceph auth ${entity}: stdout did not parse as JSON`);
356
+ }
357
+ const row = Array.isArray(parsed) ? parsed.find((entry) => isRecord(entry) && entry["entity"] === entity) : undefined;
358
+ if (!isRecord(row))
359
+ throw new Error(`ceph auth ${entity}: no matching entry in the JSON output`);
360
+ const { key } = row;
361
+ if (typeof key !== "string" || key.trim() === "") {
362
+ throw new Error(`ceph auth ${entity}: the entry has no key field`);
363
+ }
364
+ return { caps: parseCaps(row["caps"], entity), key };
365
+ };
366
+ var parseAuthGet = (result, entity) => {
367
+ if (result.exitCode !== 0) {
368
+ if (result.stderr.includes("ENOENT"))
369
+ return { kind: "absent" };
370
+ throw new Error(`ceph auth get ${entity} failed: ${result.stderr.trim().slice(0, 300)}`);
371
+ }
372
+ const { caps } = parseKeyringEntry(result.stdout, entity);
373
+ return { caps, kind: "present" };
374
+ };
375
+ var parseAuthGetOrCreate = (result, entity) => {
376
+ if (result.exitCode !== 0) {
377
+ throw new Error(`ceph auth get-or-create ${entity} failed: ${result.stderr.trim().slice(0, 300)}`);
378
+ }
379
+ return parseKeyringEntry(result.stdout, entity);
380
+ };
381
+ var parseQuorumStatus = (stdout) => {
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(stdout);
385
+ } catch (cause) {
386
+ return { healthy: false, reason: `stdout did not parse as JSON (${String(cause)})` };
387
+ }
388
+ if (!isRecord(parsed))
389
+ return { healthy: false, reason: "stdout was not a JSON object" };
390
+ const quorum = parsed["quorum"];
391
+ return Array.isArray(quorum) && quorum.length > 0 ? { healthy: true } : { healthy: false, reason: "no non-empty `quorum` array in the quorum_status output" };
392
+ };
393
+
394
+ // src/ceph/ceph-transport.ts
395
+ import { randomBytes as randomBytes2 } from "node:crypto";
396
+
397
+ // src/linux/ssh-command.ts
398
+ var ABSENT = 66;
399
+ var shellQuote = (arg) => {
400
+ if (arg.includes("\x00"))
401
+ throw new Error("ssh: an argument contains NUL");
402
+ return `'${arg.replaceAll("'", `'\\''`)}'`;
403
+ };
404
+ var quoteArgv = (argv) => argv.map(shellQuote).join(" ");
405
+ var marker = (nonce) => `__HF_RC_${nonce}=`;
406
+ var frameScript = (script, nonce) => `(
407
+ ${script}
408
+ )
409
+ __hf=$?
410
+ printf '\\n${marker(nonce)}%s\\n' "$__hf" >&2
411
+ exit "$__hf"`;
412
+ var parseFramed = (stderr, nonce) => {
413
+ const at = stderr.lastIndexOf(`
414
+ ${marker(nonce)}`);
415
+ if (at < 0)
416
+ return;
417
+ const digits = /^(\d+)\s*$/.exec(stderr.slice(at + marker(nonce).length + 1));
418
+ if (digits?.[1] === undefined)
419
+ return;
420
+ return { exitCode: Number(digits[1]), stderr: stderr.slice(0, at) };
421
+ };
422
+ var sshArgv = (host, script, options) => {
423
+ if (host === "" || host.startsWith("-")) {
424
+ throw new Error(`ssh: ${JSON.stringify(host)} is not a usable destination`);
425
+ }
426
+ return [
427
+ "ssh",
428
+ "-T",
429
+ "-o",
430
+ "BatchMode=yes",
431
+ "-o",
432
+ `ConnectTimeout=${String(options.connectTimeoutSec)}`,
433
+ ...options.sshArgs ?? [],
434
+ host,
435
+ `/bin/sh -c ${shellQuote(script)}`
436
+ ];
437
+ };
438
+
439
+ // src/linux/ssh-runner.ts
440
+ import { spawn } from "node:child_process";
441
+ import { randomBytes } from "node:crypto";
442
+
443
+ // src/launchd/host-lookup.ts
444
+ var NUMERIC = /^\d+$/;
445
+ var userQuery = (platform, nameOrId) => platform === "darwin" ? [
446
+ "/usr/bin/dscacheutil",
447
+ "-q",
448
+ "user",
449
+ "-a",
450
+ NUMERIC.test(nameOrId) ? "uid" : "name",
451
+ nameOrId
452
+ ] : ["getent", "passwd", nameOrId];
453
+ var groupQuery = (platform, nameOrId) => platform === "darwin" ? [
454
+ "/usr/bin/dscacheutil",
455
+ "-q",
456
+ "group",
457
+ "-a",
458
+ NUMERIC.test(nameOrId) ? "gid" : "name",
459
+ nameOrId
460
+ ] : ["getent", "group", nameOrId];
461
+ var firstRecord = (stdout) => {
462
+ const fields = new Map;
463
+ for (const line of stdout.split(`
464
+ `)) {
465
+ if (line.trim() === "") {
466
+ if (fields.size > 0)
467
+ break;
468
+ continue;
469
+ }
470
+ const at = line.indexOf(": ");
471
+ if (at <= 0)
472
+ continue;
473
+ const key = line.slice(0, at);
474
+ if (key === "password")
475
+ continue;
476
+ fields.set(key, line.slice(at + 2).trim());
477
+ }
478
+ return fields;
479
+ };
480
+ var toId = (text) => text !== undefined && NUMERIC.test(text) ? Number(text) : undefined;
481
+ var parseUser = (platform, stdout) => {
482
+ if (platform === "darwin") {
483
+ const record = firstRecord(stdout);
484
+ const uid2 = toId(record.get("uid"));
485
+ const gid2 = toId(record.get("gid"));
486
+ const home2 = record.get("dir");
487
+ return uid2 === undefined || gid2 === undefined || home2 === undefined ? undefined : { gid: gid2, home: home2, uid: uid2 };
488
+ }
489
+ const fields = (stdout.split(`
490
+ `)[0] ?? "").split(":");
491
+ const uid = toId(fields[2]);
492
+ const gid = toId(fields[3]);
493
+ const home = fields[5];
494
+ return uid === undefined || gid === undefined || home === undefined || home === "" ? undefined : { gid, home, uid };
495
+ };
496
+ var parseGroup = (platform, stdout) => {
497
+ if (platform === "darwin")
498
+ return toId(firstRecord(stdout).get("gid"));
499
+ return toId((stdout.split(`
500
+ `)[0] ?? "").split(":")[2]);
501
+ };
502
+
503
+ // src/linux/ssh-scripts.ts
504
+ var IF_PRESENT = (path) => `if [ -e ${path} ] || [ -L ${path} ]; then`;
505
+ var statScript = (path) => {
506
+ const quoted = shellQuote(path);
507
+ return `${IF_PRESENT(quoted)} stat -c '%f %a %u %g %s' -- ${quoted}; else exit ${String(ABSENT)}; fi`;
508
+ };
509
+ var readScript = (path) => {
510
+ const quoted = shellQuote(path);
511
+ return `${IF_PRESENT(quoted)} base64 -- ${quoted}; else exit ${String(ABSENT)}; fi`;
512
+ };
513
+ var removeScript = (path) => `rm -f -- ${shellQuote(path)}`;
514
+ var execScript = (argv) => quoteArgv(argv);
515
+ var writeScript = (path, temp, options) => {
516
+ const target = shellQuote(path);
517
+ const staged = shellQuote(temp);
518
+ const bail = `{ rm -f -- ${staged}; exit`;
519
+ const owner = options.uid === undefined ? options.gid === undefined ? undefined : `:${String(options.gid)}` : options.gid === undefined ? String(options.uid) : `${String(options.uid)}:${String(options.gid)}`;
520
+ const chown = owner === undefined ? [] : [`chown ${owner} -- ${staged} || ${bail} 72; }`];
521
+ return [
522
+ "set -C",
523
+ "umask 077",
524
+ `cat > ${staged} || ${bail} 71; }`,
525
+ ...chown,
526
+ `chmod ${options.mode.toString(8).padStart(3, "0")} -- ${staged} || ${bail} 73; }`,
527
+ `mv -f -- ${staged} ${target} || ${bail} 74; }`
528
+ ].join(`
529
+ `);
530
+ };
531
+ var TYPE_BITS = 61440;
532
+ var KINDS = new Map([
533
+ [32768, "file"],
534
+ [16384, "directory"],
535
+ [40960, "symlink"]
536
+ ]);
537
+ var parseStat = (stdout) => {
538
+ const fields = stdout.trim().split(/\s+/);
539
+ if (fields.length !== 5)
540
+ return;
541
+ const [rawMode, perms, uid, gid, size] = fields;
542
+ const raw = Number.parseInt(rawMode, 16);
543
+ const mode = Number.parseInt(perms, 8);
544
+ const numbers = [raw, mode, Number(uid), Number(gid), Number(size)];
545
+ if (numbers.some((value) => !Number.isSafeInteger(value) || value < 0))
546
+ return;
547
+ return {
548
+ gid: Number(gid),
549
+ kind: KINDS.get(raw & TYPE_BITS) ?? "other",
550
+ mode,
551
+ size: Number(size),
552
+ uid: Number(uid)
553
+ };
554
+ };
555
+
556
+ // src/linux/ssh-runner.ts
557
+ var spawnSsh = (argv, timeoutMs, stdin) => new Promise((resolve, reject) => {
558
+ const [program, ...args] = argv;
559
+ const child = spawn(program, args, {
560
+ shell: false,
561
+ stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
562
+ timeout: timeoutMs
563
+ });
564
+ const out = [];
565
+ const err = [];
566
+ child.stdout?.on("data", (chunk) => out.push(chunk));
567
+ child.stderr?.on("data", (chunk) => err.push(chunk));
568
+ child.on("error", (cause) => {
569
+ reject(new Error(`ssh: ${cause.message}`));
570
+ });
571
+ if (stdin !== undefined && child.stdin !== null) {
572
+ child.stdin.on("error", () => {
573
+ return;
574
+ });
575
+ child.stdin.end(stdin);
576
+ }
577
+ child.on("close", (exitCode, signal) => {
578
+ resolve({
579
+ exitCode: exitCode ?? -1,
580
+ signal,
581
+ stderr: Buffer.concat(err).toString("utf8"),
582
+ stdout: Buffer.concat(out).toString("utf8")
583
+ });
584
+ });
585
+ });
586
+ var runRemote = async (options, nonce, script, stdin) => {
587
+ const argv = sshArgv(options.host, frameScript(script, nonce), {
588
+ connectTimeoutSec: options.connectTimeoutSec,
589
+ ...options.sshArgs === undefined ? {} : { sshArgs: options.sshArgs }
590
+ });
591
+ const raw = await spawnSsh(argv, options.execTimeoutMs, stdin);
592
+ const framed = parseFramed(raw.stderr, nonce);
593
+ if (framed === undefined) {
594
+ throw new Error(`ssh ${options.host}: the remote command did not report a status (ssh exit ` + `${String(raw.exitCode)}${raw.signal === null ? "" : `, killed by ${raw.signal}`}): ` + `${raw.stderr.trim().slice(0, 300)}`);
595
+ }
596
+ return { exitCode: framed.exitCode, stderr: framed.stderr, stdout: raw.stdout };
597
+ };
598
+ var decodeBase64 = (text) => new Uint8Array(Buffer.from(text.replaceAll(`
599
+ `, ""), "base64"));
600
+ var failed = (host, what, result) => new Error(`ssh ${host}: ${what} -> ${String(result.exitCode)}: ${result.stderr.trim().slice(0, 300)}`);
601
+ var sshRunner = async (options) => {
602
+ const settings = {
603
+ connectTimeoutSec: options.connectTimeoutSec ?? 10,
604
+ execTimeoutMs: options.execTimeoutMs ?? 120000,
605
+ host: options.host,
606
+ ...options.sshArgs === undefined ? {} : { sshArgs: options.sshArgs }
607
+ };
608
+ const nonce = randomBytes(8).toString("hex");
609
+ const run = (script, stdin) => runRemote(settings, nonce, script, stdin);
610
+ const probe = await run("uname -s; id -u");
611
+ if (probe.exitCode !== 0)
612
+ throw failed(options.host, "uname -s; id -u", probe);
613
+ const [system, uid] = probe.stdout.trim().split(`
614
+ `);
615
+ if (system !== "Linux" || uid === undefined || !/^\d+$/.test(uid)) {
616
+ throw new Error(`ssh ${options.host}: expected a Linux host, got ${JSON.stringify(probe.stdout.trim())}. ` + "sshRunner() sends GNU/BusyBox argv (stat -c, base64, mv -f); for a Mac use localRunner().");
617
+ }
618
+ const euid = Number(uid);
619
+ return {
620
+ effectiveUid: () => euid,
621
+ exec: (argv) => run(execScript(argv)),
622
+ lookupGroup: async (nameOrId) => {
623
+ const result = await run(execScript(groupQuery("linux", nameOrId)));
624
+ return result.exitCode === 0 ? parseGroup("linux", result.stdout) : undefined;
625
+ },
626
+ lookupUser: async (nameOrId) => {
627
+ const result = await run(execScript(userQuery("linux", nameOrId)));
628
+ return result.exitCode === 0 ? parseUser("linux", result.stdout) : undefined;
629
+ },
630
+ privileged: false,
631
+ readFile: async (path) => {
632
+ const result = await run(readScript(path));
633
+ if (result.exitCode === ABSENT)
634
+ return;
635
+ if (result.exitCode !== 0)
636
+ throw failed(options.host, `read ${path}`, result);
637
+ return decodeBase64(result.stdout);
638
+ },
639
+ removeFile: async (path) => {
640
+ const result = await run(removeScript(path));
641
+ if (result.exitCode !== 0)
642
+ throw failed(options.host, `remove ${path}`, result);
643
+ },
644
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
645
+ stat: async (path) => {
646
+ const result = await run(statScript(path));
647
+ if (result.exitCode === ABSENT)
648
+ return;
649
+ if (result.exitCode !== 0)
650
+ throw failed(options.host, `stat ${path}`, result);
651
+ const parsed = parseStat(result.stdout);
652
+ if (parsed === undefined) {
653
+ throw new Error(`ssh ${options.host}: stat ${path} printed an unexpected shape`);
654
+ }
655
+ return parsed;
656
+ },
657
+ writeFileAtomic: async (path, bytes, write) => {
658
+ const at = path.lastIndexOf("/");
659
+ const temp = `${path.slice(0, at)}/.${path.slice(at + 1)}.${randomBytes(6).toString("hex")}.tmp`;
660
+ const result = await run(writeScript(path, temp, write), bytes);
661
+ if (result.exitCode !== 0)
662
+ throw failed(options.host, `write ${path}`, result);
663
+ }
664
+ };
665
+ };
666
+
667
+ // src/launchd/job-form.ts
668
+ import { createHash } from "node:crypto";
669
+
670
+ // src/launchd/plist.ts
671
+ class PlistError extends Error {
672
+ path;
673
+ constructor(path, detail) {
674
+ super(`plist ${path === "" ? "(root)" : path}: ${detail}`);
675
+ this.path = path;
676
+ this.name = "PlistError";
677
+ }
678
+ }
679
+ var HEADER = `<?xml version="1.0" encoding="UTF-8"?>
680
+ ` + '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' + `"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
681
+ ` + `<plist version="1.0">
682
+ `;
683
+ var MAX_DEPTH = 32;
684
+ var FORBIDDEN = /[\x00-\x08\x0B\x0C\x0E-\x1F\uD800-\uDFFF\uFFFE\uFFFF]/u;
685
+ var escapeText = (text, path) => {
686
+ const bad = FORBIDDEN.exec(text);
687
+ if (bad !== null) {
688
+ const code = bad[0].codePointAt(0) ?? 0;
689
+ throw new PlistError(path, `U+${code.toString(16).toUpperCase().padStart(4, "0")} cannot appear in XML 1.0`);
690
+ }
691
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\r", "&#13;");
692
+ };
693
+ var isPlainObject = (value) => {
694
+ const proto = Object.getPrototypeOf(value);
695
+ return proto === Object.prototype || proto === null;
696
+ };
697
+ var renderValue = (value, path, depth, out) => {
698
+ const pad = "\t".repeat(depth);
699
+ if (depth > MAX_DEPTH)
700
+ throw new PlistError(path, `nested deeper than ${String(MAX_DEPTH)}`);
701
+ if (typeof value === "string") {
702
+ out.push(`${pad}<string>${escapeText(value, path)}</string>`);
703
+ return;
704
+ }
705
+ if (typeof value === "boolean") {
706
+ out.push(`${pad}<${value ? "true" : "false"}/>`);
707
+ return;
708
+ }
709
+ if (typeof value === "number") {
710
+ if (!Number.isSafeInteger(value)) {
711
+ throw new PlistError(path, `${String(value)} is not a safe integer (no <real> support)`);
712
+ }
713
+ out.push(`${pad}<integer>${String(value === 0 ? 0 : value)}</integer>`);
714
+ return;
715
+ }
716
+ if (Array.isArray(value)) {
717
+ if (value.length === 0) {
718
+ out.push(`${pad}<array/>`);
719
+ return;
720
+ }
721
+ out.push(`${pad}<array>`);
722
+ for (const [index, item] of value.entries()) {
723
+ renderValue(item, `${path}[${String(index)}]`, depth + 1, out);
724
+ }
725
+ out.push(`${pad}</array>`);
726
+ return;
727
+ }
728
+ if (typeof value === "object" && value !== null && isPlainObject(value)) {
729
+ renderDict(value, path, depth, out);
730
+ return;
731
+ }
732
+ const kind = value === null ? "null" : typeof value;
733
+ throw new PlistError(path, `${kind} has no plist form — omit the key instead`);
734
+ };
735
+ var renderDict = (dict, path, depth, out) => {
736
+ const pad = "\t".repeat(depth);
737
+ const keys = Object.keys(dict).sort();
738
+ if (keys.length === 0) {
739
+ out.push(`${pad}<dict/>`);
740
+ return;
741
+ }
742
+ out.push(`${pad}<dict>`);
743
+ for (const key of keys) {
744
+ const child = path === "" ? key : `${path}.${key}`;
745
+ out.push(`${pad} <key>${escapeText(key, child)}</key>`);
746
+ renderValue(dict[key], child, depth + 1, out);
747
+ }
748
+ out.push(`${pad}</dict>`);
749
+ };
750
+ var renderPlist = (dict) => {
751
+ if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !isPlainObject(dict)) {
752
+ throw new PlistError("", "the top level of a launchd plist must be a dict");
753
+ }
754
+ const out = [];
755
+ renderDict(dict, "", 0, out);
756
+ return `${HEADER}${out.join(`
757
+ `)}
758
+ </plist>
759
+ `;
760
+ };
761
+
762
+ // src/launchd/job-form.ts
763
+ var parseDomain = (domain) => {
764
+ if (domain === "system")
765
+ return { kind: "system" };
766
+ const match = /^gui\/(\d+)$/.exec(domain);
767
+ return match?.[1] === undefined ? undefined : { kind: "gui", uid: Number(match[1]) };
768
+ };
769
+ var serviceTarget = (label, domain) => `${domain}/${label}`;
770
+ var plistPathFor = (label, domain, home) => {
771
+ if (domain.kind === "system")
772
+ return `/Library/LaunchDaemons/${label}.plist`;
773
+ if (home === undefined || !home.startsWith("/")) {
774
+ throw new Error(`gui/${String(domain.uid)}: no home directory for that uid on this host`);
775
+ }
776
+ return `${home}/Library/LaunchAgents/${label}.plist`;
777
+ };
778
+ var OWNED_KEYS = [
779
+ "EnvironmentVariables",
780
+ "GroupName",
781
+ "KeepAlive",
782
+ "Label",
783
+ "Program",
784
+ "ProgramArguments",
785
+ "RunAtLoad",
786
+ "StandardErrorPath",
787
+ "StandardOutPath",
788
+ "StartCalendarInterval",
789
+ "StartInterval",
790
+ "ThrottleInterval",
791
+ "UserName",
792
+ "WorkingDirectory"
793
+ ];
794
+ var calendarDict = (entry) => {
795
+ const dict = {};
796
+ if (entry.minute !== undefined)
797
+ dict["Minute"] = entry.minute;
798
+ if (entry.hour !== undefined)
799
+ dict["Hour"] = entry.hour;
800
+ if (entry.day !== undefined)
801
+ dict["Day"] = entry.day;
802
+ if (entry.weekday !== undefined)
803
+ dict["Weekday"] = entry.weekday;
804
+ if (entry.month !== undefined)
805
+ dict["Month"] = entry.month;
806
+ return dict;
807
+ };
808
+ var keepAliveValue = (keepAlive) => {
809
+ if (typeof keepAlive === "boolean")
810
+ return keepAlive;
811
+ const dict = {};
812
+ if (keepAlive.successfulExit !== undefined)
813
+ dict["SuccessfulExit"] = keepAlive.successfulExit;
814
+ if (keepAlive.crashed !== undefined)
815
+ dict["Crashed"] = keepAlive.crashed;
816
+ return dict;
817
+ };
818
+ var jobPlistDict = (props) => {
819
+ const dict = { ...props.extraKeys };
820
+ dict["Label"] = props.label;
821
+ dict["ProgramArguments"] = [...props.programArguments];
822
+ const env = props.environment ?? {};
823
+ if (Object.keys(env).length > 0)
824
+ dict["EnvironmentVariables"] = { ...env };
825
+ if (props.runAtLoad !== undefined)
826
+ dict["RunAtLoad"] = props.runAtLoad;
827
+ if (props.keepAlive !== undefined)
828
+ dict["KeepAlive"] = keepAliveValue(props.keepAlive);
829
+ if (props.startInterval !== undefined)
830
+ dict["StartInterval"] = props.startInterval;
831
+ const calendar = props.startCalendarInterval;
832
+ if (calendar !== undefined) {
833
+ dict["StartCalendarInterval"] = Array.isArray(calendar) ? calendar.map(calendarDict) : calendarDict(calendar);
834
+ }
835
+ if (props.throttleInterval !== undefined)
836
+ dict["ThrottleInterval"] = props.throttleInterval;
837
+ if (props.standardOutPath !== undefined)
838
+ dict["StandardOutPath"] = props.standardOutPath;
839
+ if (props.standardErrorPath !== undefined)
840
+ dict["StandardErrorPath"] = props.standardErrorPath;
841
+ if (props.workingDirectory !== undefined)
842
+ dict["WorkingDirectory"] = props.workingDirectory;
843
+ if (props.userName !== undefined)
844
+ dict["UserName"] = props.userName;
845
+ if (props.groupName !== undefined)
846
+ dict["GroupName"] = props.groupName;
847
+ return dict;
848
+ };
849
+ var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
850
+ var renderJob = (props) => {
851
+ const text = renderPlist(jobPlistDict(props));
852
+ const bytes = new TextEncoder().encode(text);
853
+ return { bytes, sha256: sha256Hex(bytes), text };
854
+ };
855
+
856
+ // src/linux/unit-text.ts
857
+ var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
858
+ var hasInstallSection = (text) => {
859
+ if (!text.includes("[Install]"))
860
+ return false;
861
+ const after = text.slice(text.indexOf("[Install]"));
862
+ return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
863
+ };
864
+ var firstSubstantiveLine = (text) => text.split(`
865
+ `).find((line) => !(line.trim() === "" || line.startsWith("#") || line.startsWith(";")));
866
+ var textProblems = (text) => {
867
+ const found = [];
868
+ if (text.trim() === "")
869
+ found.push("the unit file is empty");
870
+ if (text.includes("\x00"))
871
+ found.push("the unit file contains NUL");
872
+ const first = firstSubstantiveLine(text);
873
+ if (first === undefined || !/^\s*\[[A-Za-z]+]/.test(first)) {
874
+ found.push("a unit file must begin with a [Section] (blank lines and #/; comments before it are fine)");
875
+ }
876
+ return found;
877
+ };
878
+
879
+ // src/linux/unit-form.ts
880
+ var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
881
+ var UNIT_SEARCH_DIRECTORIES = [
882
+ "/etc/systemd/system",
883
+ "/run/systemd/system",
884
+ "/usr/local/lib/systemd/system",
885
+ "/usr/lib/systemd/system",
886
+ "/lib/systemd/system"
887
+ ];
888
+ var UNIT_WRITE = { gid: 0, mode: 420, uid: 0 };
889
+ var digestOf = (text) => sha256Hex(new TextEncoder().encode(text));
890
+ var configDigest = (restartOn) => digestOf((restartOn ?? []).join(`
891
+ `));
892
+ var isUnitRunning = (activeState, subState, started) => {
893
+ if (activeState === "active")
894
+ return true;
895
+ if (activeState !== "activating")
896
+ return false;
897
+ if (subState === "auto-restart" || subState === "auto-restart-queued")
898
+ return true;
899
+ return started;
900
+ };
901
+ var renderUnit = (sections) => `${sections.map((section) => [`[${section.name}]`, ...section.lines.map(([key, value]) => `${key}=${value}`)].join(`
902
+ `)).join(`
903
+
904
+ `)}
905
+ `;
906
+ var unitText = (props) => props.content ?? renderUnit(props.sections ?? []);
907
+ var unitPathFor = (props) => `${props.directory ?? DEFAULT_UNIT_DIRECTORY}/${props.name}`;
908
+ var NAME = /^[A-Za-z0-9][A-Za-z0-9_.@:-]*\.([a-z]+)$/;
909
+ var TYPES = new Set([
910
+ "automount",
911
+ "device",
912
+ "mount",
913
+ "path",
914
+ "scope",
915
+ "service",
916
+ "slice",
917
+ "socket",
918
+ "swap",
919
+ "target",
920
+ "timer"
921
+ ]);
922
+ var unitProblems = (props, expect) => {
923
+ const found = [];
924
+ const type = NAME.exec(props.name)?.[1];
925
+ if (type === undefined || !TYPES.has(type)) {
926
+ found.push(`name must be <unit>.<type> with a systemd unit type, got ${JSON.stringify(props.name)}`);
927
+ } else if (expect !== undefined && type !== expect) {
928
+ found.push(`this resource declares a .${expect} unit; ${props.name} is a .${type}`);
929
+ }
930
+ if (props.content === undefined === (props.sections === undefined)) {
931
+ found.push("declare exactly one of content or sections");
932
+ }
933
+ const directory = props.directory ?? DEFAULT_UNIT_DIRECTORY;
934
+ if (!directory.startsWith("/") || directory.endsWith("/") || directory.includes("/..")) {
935
+ found.push("directory must be an absolute, normalised path with no trailing slash");
936
+ }
937
+ found.push(...textProblems(unitText(props)));
938
+ if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory)) {
939
+ found.push(`an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(", ")}); ` + `${directory} is not one of them. Move it, or declare enabled: false.`);
940
+ }
941
+ if (props.enabled !== false && !hasInstallSection(unitText(props))) {
942
+ found.push("enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one " + 'systemd calls the unit "static" and `systemctl enable` has nothing to link. Add one, or ' + "declare enabled: false and let something else pull it in.");
943
+ }
944
+ if (type === "timer" && !unitText(props).includes("[Timer]")) {
945
+ found.push("a .timer needs a [Timer] section");
946
+ }
947
+ return found;
948
+ };
949
+
950
+ // src/launchd/secret-tripwire.ts
951
+ var ALTERNATIVE = "props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) " + "write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).";
952
+ var SECRET_NAME = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_KEY|PRIVATE_KEY|SECRET_KEY|CREDENTIALS?)$/i;
953
+ var INLINE_FLAG = /^--?[a-z0-9-]*(?:token|password|passwd|passphrase|secret|api-?key)=./i;
954
+ var BARE_FLAG = /^--?(?:[a-z0-9]+-)*(?:token|password|passwd|passphrase|secret|api-?key)$/i;
955
+ var PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
956
+ var keyLeaves = (value, path) => {
957
+ if (typeof value === "string")
958
+ return PRIVATE_KEY.test(value) ? [path] : [];
959
+ if (Array.isArray(value)) {
960
+ return value.flatMap((item, index) => keyLeaves(item, `${path}[${String(index)}]`));
961
+ }
962
+ if (typeof value === "object" && value !== null) {
963
+ return Object.entries(value).flatMap(([key, item]) => keyLeaves(item, `${path}.${key}`));
964
+ }
965
+ return [];
966
+ };
967
+ var jobSecretProblems = (environment, argv, extraKeys) => {
968
+ const found = [];
969
+ for (const [name, value] of Object.entries(environment ?? {})) {
970
+ if (SECRET_NAME.test(name))
971
+ found.push(`environment ${name} looks like a secret: ${ALTERNATIVE}`);
972
+ else if (PRIVATE_KEY.test(value))
973
+ found.push(`environment ${name} holds a private key: ${ALTERNATIVE}`);
974
+ }
975
+ for (const [index, arg] of argv.entries()) {
976
+ if (INLINE_FLAG.test(arg)) {
977
+ found.push(`programArguments[${String(index)}] passes a secret inline: ${ALTERNATIVE}`);
978
+ } else if (BARE_FLAG.test(arg) && index + 1 < argv.length) {
979
+ found.push(`programArguments[${String(index + 1)}] follows a secret flag: ${ALTERNATIVE}`);
980
+ } else if (PRIVATE_KEY.test(arg)) {
981
+ found.push(`programArguments[${String(index)}] holds a private key: ${ALTERNATIVE}`);
982
+ }
983
+ }
984
+ for (const path of keyLeaves(extraKeys ?? {}, "extraKeys")) {
985
+ found.push(`${path} holds a private key: ${ALTERNATIVE}`);
986
+ }
987
+ return found;
988
+ };
989
+ var fileSecretProblems = (content) => PRIVATE_KEY.test(content) ? [`content holds a private key: ${ALTERNATIVE}`] : [];
990
+
991
+ // src/launchd/host-file-form.ts
992
+ var DEFAULT_MODE = 420;
993
+ var identityProblems = (name, value) => {
994
+ if (value === undefined)
995
+ return [];
996
+ if (typeof value === "number") {
997
+ return Number.isSafeInteger(value) && value >= 0 ? [] : [`${name} must be a non-negative integer id`];
998
+ }
999
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value) ? [] : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
1000
+ };
1001
+ var pathProblems = (path) => {
1002
+ const found = [];
1003
+ if (!path.startsWith("/"))
1004
+ found.push("path must be absolute");
1005
+ if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
1006
+ found.push('path must be normalised (no trailing slash, no empty, "." or ".." segments)');
1007
+ }
1008
+ if (path.includes("\x00"))
1009
+ found.push("path contains NUL");
1010
+ return found;
1011
+ };
1012
+ var fileProblems = (props) => {
1013
+ const found = pathProblems(props.path);
1014
+ const mode = props.mode ?? DEFAULT_MODE;
1015
+ if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
1016
+ found.push("mode must be 0–0o7777");
1017
+ found.push(...identityProblems("owner", props.owner));
1018
+ found.push(...identityProblems("group", props.group));
1019
+ found.push(...fileSecretProblems(props.content));
1020
+ return found;
1021
+ };
1022
+
1023
+ // src/launchd/job-validate.ts
1024
+ var LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
1025
+ var RESERVED_LABEL_PREFIXES = [
1026
+ "org.nixos.",
1027
+ "com.apple.",
1028
+ "homebrew.mxcl."
1029
+ ];
1030
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1031
+ var RANGES = [
1032
+ ["minute", 0, 59],
1033
+ ["hour", 0, 23],
1034
+ ["day", 1, 31],
1035
+ ["weekday", 0, 7],
1036
+ ["month", 1, 12]
1037
+ ];
1038
+ var isInt = (value, min, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(value) && value >= min && value <= max;
1039
+ var calendarProblems = (entries) => {
1040
+ if (entries.length === 0)
1041
+ return ["startCalendarInterval is an empty list; omit it instead"];
1042
+ const found = [];
1043
+ for (const [index, entry] of entries.entries()) {
1044
+ for (const [field, min, max] of RANGES) {
1045
+ const value = entry[field];
1046
+ if (value !== undefined && !isInt(value, min, max)) {
1047
+ found.push(`startCalendarInterval[${String(index)}].${field} must be ${String(min)}–${String(max)}`);
1048
+ }
1049
+ }
1050
+ }
1051
+ return found;
1052
+ };
1053
+ var absolute = (name, value) => value === undefined || value.startsWith("/") ? [] : [`${name} must be an absolute path`];
1054
+ var identityProblems2 = (label, domainText) => {
1055
+ const found = [];
1056
+ if (!LABEL.test(label)) {
1057
+ found.push("label must be 1–200 of [A-Za-z0-9._-], starting with a letter or digit");
1058
+ }
1059
+ const reserved = RESERVED_LABEL_PREFIXES.find((prefix) => label.startsWith(prefix));
1060
+ if (reserved !== undefined) {
1061
+ found.push(`label ${label} is under ${reserved}, which another tool owns; declare a new label`);
1062
+ }
1063
+ if (parseDomain(domainText) === undefined)
1064
+ found.push(`domain must be 'system' or 'gui/<uid>', not ${domainText}`);
1065
+ return found;
1066
+ };
1067
+ var jobProblems = (props) => {
1068
+ const found = identityProblems2(props.label, props.domain);
1069
+ const domain = parseDomain(props.domain);
1070
+ const [program] = props.programArguments;
1071
+ if (program === undefined)
1072
+ found.push("programArguments must name a program");
1073
+ else if (!program.startsWith("/"))
1074
+ found.push("programArguments[0] must be an absolute path");
1075
+ for (const name of Object.keys(props.environment ?? {})) {
1076
+ if (!ENV_NAME.test(name))
1077
+ found.push(`environment name ${JSON.stringify(name)} is not a valid variable name`);
1078
+ }
1079
+ found.push(...jobSecretProblems(props.environment, props.programArguments, props.extraKeys));
1080
+ if (props.startInterval !== undefined && !isInt(props.startInterval, 1)) {
1081
+ found.push("startInterval must be a positive whole number of seconds");
1082
+ }
1083
+ if (props.throttleInterval !== undefined && !isInt(props.throttleInterval, 0)) {
1084
+ found.push("throttleInterval must be a non-negative whole number of seconds");
1085
+ }
1086
+ const calendar = props.startCalendarInterval;
1087
+ if (calendar !== undefined) {
1088
+ found.push(...calendarProblems(Array.isArray(calendar) ? calendar : [calendar]));
1089
+ }
1090
+ const keepAlive = props.keepAlive;
1091
+ if (typeof keepAlive === "object" && keepAlive.successfulExit === undefined && keepAlive.crashed === undefined) {
1092
+ found.push("keepAlive {} names no condition; use true, false, or a condition");
1093
+ }
1094
+ found.push(...absolute("standardOutPath", props.standardOutPath));
1095
+ found.push(...absolute("standardErrorPath", props.standardErrorPath));
1096
+ found.push(...absolute("workingDirectory", props.workingDirectory));
1097
+ if (domain?.kind === "gui" && (props.userName !== undefined || props.groupName !== undefined)) {
1098
+ found.push("userName/groupName apply only to the system domain; launchd ignores them for agents");
1099
+ }
1100
+ for (const key of Object.keys(props.extraKeys ?? {})) {
1101
+ if (OWNED_KEYS.includes(key))
1102
+ found.push(`extraKeys.${key} is owned by a typed prop; use that instead`);
1103
+ }
1104
+ return found;
1105
+ };
1106
+
1107
+ // src/launchd/launchctl.ts
1108
+ var LAUNCHCTL = "/bin/launchctl";
1109
+ var NOT_FOUND = 113;
1110
+ var NO_DOMAIN = 112;
1111
+ var NO_DOMAIN_TEXT = "Could not find domain";
1112
+
1113
+ class LaunchctlError extends Error {
1114
+ constructor(command, exitCode, stderr) {
1115
+ super(`launchctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
1116
+ this.name = "LaunchctlError";
1117
+ }
1118
+ }
1119
+ var printFields = (stdout) => {
1120
+ const fields = new Map;
1121
+ const closers = [];
1122
+ for (const line of stdout.split(`
1123
+ `)) {
1124
+ if (closers.length === 0) {
1125
+ if (fields.size === 0 && line.endsWith(" = {"))
1126
+ closers.push("}");
1127
+ continue;
1128
+ }
1129
+ if (line === closers[closers.length - 1]) {
1130
+ closers.pop();
1131
+ continue;
1132
+ }
1133
+ const opened = /^(\t+)\S.* = \{$/.exec(line);
1134
+ if (opened?.[1] !== undefined && opened[1].length === closers.length) {
1135
+ closers.push(`${opened[1]}}`);
1136
+ continue;
1137
+ }
1138
+ const field = closers.length === 1 ? /^\t([a-z][a-z ]*[a-z]) = (.*)$/.exec(line) : null;
1139
+ if (field?.[1] !== undefined && field[2] !== undefined && !fields.has(field[1])) {
1140
+ fields.set(field[1], field[2]);
1141
+ }
1142
+ }
1143
+ return fields;
1144
+ };
1145
+ var leadingInt = (text) => {
1146
+ const match = text === undefined ? null : /^(-?\d+)/.exec(text);
1147
+ return match?.[1] === undefined ? undefined : Number(match[1]);
1148
+ };
1149
+ var parsePrint = (stdout) => {
1150
+ const fields = printFields(stdout);
1151
+ const state = fields.get("state");
1152
+ const pid = leadingInt(fields.get("pid"));
1153
+ const lastExitCode = leadingInt(fields.get("last exit code"));
1154
+ return {
1155
+ loaded: true,
1156
+ ...state === undefined ? {} : { state },
1157
+ ...pid === undefined ? {} : { pid },
1158
+ ...lastExitCode === undefined ? {} : { lastExitCode }
1159
+ };
1160
+ };
1161
+ var parseDisabled = (stdout) => {
1162
+ const disabled = new Set;
1163
+ for (const match of stdout.matchAll(/^\s*"([^"]+)" => (\w+)\s*$/gm)) {
1164
+ if (match[1] !== undefined && (match[2] === "disabled" || match[2] === "true")) {
1165
+ disabled.add(match[1]);
1166
+ }
1167
+ }
1168
+ return disabled;
1169
+ };
1170
+ var printService = async (runner, target) => {
1171
+ const result = await runner.exec([LAUNCHCTL, "print", target]);
1172
+ if (result.exitCode === NOT_FOUND)
1173
+ return { loaded: false };
1174
+ if (result.exitCode === NO_DOMAIN && result.stderr.includes(NO_DOMAIN_TEXT)) {
1175
+ return { domainMissing: true, loaded: false };
1176
+ }
1177
+ if (result.exitCode !== 0)
1178
+ throw new LaunchctlError(`print ${target}`, result.exitCode, result.stderr);
1179
+ return parsePrint(result.stdout);
1180
+ };
1181
+ var isDisabled = async (runner, domain, label) => {
1182
+ const result = await runner.exec([LAUNCHCTL, "print-disabled", domain]);
1183
+ if (result.exitCode !== 0) {
1184
+ throw new LaunchctlError(`print-disabled ${domain}`, result.exitCode, result.stderr);
1185
+ }
1186
+ return parseDisabled(result.stdout).has(label);
1187
+ };
1188
+ var bootstrap = async (runner, domain, plistPath) => {
1189
+ const result = await runner.exec([LAUNCHCTL, "bootstrap", domain, plistPath]);
1190
+ if (result.exitCode !== 0) {
1191
+ throw new LaunchctlError(`bootstrap ${domain} ${plistPath}`, result.exitCode, result.stderr);
1192
+ }
1193
+ };
1194
+ var waitUnloaded = async (runner, target, attempts = 120, intervalMs = 250) => {
1195
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
1196
+ if (!(await printService(runner, target)).loaded)
1197
+ return;
1198
+ await runner.sleep(intervalMs);
1199
+ }
1200
+ throw new Error(`${target} is still loaded ${String(attempts * intervalMs / 1000)}s after bootout`);
1201
+ };
1202
+ var bootoutIfLoaded = async (runner, target) => {
1203
+ if (!(await printService(runner, target)).loaded)
1204
+ return;
1205
+ const result = await runner.exec([LAUNCHCTL, "bootout", target]);
1206
+ try {
1207
+ await waitUnloaded(runner, target);
1208
+ } catch (cause) {
1209
+ if (result.exitCode === 0)
1210
+ throw cause;
1211
+ throw new LaunchctlError(`bootout ${target}`, result.exitCode, `${result.stderr.trim()} (and still loaded after waiting)`);
1212
+ }
1213
+ };
1214
+
1215
+ // src/launchd/sudo-allowlist.ts
1216
+ class SudoRefusedError extends Error {
1217
+ constructor(message) {
1218
+ super(message);
1219
+ this.name = "SudoRefusedError";
1220
+ }
1221
+ }
1222
+ var SUDO = "/usr/bin/sudo";
1223
+ var INSTALL = "/usr/bin/install";
1224
+ var RM = "/bin/rm";
1225
+ var WRITES = new Set(["bootstrap", "bootout", "kickstart"]);
1226
+ var octalMode = (mode) => Number.isSafeInteger(mode) && mode >= 0 && mode <= 4095 ? mode.toString(8).padStart(4, "0") : undefined;
1227
+ var checkPrefixes = (prefixes) => {
1228
+ if (prefixes.length === 0) {
1229
+ throw new Error("sudoRunner: no prefixes. Name the directories root may write, e.g. /Library/LaunchDaemons.");
1230
+ }
1231
+ for (const prefix of prefixes) {
1232
+ const found = prefix === "/" ? ['"/" would allow every path'] : pathProblems(prefix);
1233
+ if (found.length > 0) {
1234
+ throw new Error(`sudoRunner: prefix ${JSON.stringify(prefix)}: ${found.join("; ")}`);
1235
+ }
1236
+ }
1237
+ return [...prefixes];
1238
+ };
1239
+ var prefixOf = (path, prefixes) => pathProblems(path).length > 0 ? undefined : prefixes.find((prefix) => path.startsWith(`${prefix}/`));
1240
+ var labelOk = (label) => LABEL.test(label) && !RESERVED_LABEL_PREFIXES.some((prefix) => label.startsWith(prefix));
1241
+ var systemLabel = (target) => {
1242
+ const label = target?.startsWith("system/") === true ? target.slice("system/".length) : undefined;
1243
+ return label !== undefined && labelOk(label) ? label : undefined;
1244
+ };
1245
+ var daemonPlist = (label, prefixes) => {
1246
+ const path = plistPathFor(label, { kind: "system" }, undefined);
1247
+ return prefixOf(path, prefixes) === undefined ? undefined : path;
1248
+ };
1249
+ var NO_DAEMON_PREFIX = "needs /Library/LaunchDaemons among the prefixes, where the job’s plist is written and removed";
1250
+ var launchctlProblem = (args, prefixes) => {
1251
+ const [sub, ...rest] = args;
1252
+ if (sub === "bootstrap") {
1253
+ const given = rest[1] ?? "";
1254
+ const name = given.slice(given.lastIndexOf("/") + 1);
1255
+ const label = name.endsWith(".plist") ? name.slice(0, -".plist".length) : "";
1256
+ const shaped = rest.length === 2 && rest[0] === "system" && labelOk(label);
1257
+ if (!shaped)
1258
+ return "bootstrap takes exactly `system /Library/LaunchDaemons/<label>.plist`";
1259
+ const plist = daemonPlist(label, prefixes);
1260
+ if (plist === undefined)
1261
+ return `bootstrap ${NO_DAEMON_PREFIX}`;
1262
+ return given === plist ? undefined : `bootstrap loads only ${plist}, never another path`;
1263
+ }
1264
+ if (sub === "bootout") {
1265
+ const label = rest.length === 1 ? systemLabel(rest[0]) : undefined;
1266
+ if (label === undefined)
1267
+ return "bootout takes exactly `system/<label>`, never a bare domain";
1268
+ return daemonPlist(label, prefixes) === undefined ? `bootout ${NO_DAEMON_PREFIX}` : undefined;
1269
+ }
1270
+ if (sub === "kickstart") {
1271
+ const flags = rest.slice(0, -1);
1272
+ const flagsOk = flags.every((flag) => flag === "-k" || flag === "-p") && new Set(flags).size === flags.length;
1273
+ return flagsOk && systemLabel(rest[rest.length - 1]) !== undefined ? undefined : "kickstart takes only -k / -p, then `system/<label>`";
1274
+ }
1275
+ return `launchctl ${String(sub)} is never run as root`;
1276
+ };
1277
+ var ID = /^\d+$/;
1278
+ var idsOk = (ids) => {
1279
+ const [flag, id, second, secondId] = ids;
1280
+ if (ids.length === 0)
1281
+ return true;
1282
+ if (ids.length === 2)
1283
+ return (flag === "-o" || flag === "-g") && ID.test(id ?? "");
1284
+ return ids.length === 4 && flag === "-o" && ID.test(id ?? "") && second === "-g" && ID.test(secondId ?? "");
1285
+ };
1286
+ var privilegedProblem = (argv, context) => {
1287
+ const [program, ...args] = argv;
1288
+ const under = (path) => path !== undefined && prefixOf(path, context.prefixes) !== undefined;
1289
+ if (program === LAUNCHCTL)
1290
+ return launchctlProblem(args, context.prefixes);
1291
+ if (program === INSTALL) {
1292
+ const [safe, m, mode, ...rest] = args;
1293
+ if (safe !== "-S" || m !== "-m" || mode === undefined || !/^[0-7]{4}$/.test(mode)) {
1294
+ return "install takes exactly `-S -m <4 octal digits> [-o <uid>] [-g <gid>] <staged> <dest>`";
1295
+ }
1296
+ if (rest.length < 2 || !idsOk(rest.slice(0, -2)))
1297
+ return "install takes only -o <uid> -g <gid>";
1298
+ const [source, dest] = rest.slice(-2);
1299
+ if (source === undefined || source !== context.staged) {
1300
+ return "install copies only the file this runner staged";
1301
+ }
1302
+ return under(dest) ? undefined : "install destination is not under a declared prefix";
1303
+ }
1304
+ if (program === RM) {
1305
+ const [force, end, path, ...extra] = args;
1306
+ return force === "-f" && end === "--" && extra.length === 0 && under(path) ? undefined : "rm takes exactly `-f -- <path under a declared prefix>`";
1307
+ }
1308
+ return `${String(program)} is not on the sudo allowlist`;
1309
+ };
1310
+ var routeExec = (argv, operatorUid) => {
1311
+ const [program, sub] = argv;
1312
+ if (program === "sudo" || program?.endsWith("/sudo") === true) {
1313
+ return {
1314
+ refuse: "sudoRunner never passes a sudo argv through; it elevates only its allowlist"
1315
+ };
1316
+ }
1317
+ if (program !== LAUNCHCTL || sub === undefined || !WRITES.has(sub))
1318
+ return { as: "operator" };
1319
+ const target = sub === "kickstart" ? argv[argv.length - 1] : argv[2];
1320
+ if (target === "system" || target?.startsWith("system/") === true)
1321
+ return { as: "root" };
1322
+ const owner = /^(?:gui|user)\/(\d+)(?:\/|$)/.exec(target ?? "")?.[1];
1323
+ if (owner === undefined || Number(owner) === operatorUid)
1324
+ return { as: "operator" };
1325
+ return {
1326
+ refuse: `launchctl ${sub} ${String(target)}: that domain belongs to uid ${owner}, and sudoRunner ` + "elevates only the system domain. Deploy as that user, or as root."
1327
+ };
1328
+ };
1329
+
1330
+ // src/linux/sudo-allowlist-dir.ts
1331
+ var MKDIR = "/usr/bin/mkdir";
1332
+ var CHMOD = "/usr/bin/chmod";
1333
+ var CHOWN = "/usr/bin/chown";
1334
+ var RMDIR = "/usr/bin/rmdir";
1335
+ var parseDirMode = (text) => /^([0-7]{3}|[1-7][0-7]{3})$/.test(text) ? Number.parseInt(text, 8) : undefined;
1336
+ var DANGEROUS_MODE_BITS = 3090;
1337
+ var dirModeProblem = (mode) => {
1338
+ if (mode === undefined)
1339
+ return "mode";
1340
+ return (mode & DANGEROUS_MODE_BITS) !== 0 ? "a mode with setuid/setgid or group/other write (this runner installs only 0–0o777 minus 0o022, no special bits)" : undefined;
1341
+ };
1342
+ var ROOT_OWNER = /^(\+0|:\+0|\+0:\+0)$/;
1343
+ var dirProgramProblem = (program, args, prefixes) => {
1344
+ const under = (path2) => path2 !== undefined && prefixOf(path2, prefixes) !== undefined;
1345
+ if (program === MKDIR) {
1346
+ const [m, modeText, end2, path2, ...extra2] = args;
1347
+ const mode = modeText === undefined ? undefined : parseDirMode(modeText);
1348
+ if (m !== "-m" || mode === undefined || end2 !== "--" || extra2.length !== 0 || !under(path2)) {
1349
+ return "mkdir takes exactly `-m <octal> -- <path under a declared prefix>`";
1350
+ }
1351
+ return dirModeProblem(mode);
1352
+ }
1353
+ if (program === CHMOD) {
1354
+ const [modeText, end2, path2, ...extra2] = args;
1355
+ const mode = modeText === undefined ? undefined : parseDirMode(modeText);
1356
+ if (mode === undefined || end2 !== "--" || extra2.length !== 0 || !under(path2)) {
1357
+ return "chmod takes exactly `<octal> -- <path under a declared prefix>`";
1358
+ }
1359
+ return dirModeProblem(mode);
1360
+ }
1361
+ if (program === CHOWN) {
1362
+ const [owner, end2, path2, ...extra2] = args;
1363
+ return owner !== undefined && ROOT_OWNER.test(owner) && end2 === "--" && extra2.length === 0 && under(path2) ? undefined : "chown takes exactly `+0`, `:+0` or `+0:+0` -- <path under a declared prefix>` — root " + "only, forced numeric; a directory this runner elevates never hands ownership to anyone else";
1364
+ }
1365
+ const [end, path, ...extra] = args;
1366
+ return end === "--" && extra.length === 0 && under(path) ? undefined : "rmdir takes exactly `-- <path under a declared prefix>`";
1367
+ };
1368
+
1369
+ // src/linux/sudo-allowlist-chown.ts
1370
+ var forceNumericOwner = (spec) => {
1371
+ const [uidPart, gidPart] = spec.includes(":") ? spec.split(":") : [spec, undefined];
1372
+ const uid = uidPart === undefined || uidPart === "" ? "" : `+${uidPart}`;
1373
+ const gid = gidPart === undefined ? "" : `:+${gidPart}`;
1374
+ return `${uid}${gid}`;
1375
+ };
1376
+ var SAFE_OWNER = /^(\+\d+(:\+\d+)?|:\+\d+)$/;
1377
+ var fileChownProblem = (args, temp) => {
1378
+ const [owner, end, path, ...extra] = args;
1379
+ if (owner !== undefined && SAFE_OWNER.test(owner) && end === "--" && extra.length === 0 && path === temp) {
1380
+ return;
1381
+ }
1382
+ return "chown on the staged temp takes exactly `+<uid>`, `:+<gid>` or `+<uid>:+<gid>` -- " + "<the exact temp install just wrote>, numeric and +-forced";
1383
+ };
1384
+ var chownProblem = (args, context) => {
1385
+ const path = args[args.length - 1];
1386
+ if (context.temp !== undefined && path === context.temp)
1387
+ return fileChownProblem(args, context.temp);
1388
+ return dirProgramProblem(CHOWN, args, context.prefixes);
1389
+ };
1390
+ // src/linux/sudo-allowlist.ts
1391
+ var MV = "/usr/bin/mv";
1392
+ var RM2 = "/usr/bin/rm";
1393
+ var SYSTEMCTL_ABS = "/usr/bin/systemctl";
1394
+ var DIRECTORY_PROGRAMS = new Set([
1395
+ "mkdir",
1396
+ "chmod",
1397
+ "chown",
1398
+ "rmdir"
1399
+ ]);
1400
+ var DIRECTORY_PROGRAMS_ABS = new Set([MKDIR, CHMOD, CHOWN, RMDIR]);
1401
+ var ABS_OF = {
1402
+ chmod: CHMOD,
1403
+ chown: CHOWN,
1404
+ mkdir: MKDIR,
1405
+ rmdir: RMDIR,
1406
+ systemctl: SYSTEMCTL_ABS
1407
+ };
1408
+ var canonicalize = (argv) => {
1409
+ const abs = ABS_OF[argv[0] ?? ""];
1410
+ const program = abs ?? argv[0];
1411
+ if (program === CHOWN) {
1412
+ const [owner, ...rest] = argv.slice(1);
1413
+ return [CHOWN, owner === undefined ? "" : forceNumericOwner(owner), ...rest];
1414
+ }
1415
+ return abs === undefined ? argv : [abs, ...argv.slice(1)];
1416
+ };
1417
+ var dirOf = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
1418
+ var installProblem = (args, context) => {
1419
+ const [m, mode, t, dd, source, dest, ...extra] = args;
1420
+ if (m !== "-m" || mode === undefined || !/^[0-7]{4}$/.test(mode) || t !== "-T" || dd !== "--" || extra.length !== 0) {
1421
+ return "install takes exactly `-m <4 octal digits> -T -- <staged> <temp>`";
1422
+ }
1423
+ if (source === undefined || source !== context.staged) {
1424
+ return "install copies only the file this runner staged";
1425
+ }
1426
+ if (dest === undefined || dest !== context.temp) {
1427
+ return "install writes only the derived temp path for this write";
1428
+ }
1429
+ return prefixOf(dest, context.prefixes) === undefined ? "install destination is not under a declared prefix" : undefined;
1430
+ };
1431
+ var mvProblem = (args, context) => {
1432
+ const [f, t, dd, source, dest] = args;
1433
+ if (f !== "-f" || t !== "-T" || dd !== "--" || args.length !== 5) {
1434
+ return "mv takes exactly `-f -T -- <temp> <dest>`";
1435
+ }
1436
+ if (source === undefined || source !== context.temp) {
1437
+ return "mv moves only the temp path this runner just installed to";
1438
+ }
1439
+ if (dest === undefined || prefixOf(dest, context.prefixes) === undefined) {
1440
+ return "mv destination is not under a declared prefix";
1441
+ }
1442
+ return dirOf(source) === dirOf(dest) ? undefined : "mv must stay inside the destination’s directory";
1443
+ };
1444
+ var rmProblem = (args, context) => {
1445
+ const [force, end, path, ...extra] = args;
1446
+ return force === "-f" && end === "--" && extra.length === 0 && path !== undefined && prefixOf(path, context.prefixes) !== undefined ? undefined : "rm takes exactly `-f -- <path under a declared prefix>`";
1447
+ };
1448
+ var BANNED_SYSTEMCTL_FLAGS = new Set([
1449
+ "--user",
1450
+ "--global",
1451
+ "-H",
1452
+ "-M",
1453
+ "--root"
1454
+ ]);
1455
+ var UNIT_WRITE_VERBS = new Set([
1456
+ "enable",
1457
+ "disable",
1458
+ "start",
1459
+ "stop",
1460
+ "restart"
1461
+ ]);
1462
+ var systemctlProblem = (args, prefixes) => {
1463
+ if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {
1464
+ return "systemctl --user/--global/-H/-M/--root is never run as root";
1465
+ }
1466
+ const [verb, ...rest] = args;
1467
+ if (verb === "daemon-reload")
1468
+ return rest.length === 0 ? undefined : "daemon-reload takes no operands";
1469
+ if (verb !== undefined && UNIT_WRITE_VERBS.has(verb)) {
1470
+ const [dd, unit, ...extra] = rest;
1471
+ return dd === "--" && unit !== undefined && NAME.test(unit) && extra.length === 0 ? undefined : `${verb} takes exactly \`-- <unit>\``;
1472
+ }
1473
+ return `systemctl ${String(verb)} is never run as root`;
1474
+ };
1475
+ var privilegedProblem2 = (argv, context) => {
1476
+ const [program, ...args] = argv;
1477
+ if (program === INSTALL)
1478
+ return installProblem(args, context);
1479
+ if (program === MV)
1480
+ return mvProblem(args, context);
1481
+ if (program === RM2)
1482
+ return rmProblem(args, context);
1483
+ if (program === SYSTEMCTL_ABS)
1484
+ return systemctlProblem(args, context.prefixes);
1485
+ if (program === CHOWN)
1486
+ return chownProblem(args, context);
1487
+ if (program === MKDIR || program === CHMOD || program === RMDIR) {
1488
+ return dirProgramProblem(program, args, context.prefixes);
1489
+ }
1490
+ return `${String(program)} is not on the sudo allowlist`;
1491
+ };
1492
+ var routeExec2 = (argv, prefixes) => {
1493
+ const [program, ...args] = argv;
1494
+ if (program === "sudo" || program?.endsWith("/sudo") === true) {
1495
+ return {
1496
+ refuse: "sshSudoRunner never passes a sudo argv through; it elevates only its allowlist"
1497
+ };
1498
+ }
1499
+ if (program === "systemctl" || program === SYSTEMCTL_ABS) {
1500
+ if (args.some((arg) => BANNED_SYSTEMCTL_FLAGS.has(arg))) {
1501
+ return { refuse: "sshSudoRunner refuses systemctl --user/--global/-H/-M/--root" };
1502
+ }
1503
+ const [verb] = args;
1504
+ return verb === "daemon-reload" || verb !== undefined && UNIT_WRITE_VERBS.has(verb) ? { as: "root" } : { as: "operator" };
1505
+ }
1506
+ if (DIRECTORY_PROGRAMS.has(program ?? "") || DIRECTORY_PROGRAMS_ABS.has(program ?? "")) {
1507
+ const path = args[args.length - 1];
1508
+ return path !== undefined && prefixOf(path, prefixes) !== undefined ? { as: "root" } : { as: "operator" };
1509
+ }
1510
+ return { as: "operator" };
1511
+ };
1512
+
1513
+ // src/ceph/ceph-transport.ts
1514
+ var CONNECT_TIMEOUT_SEC = 10;
1515
+ var EXEC_TIMEOUT_MS = 30000;
1516
+ var sshCephDial = async (node, argv) => {
1517
+ const nonce = randomBytes2(8).toString("hex");
1518
+ const script = `${SUDO} -n -- ${quoteArgv(argv)}`;
1519
+ return runRemote({ connectTimeoutSec: CONNECT_TIMEOUT_SEC, execTimeoutMs: EXEC_TIMEOUT_MS, host: node }, nonce, script);
1520
+ };
1521
+ var defaultLog = (line) => process.stderr.write(`${line}
1522
+ `);
1523
+ var runCephCommand = async (options, argv) => {
1524
+ const problem = cephCommandProblem(argv);
1525
+ if (problem !== undefined) {
1526
+ throw new SudoRefusedError(`ceph transport refused ${JSON.stringify(argv)}: ${problem}`);
1527
+ }
1528
+ if (options.nodes.length === 0)
1529
+ throw new Error("ceph transport: no mon nodes were declared");
1530
+ const dial = options.dial ?? sshCephDial;
1531
+ const log = options.log ?? defaultLog;
1532
+ const attempts = [];
1533
+ for (const node of options.nodes) {
1534
+ log(`homeflare/ceph sudo -n ${JSON.stringify(argv)} @ ${node}`);
1535
+ try {
1536
+ const result = await dial(node, argv);
1537
+ return { ...result, node };
1538
+ } catch (cause) {
1539
+ attempts.push({ message: cause instanceof Error ? cause.message : String(cause), node });
1540
+ }
1541
+ }
1542
+ const detail = attempts.map((a) => `${a.node}: ${a.message}`).join("; ");
1543
+ throw new Error(`ceph transport: every mon node failed (never treated as absent) — ${detail}`);
1544
+ };
1545
+ var assertFreshQuorum = async (options) => {
1546
+ const result = await runCephCommand(options, quorumStatusArgv());
1547
+ if (result.exitCode !== 0) {
1548
+ throw new Error(`ceph quorum_status failed after a write (mon ${result.node}): ` + `${result.stderr.trim().slice(0, 300)}`);
1549
+ }
1550
+ const quorum = parseQuorumStatus(result.stdout);
1551
+ if (!quorum.healthy) {
1552
+ throw new Error(`ceph quorum degraded after a write (mon ${result.node}): ${quorum.reason} — the write is ` + "reported as failed even if it landed; nothing here auto-repairs a quorum problem.");
1553
+ }
1554
+ };
1555
+
1556
+ // src/ceph/ceph-auth-reconcile.ts
1557
+ var asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
1558
+ var runCeph = (transport, argv) => Effect2.tryPromise({ catch: asError, try: () => runCephCommand(transport, argv) });
1559
+ var syncQuorum = (transport) => Effect2.tryPromise({ catch: asError, try: () => assertFreshQuorum(transport) });
1560
+ var neverAdopted = (entity) => new Error(`Ceph.AuthEntity ${entity}: already exists on the mon cluster and this stack holds no state ` + "for it. This family is never adopted — not even under --adopt — because taking it over " + "would mean fingerprinting a key this stack did not mint. Remove it by hand first, or bring " + "it under management through the coordinator’s own read-only inventory process.");
1561
+ var movedIdentity = (from, to) => new Error(`Ceph.AuthEntity: planned as an update, but its identity moved from ${from} to ${to}. ` + `Acting on ${to} now, with ${from}'s attributes, could re-cap an entity this generation ` + "never minted. Nothing was sent to any mon. Run the deploy again: once entity/mount fully " + "resolve at plan time this plans as a replace instead.");
1562
+ var reconcileCephAuthEntity = (props, output, deps) => Effect2.gen(function* () {
1563
+ if (identityMoved(props, output))
1564
+ return yield* Effect2.fail(movedIdentity(output?.entity ?? "", props.entity));
1565
+ const transport = { nodes: props.nodes, ...deps };
1566
+ const observed = yield* runCeph(transport, authGetArgv(props.entity)).pipe(Effect2.map((result2) => parseAuthGet(result2, props.entity)));
1567
+ if (observed.kind === "absent") {
1568
+ yield* baoWrite("POST", cephDataPath(props.mount, props.entity), {
1569
+ data: { pending: true }
1570
+ });
1571
+ const result2 = yield* runCeph(transport, authGetOrCreateArgv(props.entity, props.caps));
1572
+ const created = parseAuthGetOrCreate(result2, props.entity);
1573
+ yield* baoWrite("POST", cephDataPath(props.mount, props.entity), {
1574
+ data: { caps: created.caps, key: created.key }
1575
+ });
1576
+ yield* syncQuorum(transport);
1577
+ return attributesOf(props, sha256(created.key), result2.node);
1578
+ }
1579
+ if (output === undefined)
1580
+ return yield* Effect2.fail(neverAdopted(props.entity));
1581
+ if (capsEqual(observed.caps, props.caps))
1582
+ return output;
1583
+ const result = yield* runCeph(transport, authCapsArgv(props.entity, props.caps));
1584
+ yield* syncQuorum(transport);
1585
+ return attributesOf(props, output.fingerprint, result.node);
1586
+ });
1587
+
1588
+ // src/ceph/auth-entity.ts
1589
+ var CephAuthEntity = Resource("Ceph.AuthEntity", {
1590
+ defaultRemovalPolicy: "retain"
1591
+ });
1592
+ var refusedDelete = (entity) => new Error(`Ceph.AuthEntity ${entity}: 'ceph auth del' is refused entirely — lockout safety, never ` + "auto-delete (D3). Nothing was sent to any mon. If the entity truly must go, remove it by " + "hand, deliberately, along with its OpenBao key — this family will never do it for you.");
1593
+ var CephAuthEntityProvider = () => Provider.effect(CephAuthEntity, Effect3.succeed(CephAuthEntity.Provider.of({
1594
+ list: () => Effect3.succeed([]),
1595
+ read: ({ output }) => Effect3.succeed(output),
1596
+ diff: ({ news, output }) => Effect3.succeed(isResolved(news) ? planCephAuthEntity(news, output) : undefined),
1597
+ reconcile: ({ news, output }) => reconcileCephAuthEntity(news, output, {}),
1598
+ delete: ({ output }) => Effect3.die(refusedDelete(output.entity))
1599
+ })));
1600
+ export {
1601
+ CephAuthEntity,
1602
+ CephAuthEntityProvider,
1603
+ assertFreshQuorum,
1604
+ runCephCommand,
1605
+ sshCephDial
1606
+ };
1607
+
1608
+ //# debugId=B185E625634786EC64756E2164756E21
1609
+ //# sourceMappingURL=index.js.map