@homeflare/alchemy 0.8.0 → 0.10.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 (240) hide show
  1. package/README.md +22 -11
  2. package/dist/caddy/config-lifecycle.d.ts +1 -1
  3. package/dist/caddy/config.d.ts.map +1 -1
  4. package/dist/caddy/index.js +221 -53
  5. package/dist/caddy/index.js.map +13 -9
  6. package/dist/launchd/fake-runner.d.ts +1 -0
  7. package/dist/launchd/fake-runner.d.ts.map +1 -1
  8. package/dist/launchd/fake-sudo.d.ts +1 -0
  9. package/dist/launchd/fake-sudo.d.ts.map +1 -1
  10. package/dist/launchd/host-effect.d.ts +9 -18
  11. package/dist/launchd/host-effect.d.ts.map +1 -1
  12. package/dist/launchd/host-file-lifecycle.d.ts +14 -1
  13. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  14. package/dist/launchd/host-file.d.ts +2 -1
  15. package/dist/launchd/host-file.d.ts.map +1 -1
  16. package/dist/launchd/index.js +345 -86
  17. package/dist/launchd/index.js.map +18 -12
  18. package/dist/launchd/job-lifecycle.d.ts +8 -1
  19. package/dist/launchd/job-lifecycle.d.ts.map +1 -1
  20. package/dist/launchd/job-preflight.d.ts +13 -1
  21. package/dist/launchd/job-preflight.d.ts.map +1 -1
  22. package/dist/launchd/job.d.ts +2 -1
  23. package/dist/launchd/job.d.ts.map +1 -1
  24. package/dist/launchd/runner.d.ts +7 -0
  25. package/dist/launchd/runner.d.ts.map +1 -1
  26. package/dist/launchd/sudo-acl.d.ts +25 -0
  27. package/dist/launchd/sudo-acl.d.ts.map +1 -0
  28. package/dist/launchd/sudo-guard.d.ts +31 -4
  29. package/dist/launchd/sudo-guard.d.ts.map +1 -1
  30. package/dist/launchd/sudo-runner.d.ts +3 -3
  31. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  32. package/dist/launchd/sudo-said.d.ts +10 -0
  33. package/dist/launchd/sudo-said.d.ts.map +1 -0
  34. package/dist/openbao/auth-method-reconcile.d.ts +6 -2
  35. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -1
  36. package/dist/openbao/auth-method.d.ts.map +1 -1
  37. package/dist/openbao/auth-role.d.ts.map +1 -1
  38. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  39. package/dist/openbao/fake-bao.d.ts +18 -0
  40. package/dist/openbao/fake-bao.d.ts.map +1 -1
  41. package/dist/openbao/fake-core.d.ts +39 -0
  42. package/dist/openbao/fake-core.d.ts.map +1 -0
  43. package/dist/openbao/fake-families.d.ts +6 -3
  44. package/dist/openbao/fake-families.d.ts.map +1 -1
  45. package/dist/openbao/fake-mounts.d.ts +16 -0
  46. package/dist/openbao/fake-mounts.d.ts.map +1 -0
  47. package/dist/openbao/fake-stack.d.ts +5 -1
  48. package/dist/openbao/fake-stack.d.ts.map +1 -1
  49. package/dist/openbao/index.js +541 -270
  50. package/dist/openbao/index.js.map +26 -21
  51. package/dist/openbao/jwt-config.d.ts.map +1 -1
  52. package/dist/openbao/jwt-role.d.ts.map +1 -1
  53. package/dist/openbao/kubernetes-role.d.ts.map +1 -1
  54. package/dist/openbao/mfa-enforcement.d.ts +6 -2
  55. package/dist/openbao/mfa-enforcement.d.ts.map +1 -1
  56. package/dist/openbao/mfa-totp.d.ts +6 -2
  57. package/dist/openbao/mfa-totp.d.ts.map +1 -1
  58. package/dist/openbao/mount-reconcile.d.ts +5 -1
  59. package/dist/openbao/mount-reconcile.d.ts.map +1 -1
  60. package/dist/openbao/mount.d.ts.map +1 -1
  61. package/dist/openbao/pki-role.d.ts.map +1 -1
  62. package/dist/openbao/plugin-reconcile.d.ts +6 -2
  63. package/dist/openbao/plugin-reconcile.d.ts.map +1 -1
  64. package/dist/openbao/plugin.d.ts.map +1 -1
  65. package/dist/openbao/policy.d.ts.map +1 -1
  66. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  67. package/dist/openbao/role-reconcile.d.ts +11 -1
  68. package/dist/openbao/role-reconcile.d.ts.map +1 -1
  69. package/dist/openbao/ssh-role.d.ts.map +1 -1
  70. package/dist/ownership/adopt.d.ts +52 -0
  71. package/dist/ownership/adopt.d.ts.map +1 -0
  72. package/dist/ownership/probe.d.ts +13 -0
  73. package/dist/ownership/probe.d.ts.map +1 -0
  74. package/dist/ownership/resume.d.ts +40 -0
  75. package/dist/ownership/resume.d.ts.map +1 -0
  76. package/dist/ownership/rows.d.ts +33 -0
  77. package/dist/ownership/rows.d.ts.map +1 -0
  78. package/dist/ownership/whole.d.ts +14 -0
  79. package/dist/ownership/whole.d.ts.map +1 -0
  80. package/dist/proxmox/ceph-pool-form.d.ts +3 -3
  81. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  82. package/dist/proxmox/fake-bao-env.d.ts +3 -0
  83. package/dist/proxmox/fake-bao-env.d.ts.map +1 -0
  84. package/dist/proxmox/fake-pve-lxc.d.ts +42 -0
  85. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -0
  86. package/dist/proxmox/fake-pve.d.ts +38 -0
  87. package/dist/proxmox/fake-pve.d.ts.map +1 -0
  88. package/dist/proxmox/index.d.ts +6 -2
  89. package/dist/proxmox/index.d.ts.map +1 -1
  90. package/dist/proxmox/index.js +970 -186
  91. package/dist/proxmox/index.js.map +22 -9
  92. package/dist/proxmox/lxc-create-form.d.ts +6 -0
  93. package/dist/proxmox/lxc-create-form.d.ts.map +1 -0
  94. package/dist/proxmox/lxc-harness.d.ts +34 -0
  95. package/dist/proxmox/lxc-harness.d.ts.map +1 -0
  96. package/dist/proxmox/lxc-identity.d.ts +18 -0
  97. package/dist/proxmox/lxc-identity.d.ts.map +1 -0
  98. package/dist/proxmox/lxc-judge.d.ts +46 -0
  99. package/dist/proxmox/lxc-judge.d.ts.map +1 -0
  100. package/dist/proxmox/lxc-lifecycle.d.ts +68 -0
  101. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -0
  102. package/dist/proxmox/lxc-props.d.ts +139 -0
  103. package/dist/proxmox/lxc-props.d.ts.map +1 -0
  104. package/dist/proxmox/lxc-volume.d.ts +29 -0
  105. package/dist/proxmox/lxc-volume.d.ts.map +1 -0
  106. package/dist/proxmox/lxc-wire.d.ts +51 -0
  107. package/dist/proxmox/lxc-wire.d.ts.map +1 -0
  108. package/dist/proxmox/lxc.d.ts +27 -47
  109. package/dist/proxmox/lxc.d.ts.map +1 -1
  110. package/dist/proxmox/network-apply-read.d.ts +1 -1
  111. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  112. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  113. package/dist/proxmox/resource.d.ts.map +1 -1
  114. package/dist/proxmox/update-guard.d.ts +27 -0
  115. package/dist/proxmox/update-guard.d.ts.map +1 -0
  116. package/dist/verify/args.d.ts +16 -0
  117. package/dist/verify/args.d.ts.map +1 -0
  118. package/dist/verify/cli.d.ts +3 -0
  119. package/dist/verify/cli.d.ts.map +1 -0
  120. package/dist/verify/cli.js +320 -0
  121. package/dist/verify/cli.js.map +15 -0
  122. package/dist/verify/fake-engine.d.ts +57 -0
  123. package/dist/verify/fake-engine.d.ts.map +1 -0
  124. package/dist/verify/index.d.ts +16 -0
  125. package/dist/verify/index.d.ts.map +1 -0
  126. package/dist/verify/index.js +297 -0
  127. package/dist/verify/index.js.map +14 -0
  128. package/dist/verify/report.d.ts +6 -0
  129. package/dist/verify/report.d.ts.map +1 -0
  130. package/dist/verify/rows.d.ts +57 -0
  131. package/dist/verify/rows.d.ts.map +1 -0
  132. package/dist/verify/spy.d.ts +31 -0
  133. package/dist/verify/spy.d.ts.map +1 -0
  134. package/dist/verify/verify.d.ts +43 -0
  135. package/dist/verify/verify.d.ts.map +1 -0
  136. package/docs/adopt-verify.md +108 -0
  137. package/docs/adopted-deploys.md +85 -0
  138. package/docs/credentials.md +15 -0
  139. package/docs/launchd-sudo.md +31 -6
  140. package/docs/launchd.md +7 -3
  141. package/docs/ownership.md +113 -0
  142. package/docs/proxmox-lxc.md +196 -0
  143. package/docs/proxmox.md +12 -0
  144. package/package.json +9 -2
  145. package/src/caddy/adopt-scope.test.ts +1 -1
  146. package/src/caddy/config-lifecycle.ts +1 -1
  147. package/src/caddy/config.ts +2 -1
  148. package/src/launchd/adopt-apply.test.ts +166 -0
  149. package/src/launchd/fake-runner.ts +26 -4
  150. package/src/launchd/host-effect.ts +1 -33
  151. package/src/launchd/host-file-lifecycle.ts +39 -14
  152. package/src/launchd/host-file.ts +13 -4
  153. package/src/launchd/job-lifecycle.ts +22 -14
  154. package/src/launchd/job-preflight.ts +18 -1
  155. package/src/launchd/job.ts +13 -4
  156. package/src/launchd/runner.ts +7 -0
  157. package/src/launchd/sudo-acl.ts +68 -0
  158. package/src/launchd/sudo-chain.test.ts +121 -0
  159. package/src/launchd/sudo-guard.ts +89 -10
  160. package/src/launchd/sudo-modes.test.ts +135 -0
  161. package/src/launchd/sudo-runner.ts +40 -59
  162. package/src/launchd/sudo-said.ts +58 -0
  163. package/src/openbao/README.md +5 -2
  164. package/src/openbao/REPLACE.md +13 -11
  165. package/src/openbao/adopt-core.test.ts +119 -0
  166. package/src/openbao/adopt-families.test.ts +97 -0
  167. package/src/openbao/adopt-holes.test.ts +168 -0
  168. package/src/openbao/adopt-recovery.test.ts +71 -0
  169. package/src/openbao/auth-method-reconcile.ts +15 -3
  170. package/src/openbao/auth-method.ts +15 -6
  171. package/src/openbao/auth-role.ts +14 -4
  172. package/src/openbao/cloudflare-role.ts +17 -4
  173. package/src/openbao/fake-bao.ts +52 -0
  174. package/src/openbao/fake-core.ts +141 -0
  175. package/src/openbao/fake-families.ts +11 -5
  176. package/src/openbao/fake-mounts.ts +74 -0
  177. package/src/openbao/fake-stack.ts +17 -4
  178. package/src/openbao/jwt-config.ts +10 -7
  179. package/src/openbao/jwt-role.ts +10 -7
  180. package/src/openbao/kubernetes-role.ts +13 -7
  181. package/src/openbao/mfa-enforcement.ts +20 -6
  182. package/src/openbao/mfa-totp.ts +22 -7
  183. package/src/openbao/mount-move.test.ts +2 -35
  184. package/src/openbao/mount-reconcile.ts +13 -2
  185. package/src/openbao/mount.ts +15 -6
  186. package/src/openbao/pki-role.ts +15 -4
  187. package/src/openbao/plugin-reconcile.ts +16 -5
  188. package/src/openbao/plugin.ts +17 -6
  189. package/src/openbao/policy.ts +17 -4
  190. package/src/openbao/proxmox-role.ts +13 -4
  191. package/src/openbao/rename-adoption.test.ts +32 -19
  192. package/src/openbao/role-reconcile.ts +23 -0
  193. package/src/openbao/ssh-role.ts +14 -4
  194. package/src/ownership/adopt.ts +122 -0
  195. package/src/ownership/ownership.test.ts +200 -0
  196. package/src/ownership/probe.ts +65 -0
  197. package/src/ownership/resume.ts +118 -0
  198. package/src/ownership/rows.ts +117 -0
  199. package/src/ownership/whole.test.ts +39 -0
  200. package/src/ownership/whole.ts +60 -0
  201. package/src/proxmox/adopt-noop.test.ts +112 -0
  202. package/src/proxmox/ceph-pool-adopt.test.ts +88 -0
  203. package/src/proxmox/ceph-pool-form.ts +3 -3
  204. package/src/proxmox/ceph-pool.ts +10 -3
  205. package/src/proxmox/fake-bao-env.ts +28 -0
  206. package/src/proxmox/fake-pve-lxc.ts +210 -0
  207. package/src/proxmox/fake-pve.ts +75 -0
  208. package/src/proxmox/index.test.ts +6 -6
  209. package/src/proxmox/index.ts +6 -2
  210. package/src/proxmox/lxc-adopt.test.ts +170 -0
  211. package/src/proxmox/lxc-create-form.ts +76 -0
  212. package/src/proxmox/lxc-create.test.ts +126 -0
  213. package/src/proxmox/lxc-guards.test.ts +158 -0
  214. package/src/proxmox/lxc-harness.ts +150 -0
  215. package/src/proxmox/lxc-identity.ts +43 -0
  216. package/src/proxmox/lxc-judge.test.ts +175 -0
  217. package/src/proxmox/lxc-judge.ts +196 -0
  218. package/src/proxmox/lxc-lifecycle.ts +200 -0
  219. package/src/proxmox/lxc-ownership.test.ts +159 -0
  220. package/src/proxmox/lxc-props.ts +215 -0
  221. package/src/proxmox/lxc-read.test.ts +60 -0
  222. package/src/proxmox/lxc-volume.test.ts +96 -0
  223. package/src/proxmox/lxc-volume.ts +153 -0
  224. package/src/proxmox/lxc-wire.ts +210 -0
  225. package/src/proxmox/lxc.ts +216 -93
  226. package/src/proxmox/network-apply-read.ts +3 -1
  227. package/src/proxmox/pbs-datastore.ts +4 -3
  228. package/src/proxmox/resource.ts +7 -7
  229. package/src/proxmox/update-guard.ts +31 -0
  230. package/src/verify/args.ts +82 -0
  231. package/src/verify/cli.test.ts +86 -0
  232. package/src/verify/cli.ts +49 -0
  233. package/src/verify/fake-engine.ts +158 -0
  234. package/src/verify/index.ts +36 -0
  235. package/src/verify/report.ts +52 -0
  236. package/src/verify/rows.ts +169 -0
  237. package/src/verify/spy.test.ts +141 -0
  238. package/src/verify/spy.ts +149 -0
  239. package/src/verify/verify.test.ts +124 -0
  240. package/src/verify/verify.ts +172 -0
package/README.md CHANGED
@@ -104,6 +104,11 @@ Cloudflare and Proxmox roles, plugins — plus `assertBaoIdentity` (call it firs
104
104
  ⛔ **Metadata only.** Alchemy stores props and attributes unencrypted, so no secret, CA key,
105
105
  plugin `env` or OIDC client secret is declarable. Every family defaults to `retain` on destroy.
106
106
 
107
+ ⛔ **Nothing is adopted without `--adopt`** (0.9.0), for every family and even when the live object
108
+ is identical to the declaration; an interrupted create of this stack's own resumes when its row
109
+ can prove it (one killed while a prop was an `Output` needs `--adopt`). The same rule holds for
110
+ `HostFile`, `LaunchdJob`, `CaddyConfig` and `ProxmoxLxc`: [docs/ownership.md](./docs/ownership.md).
111
+
107
112
  ★ **Usage lives beside the code**, so it ships in the tarball with it:
108
113
  [src/openbao/README.md](./src/openbao/README.md). What each resource does on a rename is in
109
114
  [src/openbao/REPLACE.md](./src/openbao/REPLACE.md) — read it before changing a path or name.
@@ -131,9 +136,10 @@ one is logged (argv only, never content) before it runs. A plan never calls sudo
131
136
 
132
137
  - ⛔ **Opt-in, never a fallback**, and it never prompts: if sudo needs a password, the call fails at
133
138
  once and says to run `sudo -v`, or to grant exactly these commands `NOPASSWD`.
134
- - ⚠️ Refused before sudo is asked: any other argv, a prefix that is not a real directory only root
135
- may write, a path outside every prefix that needs root, a symlink on the way, a file you could
136
- not read back, and another user's `gui/<uid>`.
139
+ - ⚠️ Refused before sudo is asked: any other argv, a prefix — or any directory below it on the way
140
+ to the file — that is not a real directory only root may write, a root-owned file that would be
141
+ group/world-writable, setuid or setgid, a path outside every prefix that needs root, a file you
142
+ could not read back, and another user's `gui/<uid>`. A plan that will write runs the same checks.
137
143
  - Full list, sudoers cautions and limits: [docs/launchd-sudo.md](./docs/launchd-sudo.md).
138
144
 
139
145
  ## Caddy — `@homeflare/alchemy/caddy`
@@ -151,16 +157,21 @@ with `HostFile`; `caddyProviders()` provides the transport, `http://127.0.0.1:20
151
157
  - ★ **Managed Caddies run `--resume` with their own `XDG_CONFIG_HOME`**, so a restart runs the last
152
158
  config Caddy accepted — and after one, SIGUSR1 has no file to reload. Why, and the rest: same doc.
153
159
 
154
- ## Credentials
160
+ ## Proxmox — `@homeflare/alchemy/proxmox`
161
+
162
+ PVE and PBS objects over the PVE API, and `ProxmoxLxc` for containers. What the subpath never does
163
+ to a guest, and why: [docs/proxmox.md](./docs/proxmox.md).
155
164
 
156
- `CLOUDFLARE_API_TOKEN` is read from the environment at call time, never at module scope.
157
- `MeshNode` instead resolves credentials and the account the way `Cloudflare.providers()` does
158
- (an Alchemy profile, or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` in CI).
165
+ ## Adopt verifier — `hf-adopt-verify` / `@homeflare/alchemy/verify`
166
+
167
+ ⛔ A plan prints `adopted` for a match and for a drift alike. Before a gated deploy run
168
+ `bunx --bun hf-adopt-verify --config alchemy.run.ts --stage live`: Alchemy's planner, no writes,
169
+ each row's own diff, exit 0 only when all are no-ops. [docs/adopt-verify.md](./docs/adopt-verify.md)
170
+
171
+ ## Credentials
159
172
 
160
- ⛔ **Mint a short-lived, scoped token** — do not reuse a long-lived one, and never a Global
161
- API Key. An empty value fails closed with a message saying so, because an empty render is
162
- a denied grant rather than a missing file, and `Bearer ` + nothing 401s in a way that reads
163
- like a bad credential.
173
+ ⛔ **Mint a short-lived, scoped token** — never a long-lived one, never a Global API Key. How each
174
+ subpath reads its credential, and why an empty one fails closed: [docs/credentials.md](./docs/credentials.md).
164
175
 
165
176
  ## Peers — and one override you need
166
177
 
@@ -70,7 +70,7 @@ export type Applied = {
70
70
  * What this apply may load over — config.ts decides from the state and the adopt setting:
71
71
  * · `takeOver` — any running config: the state was applied to THIS Caddy (an update, drift
72
72
  * correction, or a create the engine adopted), or adoption is on (`--adopt`, or the
73
- * resource's own `adopt(true)` — host-effect.ts adoptEnabled).
73
+ * resource's own `adopt(true)` — ownership/adopt.ts adoptEnabled).
74
74
  * · otherwise only a claimable one — plus, as `stored`, the digest the state last recorded: a
75
75
  * Caddy reached at a NEW endpoint that runs exactly that is the same config under another name.
76
76
  */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/caddy/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAyB,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGhF,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,gBAAgB,EAChB,qBAAqB,CACtB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAEtB,CAAC;AAsBH,eAAO,MAAM,mBAAmB,8FAsF7B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/caddy/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAAE,iBAAiB,EAAyB,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGhF,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,gBAAgB,EAChB,qBAAqB,CACtB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAEtB,CAAC;AAsBH,eAAO,MAAM,mBAAmB,8FAsF7B,CAAC"}
@@ -292,17 +292,13 @@ var loadCaddyfile = async (admin, caddyfile, sourceFile) => {
292
292
  };
293
293
  // src/caddy/config.ts
294
294
  import { Resource } from "alchemy";
295
- import { Unowned } from "alchemy/AdoptPolicy";
295
+ import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
296
296
  import { isResolved } from "alchemy/Diff";
297
297
  import * as Provider from "alchemy/Provider";
298
- import * as Effect2 from "effect/Effect";
298
+ import * as Effect6 from "effect/Effect";
299
299
 
300
300
  // src/launchd/host-effect.ts
301
- import { AdoptPolicy } from "alchemy/AdoptPolicy";
302
- import { AlchemyContext } from "alchemy/AlchemyContext";
303
- import { Stack } from "alchemy/Stack";
304
301
  import * as Effect from "effect/Effect";
305
- import * as Option from "effect/Option";
306
302
  var lift = (step) => Effect.tryPromise({
307
303
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
308
304
  try: step
@@ -313,17 +309,183 @@ var resolvedString = (news, key) => {
313
309
  const value = news[key];
314
310
  return typeof value === "string" ? value : undefined;
315
311
  };
316
- var adoptEnabled = (fqn) => Effect.gen(function* () {
317
- const stack = yield* Effect.serviceOption(Stack);
318
- const scoped = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : undefined;
312
+
313
+ // src/ownership/adopt.ts
314
+ import { AdoptPolicy } from "alchemy/AdoptPolicy";
315
+ import { AlchemyContext } from "alchemy/AlchemyContext";
316
+ import { Stack as Stack3 } from "alchemy/Stack";
317
+ import * as Effect5 from "effect/Effect";
318
+ import * as Option4 from "effect/Option";
319
+
320
+ // src/ownership/resume.ts
321
+ import { Unowned } from "alchemy/AdoptPolicy";
322
+ import { Artifacts } from "alchemy/Artifacts";
323
+ import * as Effect2 from "effect/Effect";
324
+ import * as Option from "effect/Option";
325
+ var PROVEN = "homeflare/ownership/resumes";
326
+ var UNFINISHED = "homeflare/ownership/unfinished";
327
+ var note = (key, instanceId) => Effect2.gen(function* () {
328
+ const bag = yield* Effect2.serviceOption(Artifacts);
329
+ if (Option.isSome(bag))
330
+ yield* bag.value.set(key, instanceId);
331
+ });
332
+ var noted = (key, instanceId) => Effect2.gen(function* () {
333
+ const bag = yield* Effect2.serviceOption(Artifacts);
334
+ if (Option.isNone(bag))
335
+ return false;
336
+ return (yield* bag.value.get(key)) === instanceId;
337
+ });
338
+ var noteUnfinished = (instanceId) => Effect2.as(note(UNFINISHED, instanceId), undefined);
339
+ var noteResume = (instanceId, recovered) => Effect2.gen(function* () {
340
+ yield* noteUnfinished(instanceId);
341
+ const unproven = () => Effect2.succeed(undefined);
342
+ const found = yield* recovered.pipe(Effect2.catch(unproven), Effect2.catchDefect(unproven));
343
+ if (found === undefined || found === null || Unowned.is(found))
344
+ return;
345
+ yield* note(PROVEN, instanceId);
346
+ });
347
+ var resumes = (instanceId) => noted(PROVEN, instanceId);
348
+ var unfinished = (instanceId) => noted(UNFINISHED, instanceId);
349
+ var provingResumes = (service) => {
350
+ const { diff, read } = service;
351
+ if (diff === undefined || read === undefined)
352
+ return service;
353
+ const proving = (input) => Effect2.gen(function* () {
354
+ if (input.output === undefined) {
355
+ yield* noteResume(input.instanceId, read({ ...input, output: undefined }));
356
+ }
357
+ return yield* diff(input);
358
+ });
359
+ return { ...service, diff: proving };
360
+ };
361
+
362
+ // src/ownership/rows.ts
363
+ import { Stack as Stack2 } from "alchemy/Stack";
364
+ import { State, isActionState } from "alchemy/State/State";
365
+ import * as Effect4 from "effect/Effect";
366
+ import * as Option3 from "effect/Option";
367
+
368
+ // src/ownership/whole.ts
369
+ import { Stack } from "alchemy/Stack";
370
+ import * as Effect3 from "effect/Effect";
371
+ import * as Option2 from "effect/Option";
372
+ var absent = (value) => value === undefined || value === null;
373
+ var isPlainObject2 = (value) => {
374
+ if (typeof value !== "object" || value === null || Array.isArray(value))
375
+ return false;
376
+ const proto = Object.getPrototypeOf(value);
377
+ return proto === Object.prototype || proto === null;
378
+ };
379
+ var carries = (declared, recorded) => {
380
+ if (absent(declared))
381
+ return true;
382
+ if (absent(recorded))
383
+ return false;
384
+ if (Array.isArray(declared)) {
385
+ if (!Array.isArray(recorded))
386
+ return false;
387
+ return declared.every((each, index) => carries(each, recorded[index]));
388
+ }
389
+ if (isPlainObject2(declared)) {
390
+ if (!isPlainObject2(recorded))
391
+ return false;
392
+ return Object.entries(declared).every(([key, each]) => carries(each, recorded[key]));
393
+ }
394
+ return true;
395
+ };
396
+ var wholeDeclaration = (fqn, props) => Effect3.map(Effect3.serviceOption(Stack), (stack) => {
397
+ const declared = Option2.isSome(stack) ? stack.value.resources[fqn] : undefined;
398
+ return declared !== undefined && carries(declared.Props, props);
399
+ });
400
+
401
+ // src/ownership/rows.ts
402
+ var storeOf = Effect4.gen(function* () {
403
+ const stack = yield* Effect4.serviceOption(Stack2);
404
+ const state = yield* Effect4.serviceOption(State);
405
+ if (Option3.isNone(stack) || Option3.isNone(state))
406
+ return;
407
+ const store = yield* state.value;
408
+ const where = { stack: stack.value.name, stage: stack.value.stage };
409
+ return { resources: stack.value.resources, store, where };
410
+ });
411
+ var rowAt = (store, where, fqn) => store.get({ ...where, fqn }).pipe(Effect4.orElseSucceed(() => {
412
+ return;
413
+ }));
414
+ var generationOf = (row, instanceId) => {
415
+ let at = row;
416
+ while (typeof at === "object" && at !== null && !isActionState(at)) {
417
+ const generation = at;
418
+ if (generation.instanceId === instanceId)
419
+ return generation;
420
+ at = generation.old;
421
+ }
422
+ return;
423
+ };
424
+ var recordedGeneration = (fqn, instanceId) => Effect4.gen(function* () {
425
+ const found = yield* storeOf;
426
+ if (found === undefined)
427
+ return;
428
+ for (const at of [fqn, ...found.resources[fqn]?.FormerFqns ?? []]) {
429
+ const generation = generationOf(yield* rowAt(found.store, found.where, at), instanceId);
430
+ if (generation !== undefined)
431
+ return generation;
432
+ }
433
+ return;
434
+ });
435
+ var recordedInstance = (fqn, instanceId) => Effect4.gen(function* () {
436
+ const generation = yield* recordedGeneration(fqn, instanceId);
437
+ if (generation === undefined)
438
+ return "absent";
439
+ return (yield* wholeDeclaration(fqn, generation.props)) ? "whole" : "partial";
440
+ });
441
+ var isCreate = (fqn, instanceId) => Effect4.map(recordedGeneration(fqn, instanceId), (generation) => generation?.status === "creating");
442
+ var forgetRefusedCreate = (fqn, instanceId) => Effect4.gen(function* () {
443
+ const found = yield* storeOf;
444
+ if (found === undefined)
445
+ return;
446
+ const row = yield* rowAt(found.store, found.where, fqn);
447
+ const mine = row;
448
+ if (mine?.status !== "creating" || mine.instanceId !== instanceId || mine.attr !== undefined) {
449
+ return;
450
+ }
451
+ yield* found.store.delete({ ...found.where, fqn }).pipe(Effect4.ignore);
452
+ });
453
+
454
+ // src/ownership/adopt.ts
455
+ var adoptEnabled = (fqn) => Effect5.gen(function* () {
456
+ const stack = yield* Effect5.serviceOption(Stack3);
457
+ const scoped = Option4.isSome(stack) ? stack.value.resources[fqn]?.Adopt : undefined;
319
458
  if (scoped !== undefined)
320
459
  return scoped;
321
- const policy = yield* Effect.serviceOption(AdoptPolicy);
322
- if (Option.isSome(policy))
460
+ const policy = yield* Effect5.serviceOption(AdoptPolicy);
461
+ if (Option4.isSome(policy))
323
462
  return policy.value;
324
- const context = yield* Effect.serviceOption(AlchemyContext);
325
- return Option.isSome(context) ? context.value.adopt : false;
463
+ const context = yield* Effect5.serviceOption(AlchemyContext);
464
+ return Option4.isSome(context) ? context.value.adopt : false;
465
+ });
466
+ var adoptable = (owner) => Effect5.gen(function* () {
467
+ if (yield* isCreate(owner.fqn, owner.instanceId))
468
+ return true;
469
+ return yield* unfinished(owner.instanceId);
470
+ });
471
+ var adoptsAtApply = (owner) => Effect5.gen(function* () {
472
+ if (!(yield* adoptEnabled(owner.fqn)))
473
+ return false;
474
+ return yield* adoptable(owner);
475
+ });
476
+ var refuseTakeover = (owner, what) => Effect5.gen(function* () {
477
+ if (owner.output !== undefined)
478
+ return;
479
+ if (yield* resumes(owner.instanceId))
480
+ return;
481
+ const may = yield* adoptable(owner);
482
+ if (may && (yield* adoptEnabled(owner.fqn)))
483
+ return;
484
+ yield* forgetRefusedCreate(owner.fqn, owner.instanceId);
485
+ const remedy = may ? "Deploy with --adopt (or wrap the resource in adopt(true)) to take it over, or remove it first." : "It is the new identity of a replace, which --adopt does not cover: remove it first.";
486
+ return yield* Effect5.die(new Error(`${what}: already exists, and this stack holds no state for it — the plan could not ask ` + "first (a prop was still an Output, a replace moved onto it, or it appeared after the " + `plan). Nothing was written. ${remedy}`));
326
487
  });
488
+ var claimFor = (owner) => (what) => refuseTakeover(owner, what);
327
489
 
328
490
  // src/caddy/admin-guard.ts
329
491
  var DEFAULT_ADMIN_PORT = 2019;
@@ -629,46 +791,46 @@ var CaddyConfig = Resource("Caddy.Config", {
629
791
  defaultRemovalPolicy: "retain"
630
792
  });
631
793
  var unreachable = (error) => error instanceof CaddyUnreachableError;
632
- var planWithoutCaddy = (admin, fallback) => (error) => Effect2.as(Effect2.logWarning(`Caddy.Config: no Caddy at ${admin.endpoint} (${error.message}); planning without it — ` + "the Caddyfile is validated when the deploy reaches Caddy"), fallback);
633
- var CaddyConfigProvider = () => Provider.effect(CaddyConfig, Effect2.gen(function* () {
794
+ var planWithoutCaddy = (admin, fallback) => (error) => Effect6.as(Effect6.logWarning(`Caddy.Config: no Caddy at ${admin.endpoint} (${error.message}); planning without it — ` + "the Caddyfile is validated when the deploy reaches Caddy"), fallback);
795
+ var CaddyConfigProvider = () => Provider.effect(CaddyConfig, Effect6.gen(function* () {
634
796
  const admin = yield* CaddyAdminService;
635
- const probe = (caddyfile, sourceFile) => Effect2.flatMap(lift(() => probeLive(admin, caddyfile, sourceFile)), (found) => {
797
+ const probe = (caddyfile, sourceFile) => Effect6.flatMap(lift(() => probeLive(admin, caddyfile, sourceFile)), (found) => {
636
798
  if (found === undefined)
637
- return Effect2.succeed(undefined);
799
+ return Effect6.succeed(undefined);
638
800
  if (found.ours)
639
- return Effect2.succeed(found.attributes);
640
- const unowned = Unowned(found.attributes);
641
- return found.unchecked === undefined ? Effect2.succeed(unowned) : Effect2.as(Effect2.logWarning(`Caddy.Config at ${admin.endpoint}: the running config could not be compared ` + `with the declared Caddyfile (${found.unchecked}), so it is not treated as ours`), unowned);
801
+ return Effect6.succeed(found.attributes);
802
+ const unowned = Unowned2(found.attributes);
803
+ return found.unchecked === undefined ? Effect6.succeed(unowned) : Effect6.as(Effect6.logWarning(`Caddy.Config at ${admin.endpoint}: the running config could not be compared ` + `with the declared Caddyfile (${found.unchecked}), so it is not treated as ours`), unowned);
642
804
  });
643
805
  return CaddyConfig.Provider.of({
644
- list: () => Effect2.succeed([]),
645
- read: ({ olds, output }) => (output === undefined ? probe(resolvedString(olds, "caddyfile"), resolvedString(olds, "sourceFile")) : lift(() => readLive(admin, resolvedString(olds, "sourceFile")))).pipe(Effect2.catchIf(unreachable, planWithoutCaddy(admin, undefined))),
806
+ list: () => Effect6.succeed([]),
807
+ read: ({ olds, output }) => (output === undefined ? probe(resolvedString(olds, "caddyfile"), resolvedString(olds, "sourceFile")) : lift(() => readLive(admin, resolvedString(olds, "sourceFile")))).pipe(Effect6.catchIf(unreachable, planWithoutCaddy(admin, undefined))),
646
808
  diff: ({ news, output }) => {
647
809
  if (output === undefined)
648
- return Effect2.succeed(undefined);
810
+ return Effect6.succeed(undefined);
649
811
  const update = planWithoutCaddy(admin, { action: "update" });
650
812
  if (isResolved(news)) {
651
- return lift(() => diffConfig(admin, news, output)).pipe(Effect2.catchIf(unreachable, update));
813
+ return lift(() => diffConfig(admin, news, output)).pipe(Effect6.catchIf(unreachable, update));
652
814
  }
653
815
  const caddyfile = resolvedString(news, "caddyfile");
654
816
  if (caddyfile === undefined)
655
- return Effect2.succeed(undefined);
817
+ return Effect6.succeed(undefined);
656
818
  return lift(async () => {
657
819
  await diffConfig(admin, { caddyfile }, output);
658
820
  return { action: "update" };
659
- }).pipe(Effect2.catchIf(unreachable, update));
821
+ }).pipe(Effect6.catchIf(unreachable, update));
660
822
  },
661
- reconcile: ({ fqn, news, output }) => Effect2.gen(function* () {
823
+ reconcile: ({ fqn, news, output }) => Effect6.gen(function* () {
662
824
  const sameCaddy = output !== undefined && output.endpoint === admin.endpoint;
663
825
  const takeOver = sameCaddy || (yield* adoptEnabled(fqn));
664
826
  const stored = output === undefined ? {} : { stored: output.configSha256 };
665
827
  const applied = yield* lift(() => reconcileConfig(admin, news, { takeOver, ...stored }));
666
828
  for (const warning of applied.warnings) {
667
- yield* Effect2.logWarning(`Caddy.Config at ${admin.endpoint}: ${warning}`);
829
+ yield* Effect6.logWarning(`Caddy.Config at ${admin.endpoint}: ${warning}`);
668
830
  }
669
831
  return applied.attributes;
670
832
  }),
671
- delete: () => Effect2.void
833
+ delete: () => Effect6.void
672
834
  });
673
835
  }));
674
836
  // src/caddy/local-admin.ts
@@ -766,15 +928,15 @@ var localCaddyAdmin = (options = {}) => {
766
928
  import * as Layer2 from "effect/Layer";
767
929
  var caddyProviders = (admin = localCaddyAdmin()) => CaddyConfigProvider().pipe(Layer2.provide(caddyAdminLayer(admin)));
768
930
  // src/caddy/with-file.ts
769
- import * as Effect4 from "effect/Effect";
931
+ import * as Effect8 from "effect/Effect";
770
932
  import * as RemovalPolicy from "alchemy/RemovalPolicy";
771
933
 
772
934
  // src/launchd/host-file.ts
773
935
  import { Resource as Resource2 } from "alchemy";
774
- import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
936
+ import { Unowned as Unowned3 } from "alchemy/AdoptPolicy";
775
937
  import { isResolved as isResolved2 } from "alchemy/Diff";
776
938
  import * as Provider2 from "alchemy/Provider";
777
- import * as Effect3 from "effect/Effect";
939
+ import * as Effect7 from "effect/Effect";
778
940
 
779
941
  // src/launchd/host-file-form.ts
780
942
  var DEFAULT_MODE = 420;
@@ -859,17 +1021,26 @@ var readFileAttributes = async (runner, path) => {
859
1021
  uid: stat.uid
860
1022
  };
861
1023
  };
1024
+ var writeOptionsOf = (want) => ({
1025
+ mode: want.mode,
1026
+ ...want.uid === undefined ? {} : { uid: want.uid },
1027
+ ...want.gid === undefined ? {} : { gid: want.gid }
1028
+ });
862
1029
  var matches = (live, want) => live.sha256 === want.sha256 && live.mode === want.mode && (want.uid === undefined || live.uid === want.uid) && (want.gid === undefined || live.gid === want.gid);
863
1030
  var diffFile = async (runner, news, output) => {
864
- if (news.path !== output.path)
865
- return { action: "replace" };
866
1031
  const want = await desiredFile(runner, news);
1032
+ const writes = async (diff) => {
1033
+ await runner.checkWrite?.(news.path, writeOptionsOf(want));
1034
+ return diff;
1035
+ };
1036
+ if (news.path !== output.path)
1037
+ return writes({ action: "replace" });
867
1038
  if (want.sha256 !== output.sha256)
868
- return { action: "update" };
1039
+ return writes({ action: "update" });
869
1040
  const live = await readFileAttributes(runner, news.path);
870
- return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
1041
+ return live !== undefined && matches(live, want) ? { action: "noop" } : writes({ action: "update" });
871
1042
  };
872
- var reconcileFile = async (runner, props, output) => {
1043
+ var reconcileFile = async (runner, props, output, adopt = false) => {
873
1044
  const want = await desiredFile(runner, props);
874
1045
  if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
875
1046
  throw refuse3(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (sudoRunner() with this path under a prefix). This " + "provider never calls sudo itself.");
@@ -881,15 +1052,12 @@ var reconcileFile = async (runner, props, output) => {
881
1052
  const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
882
1053
  const moved = output !== undefined && output.path !== props.path;
883
1054
  const prior = moved ? undefined : output;
884
- if (prior === undefined && before !== undefined && !matches(before, want)) {
1055
+ const takeOver = adopt && output === undefined;
1056
+ if (prior === undefined && before !== undefined && !matches(before, want) && !takeOver) {
885
1057
  throw refuse3(props.path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
886
1058
  }
887
1059
  if (before === undefined || !matches(before, want)) {
888
- await runner.writeFileAtomic(props.path, want.bytes, {
889
- mode: want.mode,
890
- ...want.uid === undefined ? {} : { uid: want.uid },
891
- ...want.gid === undefined ? {} : { gid: want.gid }
892
- });
1060
+ await runner.writeFileAtomic(props.path, want.bytes, writeOptionsOf(want));
893
1061
  }
894
1062
  const after = await readFileAttributes(runner, props.path);
895
1063
  if (after === undefined || !matches(after, want)) {
@@ -914,34 +1082,34 @@ var deleteFile = async (runner, output) => {
914
1082
 
915
1083
  // src/launchd/host-file.ts
916
1084
  var HostFile = Resource2("Host.File");
917
- var HostFileProvider = () => Provider2.effect(HostFile, Effect3.gen(function* () {
1085
+ var HostFileProvider = () => Provider2.effect(HostFile, Effect7.gen(function* () {
918
1086
  const runner = yield* HostRunnerService;
919
1087
  return HostFile.Provider.of({
920
- list: () => Effect3.succeed([]),
1088
+ list: () => Effect7.succeed([]),
921
1089
  read: ({ olds, output }) => lift(async () => {
922
1090
  const found = await readFileAttributes(runner, olds.path);
923
1091
  if (found === undefined)
924
1092
  return;
925
- return output === undefined ? Unowned2(found) : found;
1093
+ return output === undefined ? Unowned3(found) : found;
926
1094
  }),
927
- diff: ({ news, output }) => {
1095
+ diff: ({ instanceId, news, output }) => {
928
1096
  if (output === undefined)
929
- return Effect3.succeed(undefined);
1097
+ return noteUnfinished(instanceId);
930
1098
  if (isResolved2(news))
931
1099
  return lift(() => diffFile(runner, news, output));
932
1100
  const path = resolvedString(news, "path");
933
- return Effect3.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
1101
+ return Effect7.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
934
1102
  },
935
- reconcile: ({ news, output }) => lift(() => reconcileFile(runner, news, output)),
1103
+ reconcile: ({ fqn, instanceId, news, output }) => Effect7.flatMap(adoptsAtApply({ fqn, instanceId, output }), (adopt) => lift(() => reconcileFile(runner, news, output, adopt))),
936
1104
  delete: ({ output }) => lift(() => deleteFile(runner, output))
937
1105
  });
938
1106
  }));
939
1107
 
940
1108
  // src/caddy/with-file.ts
941
- var caddyWithFile = (id, props) => Effect4.gen(function* () {
1109
+ var caddyWithFile = (id, props) => Effect8.gen(function* () {
942
1110
  const found = configProblems({ caddyfile: props.caddyfile, sourceFile: props.path });
943
1111
  if (found.length > 0) {
944
- return yield* Effect4.fail(new Error(`caddyWithFile ${id}: ${found.join("; ")}`));
1112
+ return yield* Effect8.fail(new Error(`caddyWithFile ${id}: ${found.join("; ")}`));
945
1113
  }
946
1114
  const file = yield* HostFile(`${id}-file`, {
947
1115
  content: props.caddyfile,
@@ -966,5 +1134,5 @@ export {
966
1134
  localCaddyAdmin
967
1135
  };
968
1136
 
969
- //# debugId=7B177B132A41844D64756E2164756E21
1137
+ //# debugId=13AB2C6EDF123C4E64756E2164756E21
970
1138
  //# sourceMappingURL=index.js.map