@homeflare/alchemy 0.7.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 (312) hide show
  1. package/README.md +45 -45
  2. package/dist/caddy/config-lifecycle.d.ts +49 -3
  3. package/dist/caddy/config-lifecycle.d.ts.map +1 -1
  4. package/dist/caddy/config.d.ts +6 -2
  5. package/dist/caddy/config.d.ts.map +1 -1
  6. package/dist/caddy/fake-caddy.d.ts +11 -0
  7. package/dist/caddy/fake-caddy.d.ts.map +1 -1
  8. package/dist/caddy/index.d.ts +2 -1
  9. package/dist/caddy/index.d.ts.map +1 -1
  10. package/dist/caddy/index.js +270 -44
  11. package/dist/caddy/index.js.map +15 -11
  12. package/dist/caddy/with-file.d.ts +7 -3
  13. package/dist/caddy/with-file.d.ts.map +1 -1
  14. package/dist/launchd/fake-runner.d.ts +1 -0
  15. package/dist/launchd/fake-runner.d.ts.map +1 -1
  16. package/dist/launchd/fake-sudo.d.ts +51 -0
  17. package/dist/launchd/fake-sudo.d.ts.map +1 -0
  18. package/dist/launchd/host-effect.d.ts +1 -1
  19. package/dist/launchd/host-file-form.d.ts +5 -0
  20. package/dist/launchd/host-file-form.d.ts.map +1 -1
  21. package/dist/launchd/host-file-lifecycle.d.ts +14 -1
  22. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  23. package/dist/launchd/host-file.d.ts +2 -1
  24. package/dist/launchd/host-file.d.ts.map +1 -1
  25. package/dist/launchd/index.d.ts +2 -0
  26. package/dist/launchd/index.d.ts.map +1 -1
  27. package/dist/launchd/index.js +642 -49
  28. package/dist/launchd/index.js.map +24 -14
  29. package/dist/launchd/job-lifecycle.d.ts +8 -1
  30. package/dist/launchd/job-lifecycle.d.ts.map +1 -1
  31. package/dist/launchd/job-preflight.d.ts +13 -1
  32. package/dist/launchd/job-preflight.d.ts.map +1 -1
  33. package/dist/launchd/job-validate.d.ts +2 -0
  34. package/dist/launchd/job-validate.d.ts.map +1 -1
  35. package/dist/launchd/job.d.ts +2 -1
  36. package/dist/launchd/job.d.ts.map +1 -1
  37. package/dist/launchd/local-runner.d.ts +2 -0
  38. package/dist/launchd/local-runner.d.ts.map +1 -1
  39. package/dist/launchd/providers.d.ts +2 -1
  40. package/dist/launchd/providers.d.ts.map +1 -1
  41. package/dist/launchd/runner.d.ts +14 -5
  42. package/dist/launchd/runner.d.ts.map +1 -1
  43. package/dist/launchd/sudo-acl.d.ts +25 -0
  44. package/dist/launchd/sudo-acl.d.ts.map +1 -0
  45. package/dist/launchd/sudo-allowlist.d.ts +47 -0
  46. package/dist/launchd/sudo-allowlist.d.ts.map +1 -0
  47. package/dist/launchd/sudo-guard.d.ts +83 -0
  48. package/dist/launchd/sudo-guard.d.ts.map +1 -0
  49. package/dist/launchd/sudo-runner.d.ts +60 -0
  50. package/dist/launchd/sudo-runner.d.ts.map +1 -0
  51. package/dist/launchd/sudo-said.d.ts +10 -0
  52. package/dist/launchd/sudo-said.d.ts.map +1 -0
  53. package/dist/launchd/sudo-stage.d.ts +7 -0
  54. package/dist/launchd/sudo-stage.d.ts.map +1 -0
  55. package/dist/openbao/auth-method-reconcile.d.ts +6 -2
  56. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -1
  57. package/dist/openbao/auth-method.d.ts.map +1 -1
  58. package/dist/openbao/auth-role-form.d.ts +0 -9
  59. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  60. package/dist/openbao/auth-role.d.ts +3 -2
  61. package/dist/openbao/auth-role.d.ts.map +1 -1
  62. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  63. package/dist/openbao/fake-bao.d.ts +18 -0
  64. package/dist/openbao/fake-bao.d.ts.map +1 -1
  65. package/dist/openbao/fake-core.d.ts +39 -0
  66. package/dist/openbao/fake-core.d.ts.map +1 -0
  67. package/dist/openbao/fake-engines-roles.d.ts +32 -0
  68. package/dist/openbao/fake-engines-roles.d.ts.map +1 -0
  69. package/dist/openbao/fake-engines.d.ts +8 -2
  70. package/dist/openbao/fake-engines.d.ts.map +1 -1
  71. package/dist/openbao/fake-families.d.ts +52 -0
  72. package/dist/openbao/fake-families.d.ts.map +1 -0
  73. package/dist/openbao/fake-mounts.d.ts +16 -0
  74. package/dist/openbao/fake-mounts.d.ts.map +1 -0
  75. package/dist/openbao/fake-stack.d.ts +5 -1
  76. package/dist/openbao/fake-stack.d.ts.map +1 -1
  77. package/dist/openbao/host-approles.d.ts.map +1 -1
  78. package/dist/openbao/index.js +709 -359
  79. package/dist/openbao/index.js.map +32 -25
  80. package/dist/openbao/jwt-config.d.ts +5 -0
  81. package/dist/openbao/jwt-config.d.ts.map +1 -1
  82. package/dist/openbao/jwt-role.d.ts +2 -1
  83. package/dist/openbao/jwt-role.d.ts.map +1 -1
  84. package/dist/openbao/kubernetes-role.d.ts +2 -1
  85. package/dist/openbao/kubernetes-role.d.ts.map +1 -1
  86. package/dist/openbao/mfa-enforcement.d.ts +12 -5
  87. package/dist/openbao/mfa-enforcement.d.ts.map +1 -1
  88. package/dist/openbao/mfa-totp.d.ts +6 -2
  89. package/dist/openbao/mfa-totp.d.ts.map +1 -1
  90. package/dist/openbao/mount-reconcile.d.ts +5 -1
  91. package/dist/openbao/mount-reconcile.d.ts.map +1 -1
  92. package/dist/openbao/mount.d.ts.map +1 -1
  93. package/dist/openbao/pki-role.d.ts.map +1 -1
  94. package/dist/openbao/plugin-reconcile.d.ts +6 -2
  95. package/dist/openbao/plugin-reconcile.d.ts.map +1 -1
  96. package/dist/openbao/plugin.d.ts.map +1 -1
  97. package/dist/openbao/policy.d.ts.map +1 -1
  98. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  99. package/dist/openbao/rename-identity.d.ts +69 -0
  100. package/dist/openbao/rename-identity.d.ts.map +1 -0
  101. package/dist/openbao/rename.d.ts +13 -12
  102. package/dist/openbao/rename.d.ts.map +1 -1
  103. package/dist/openbao/role-reconcile.d.ts +11 -1
  104. package/dist/openbao/role-reconcile.d.ts.map +1 -1
  105. package/dist/openbao/ssh-role-erase.d.ts +11 -0
  106. package/dist/openbao/ssh-role-erase.d.ts.map +1 -0
  107. package/dist/openbao/ssh-role.d.ts.map +1 -1
  108. package/dist/ownership/adopt.d.ts +52 -0
  109. package/dist/ownership/adopt.d.ts.map +1 -0
  110. package/dist/ownership/probe.d.ts +13 -0
  111. package/dist/ownership/probe.d.ts.map +1 -0
  112. package/dist/ownership/resume.d.ts +40 -0
  113. package/dist/ownership/resume.d.ts.map +1 -0
  114. package/dist/ownership/rows.d.ts +33 -0
  115. package/dist/ownership/rows.d.ts.map +1 -0
  116. package/dist/ownership/whole.d.ts +14 -0
  117. package/dist/ownership/whole.d.ts.map +1 -0
  118. package/dist/proxmox/ceph-pool-form.d.ts +3 -3
  119. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  120. package/dist/proxmox/fake-bao-env.d.ts +3 -0
  121. package/dist/proxmox/fake-bao-env.d.ts.map +1 -0
  122. package/dist/proxmox/fake-pve-lxc.d.ts +42 -0
  123. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -0
  124. package/dist/proxmox/fake-pve.d.ts +38 -0
  125. package/dist/proxmox/fake-pve.d.ts.map +1 -0
  126. package/dist/proxmox/index.d.ts +6 -2
  127. package/dist/proxmox/index.d.ts.map +1 -1
  128. package/dist/proxmox/index.js +970 -186
  129. package/dist/proxmox/index.js.map +22 -9
  130. package/dist/proxmox/lxc-create-form.d.ts +6 -0
  131. package/dist/proxmox/lxc-create-form.d.ts.map +1 -0
  132. package/dist/proxmox/lxc-harness.d.ts +34 -0
  133. package/dist/proxmox/lxc-harness.d.ts.map +1 -0
  134. package/dist/proxmox/lxc-identity.d.ts +18 -0
  135. package/dist/proxmox/lxc-identity.d.ts.map +1 -0
  136. package/dist/proxmox/lxc-judge.d.ts +46 -0
  137. package/dist/proxmox/lxc-judge.d.ts.map +1 -0
  138. package/dist/proxmox/lxc-lifecycle.d.ts +68 -0
  139. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -0
  140. package/dist/proxmox/lxc-props.d.ts +139 -0
  141. package/dist/proxmox/lxc-props.d.ts.map +1 -0
  142. package/dist/proxmox/lxc-volume.d.ts +29 -0
  143. package/dist/proxmox/lxc-volume.d.ts.map +1 -0
  144. package/dist/proxmox/lxc-wire.d.ts +51 -0
  145. package/dist/proxmox/lxc-wire.d.ts.map +1 -0
  146. package/dist/proxmox/lxc.d.ts +27 -47
  147. package/dist/proxmox/lxc.d.ts.map +1 -1
  148. package/dist/proxmox/network-apply-read.d.ts +1 -1
  149. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  150. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  151. package/dist/proxmox/resource.d.ts.map +1 -1
  152. package/dist/proxmox/update-guard.d.ts +27 -0
  153. package/dist/proxmox/update-guard.d.ts.map +1 -0
  154. package/dist/verify/args.d.ts +16 -0
  155. package/dist/verify/args.d.ts.map +1 -0
  156. package/dist/verify/cli.d.ts +3 -0
  157. package/dist/verify/cli.d.ts.map +1 -0
  158. package/dist/verify/cli.js +320 -0
  159. package/dist/verify/cli.js.map +15 -0
  160. package/dist/verify/fake-engine.d.ts +57 -0
  161. package/dist/verify/fake-engine.d.ts.map +1 -0
  162. package/dist/verify/index.d.ts +16 -0
  163. package/dist/verify/index.d.ts.map +1 -0
  164. package/dist/verify/index.js +297 -0
  165. package/dist/verify/index.js.map +14 -0
  166. package/dist/verify/report.d.ts +6 -0
  167. package/dist/verify/report.d.ts.map +1 -0
  168. package/dist/verify/rows.d.ts +57 -0
  169. package/dist/verify/rows.d.ts.map +1 -0
  170. package/dist/verify/spy.d.ts +31 -0
  171. package/dist/verify/spy.d.ts.map +1 -0
  172. package/dist/verify/verify.d.ts +43 -0
  173. package/dist/verify/verify.d.ts.map +1 -0
  174. package/docs/adopt-verify.md +108 -0
  175. package/docs/adopted-deploys.md +85 -0
  176. package/docs/caddy-admin.md +24 -0
  177. package/docs/caddy.md +73 -54
  178. package/docs/credentials.md +15 -0
  179. package/docs/launchd-sudo.md +147 -0
  180. package/docs/launchd.md +14 -7
  181. package/docs/ownership.md +113 -0
  182. package/docs/peers.md +39 -0
  183. package/docs/proxmox-lxc.md +196 -0
  184. package/docs/proxmox.md +12 -0
  185. package/package.json +9 -2
  186. package/src/caddy/adopt-scope.test.ts +82 -0
  187. package/src/caddy/config-adopt.test.ts +235 -0
  188. package/src/caddy/config-lifecycle.test.ts +31 -53
  189. package/src/caddy/config-lifecycle.ts +98 -7
  190. package/src/caddy/config.ts +48 -10
  191. package/src/caddy/fake-caddy.ts +19 -0
  192. package/src/caddy/index.ts +2 -1
  193. package/src/caddy/providers.test.ts +8 -23
  194. package/src/caddy/with-file.ts +7 -3
  195. package/src/launchd/adopt-apply.test.ts +166 -0
  196. package/src/launchd/fake-runner.ts +26 -4
  197. package/src/launchd/fake-sudo.ts +136 -0
  198. package/src/launchd/host-effect.ts +1 -1
  199. package/src/launchd/host-file-form.ts +10 -2
  200. package/src/launchd/host-file-lifecycle.ts +41 -15
  201. package/src/launchd/host-file.ts +13 -4
  202. package/src/launchd/index.ts +2 -0
  203. package/src/launchd/job-lifecycle.ts +22 -14
  204. package/src/launchd/job-preflight.ts +21 -3
  205. package/src/launchd/job-validate.ts +1 -1
  206. package/src/launchd/job.ts +13 -4
  207. package/src/launchd/local-runner.ts +6 -3
  208. package/src/launchd/providers.test.ts +3 -0
  209. package/src/launchd/providers.ts +2 -1
  210. package/src/launchd/runner.ts +14 -5
  211. package/src/launchd/sudo-acl.ts +68 -0
  212. package/src/launchd/sudo-allowlist.test.ts +191 -0
  213. package/src/launchd/sudo-allowlist.ts +213 -0
  214. package/src/launchd/sudo-chain.test.ts +121 -0
  215. package/src/launchd/sudo-guard.test.ts +100 -0
  216. package/src/launchd/sudo-guard.ts +230 -0
  217. package/src/launchd/sudo-lifecycle.test.ts +172 -0
  218. package/src/launchd/sudo-modes.test.ts +135 -0
  219. package/src/launchd/sudo-runner-files.test.ts +240 -0
  220. package/src/launchd/sudo-runner.test.ts +126 -0
  221. package/src/launchd/sudo-runner.ts +216 -0
  222. package/src/launchd/sudo-said.ts +58 -0
  223. package/src/launchd/sudo-stage.ts +28 -0
  224. package/src/openbao/README.md +11 -2
  225. package/src/openbao/REPLACE.md +67 -37
  226. package/src/openbao/adopt-core.test.ts +119 -0
  227. package/src/openbao/adopt-families.test.ts +97 -0
  228. package/src/openbao/adopt-holes.test.ts +168 -0
  229. package/src/openbao/adopt-recovery.test.ts +71 -0
  230. package/src/openbao/auth-method-reconcile.ts +15 -3
  231. package/src/openbao/auth-method.ts +15 -6
  232. package/src/openbao/auth-role-form.test.ts +2 -16
  233. package/src/openbao/auth-role-form.ts +0 -11
  234. package/src/openbao/auth-role.ts +48 -17
  235. package/src/openbao/cloudflare-role.ts +23 -18
  236. package/src/openbao/fake-bao.ts +52 -0
  237. package/src/openbao/fake-core.ts +141 -0
  238. package/src/openbao/fake-engines-roles.ts +114 -0
  239. package/src/openbao/fake-engines.ts +18 -11
  240. package/src/openbao/fake-families.ts +188 -0
  241. package/src/openbao/fake-mounts.ts +74 -0
  242. package/src/openbao/fake-stack.ts +17 -4
  243. package/src/openbao/host-approles.test.ts +15 -2
  244. package/src/openbao/host-approles.ts +19 -4
  245. package/src/openbao/jwt-config.ts +35 -11
  246. package/src/openbao/jwt-role.ts +33 -14
  247. package/src/openbao/kubernetes-role.ts +32 -14
  248. package/src/openbao/mfa-enforcement.ts +42 -13
  249. package/src/openbao/mfa-totp.ts +42 -14
  250. package/src/openbao/mount-move.test.ts +2 -35
  251. package/src/openbao/mount-reconcile.ts +13 -2
  252. package/src/openbao/mount.ts +15 -6
  253. package/src/openbao/pki-role.ts +33 -10
  254. package/src/openbao/plugin-reconcile.ts +16 -5
  255. package/src/openbao/plugin.ts +57 -20
  256. package/src/openbao/policy.ts +24 -11
  257. package/src/openbao/proxmox-role.ts +20 -20
  258. package/src/openbao/rename-adoption.test.ts +95 -0
  259. package/src/openbao/rename-families.test.ts +122 -0
  260. package/src/openbao/rename-identity.test.ts +129 -0
  261. package/src/openbao/rename-identity.ts +117 -0
  262. package/src/openbao/rename.ts +19 -15
  263. package/src/openbao/role-reconcile.ts +23 -0
  264. package/src/openbao/ssh-role-erase.ts +50 -0
  265. package/src/openbao/ssh-role.ts +29 -57
  266. package/src/ownership/adopt.ts +122 -0
  267. package/src/ownership/ownership.test.ts +200 -0
  268. package/src/ownership/probe.ts +65 -0
  269. package/src/ownership/resume.ts +118 -0
  270. package/src/ownership/rows.ts +117 -0
  271. package/src/ownership/whole.test.ts +39 -0
  272. package/src/ownership/whole.ts +60 -0
  273. package/src/proxmox/adopt-noop.test.ts +112 -0
  274. package/src/proxmox/ceph-pool-adopt.test.ts +88 -0
  275. package/src/proxmox/ceph-pool-form.ts +3 -3
  276. package/src/proxmox/ceph-pool.ts +10 -3
  277. package/src/proxmox/fake-bao-env.ts +28 -0
  278. package/src/proxmox/fake-pve-lxc.ts +210 -0
  279. package/src/proxmox/fake-pve.ts +75 -0
  280. package/src/proxmox/index.test.ts +6 -6
  281. package/src/proxmox/index.ts +6 -2
  282. package/src/proxmox/lxc-adopt.test.ts +170 -0
  283. package/src/proxmox/lxc-create-form.ts +76 -0
  284. package/src/proxmox/lxc-create.test.ts +126 -0
  285. package/src/proxmox/lxc-guards.test.ts +158 -0
  286. package/src/proxmox/lxc-harness.ts +150 -0
  287. package/src/proxmox/lxc-identity.ts +43 -0
  288. package/src/proxmox/lxc-judge.test.ts +175 -0
  289. package/src/proxmox/lxc-judge.ts +196 -0
  290. package/src/proxmox/lxc-lifecycle.ts +200 -0
  291. package/src/proxmox/lxc-ownership.test.ts +159 -0
  292. package/src/proxmox/lxc-props.ts +215 -0
  293. package/src/proxmox/lxc-read.test.ts +60 -0
  294. package/src/proxmox/lxc-volume.test.ts +96 -0
  295. package/src/proxmox/lxc-volume.ts +153 -0
  296. package/src/proxmox/lxc-wire.ts +210 -0
  297. package/src/proxmox/lxc.ts +216 -93
  298. package/src/proxmox/network-apply-read.ts +3 -1
  299. package/src/proxmox/pbs-datastore.ts +4 -3
  300. package/src/proxmox/resource.ts +7 -7
  301. package/src/proxmox/update-guard.ts +31 -0
  302. package/src/verify/args.ts +82 -0
  303. package/src/verify/cli.test.ts +86 -0
  304. package/src/verify/cli.ts +49 -0
  305. package/src/verify/fake-engine.ts +158 -0
  306. package/src/verify/index.ts +36 -0
  307. package/src/verify/report.ts +52 -0
  308. package/src/verify/rows.ts +169 -0
  309. package/src/verify/spy.test.ts +141 -0
  310. package/src/verify/spy.ts +149 -0
  311. package/src/verify/verify.test.ts +124 -0
  312. package/src/verify/verify.ts +172 -0
@@ -292,9 +292,10 @@ var loadCaddyfile = async (admin, caddyfile, sourceFile) => {
292
292
  };
293
293
  // src/caddy/config.ts
294
294
  import { Resource } from "alchemy";
295
+ import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
295
296
  import { isResolved } from "alchemy/Diff";
296
297
  import * as Provider from "alchemy/Provider";
297
- import * as Effect2 from "effect/Effect";
298
+ import * as Effect6 from "effect/Effect";
298
299
 
299
300
  // src/launchd/host-effect.ts
300
301
  import * as Effect from "effect/Effect";
@@ -309,6 +310,183 @@ var resolvedString = (news, key) => {
309
310
  return typeof value === "string" ? value : undefined;
310
311
  };
311
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;
458
+ if (scoped !== undefined)
459
+ return scoped;
460
+ const policy = yield* Effect5.serviceOption(AdoptPolicy);
461
+ if (Option4.isSome(policy))
462
+ return policy.value;
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}`));
487
+ });
488
+ var claimFor = (owner) => (what) => refuseTakeover(owner, what);
489
+
312
490
  // src/caddy/admin-guard.ts
313
491
  var DEFAULT_ADMIN_PORT = 2019;
314
492
  var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|\[?::1\]?)$/;
@@ -544,20 +722,48 @@ var desiredConfig = async (admin, props) => {
544
722
  throw refuse(admin, guard.join("; "));
545
723
  return { digest: configDigest(adapted.config), warnings: adapted.warnings };
546
724
  };
547
- var readLive = async (admin, sourceFile) => ({
548
- configSha256: configDigest(await readRunningConfig(admin)),
725
+ var attributesOf = (admin, running, sourceFile) => ({
726
+ configSha256: configDigest(running),
549
727
  endpoint: admin.endpoint,
550
728
  ...sourceFile === undefined ? {} : { sourceFile }
551
729
  });
730
+ var readLive = async (admin, sourceFile) => attributesOf(admin, await readRunningConfig(admin), sourceFile);
731
+ var claimable = (running, declaredDigest) => servesNothing(running) || configDigest(running) === declaredDigest;
732
+ var probeLive = async (admin, caddyfile, sourceFile) => {
733
+ const running = await readRunningConfig(admin);
734
+ if (servesNothing(running))
735
+ return;
736
+ const attributes = attributesOf(admin, running, sourceFile);
737
+ if (caddyfile === undefined) {
738
+ return { attributes, ours: false, unchecked: "the stored Caddyfile is not plain text" };
739
+ }
740
+ try {
741
+ const want = await desiredConfig(admin, { caddyfile });
742
+ return { attributes, ours: claimable(running, want.digest) };
743
+ } catch (cause) {
744
+ if (cause instanceof CaddyUnreachableError)
745
+ throw cause;
746
+ return {
747
+ attributes,
748
+ ours: false,
749
+ unchecked: cause instanceof Error ? cause.message : String(cause)
750
+ };
751
+ }
752
+ };
552
753
  var diffConfig = async (admin, news, output) => {
553
754
  const want = await desiredConfig(admin, news);
554
755
  const live = await readLive(admin);
555
756
  const converged = want.digest === live.configSha256 && live.configSha256 === output.configSha256 && news.sourceFile === output.sourceFile && admin.endpoint === output.endpoint;
556
757
  return converged ? { action: "noop" } : { action: "update" };
557
758
  };
558
- var reconcileConfig = async (admin, props) => {
759
+ var reconcileConfig = async (admin, props, authority) => {
559
760
  const want = await desiredConfig(admin, props);
560
- const before = configDigest(await readRunningConfig(admin));
761
+ const running = await readRunningConfig(admin);
762
+ const before = configDigest(running);
763
+ const known = before === authority.stored || claimable(running, want.digest);
764
+ if (!authority.takeOver && !known) {
765
+ throw refuse(admin, `Caddy runs config ${short(before)}, which this stack did not load, and the Caddyfile adapts ` + `to ${short(want.digest)} — loading it would replace every site that config serves. ` + "Deploy with --adopt (or wrap the resource in adopt(true)) to take this Caddy over, or " + "point caddyProviders() at the Caddy you meant.");
766
+ }
561
767
  let warnings = want.warnings;
562
768
  const loaded = before !== want.digest;
563
769
  if (loaded) {
@@ -585,35 +791,46 @@ var CaddyConfig = Resource("Caddy.Config", {
585
791
  defaultRemovalPolicy: "retain"
586
792
  });
587
793
  var unreachable = (error) => error instanceof CaddyUnreachableError;
588
- 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);
589
- 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* () {
590
796
  const admin = yield* CaddyAdminService;
797
+ const probe = (caddyfile, sourceFile) => Effect6.flatMap(lift(() => probeLive(admin, caddyfile, sourceFile)), (found) => {
798
+ if (found === undefined)
799
+ return Effect6.succeed(undefined);
800
+ if (found.ours)
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);
804
+ });
591
805
  return CaddyConfig.Provider.of({
592
- list: () => Effect2.succeed([]),
593
- read: ({ olds }) => 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))),
594
808
  diff: ({ news, output }) => {
595
809
  if (output === undefined)
596
- return Effect2.succeed(undefined);
810
+ return Effect6.succeed(undefined);
597
811
  const update = planWithoutCaddy(admin, { action: "update" });
598
812
  if (isResolved(news)) {
599
- return lift(() => diffConfig(admin, news, output)).pipe(Effect2.catchIf(unreachable, update));
813
+ return lift(() => diffConfig(admin, news, output)).pipe(Effect6.catchIf(unreachable, update));
600
814
  }
601
815
  const caddyfile = resolvedString(news, "caddyfile");
602
816
  if (caddyfile === undefined)
603
- return Effect2.succeed(undefined);
817
+ return Effect6.succeed(undefined);
604
818
  return lift(async () => {
605
819
  await diffConfig(admin, { caddyfile }, output);
606
820
  return { action: "update" };
607
- }).pipe(Effect2.catchIf(unreachable, update));
821
+ }).pipe(Effect6.catchIf(unreachable, update));
608
822
  },
609
- reconcile: ({ news }) => Effect2.gen(function* () {
610
- const applied = yield* lift(() => reconcileConfig(admin, news));
823
+ reconcile: ({ fqn, news, output }) => Effect6.gen(function* () {
824
+ const sameCaddy = output !== undefined && output.endpoint === admin.endpoint;
825
+ const takeOver = sameCaddy || (yield* adoptEnabled(fqn));
826
+ const stored = output === undefined ? {} : { stored: output.configSha256 };
827
+ const applied = yield* lift(() => reconcileConfig(admin, news, { takeOver, ...stored }));
611
828
  for (const warning of applied.warnings) {
612
- yield* Effect2.logWarning(`Caddy.Config at ${admin.endpoint}: ${warning}`);
829
+ yield* Effect6.logWarning(`Caddy.Config at ${admin.endpoint}: ${warning}`);
613
830
  }
614
831
  return applied.attributes;
615
832
  }),
616
- delete: () => Effect2.void
833
+ delete: () => Effect6.void
617
834
  });
618
835
  }));
619
836
  // src/caddy/local-admin.ts
@@ -711,15 +928,15 @@ var localCaddyAdmin = (options = {}) => {
711
928
  import * as Layer2 from "effect/Layer";
712
929
  var caddyProviders = (admin = localCaddyAdmin()) => CaddyConfigProvider().pipe(Layer2.provide(caddyAdminLayer(admin)));
713
930
  // src/caddy/with-file.ts
714
- import * as Effect4 from "effect/Effect";
931
+ import * as Effect8 from "effect/Effect";
715
932
  import * as RemovalPolicy from "alchemy/RemovalPolicy";
716
933
 
717
934
  // src/launchd/host-file.ts
718
935
  import { Resource as Resource2 } from "alchemy";
719
- import { Unowned } from "alchemy/AdoptPolicy";
936
+ import { Unowned as Unowned3 } from "alchemy/AdoptPolicy";
720
937
  import { isResolved as isResolved2 } from "alchemy/Diff";
721
938
  import * as Provider2 from "alchemy/Provider";
722
- import * as Effect3 from "effect/Effect";
939
+ import * as Effect7 from "effect/Effect";
723
940
 
724
941
  // src/launchd/host-file-form.ts
725
942
  var DEFAULT_MODE = 420;
@@ -731,9 +948,8 @@ var identityProblems = (name, value) => {
731
948
  }
732
949
  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`];
733
950
  };
734
- var fileProblems = (props) => {
951
+ var pathProblems = (path) => {
735
952
  const found = [];
736
- const { path } = props;
737
953
  if (!path.startsWith("/"))
738
954
  found.push("path must be absolute");
739
955
  if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
@@ -741,6 +957,10 @@ var fileProblems = (props) => {
741
957
  }
742
958
  if (path.includes("\x00"))
743
959
  found.push("path contains NUL");
960
+ return found;
961
+ };
962
+ var fileProblems = (props) => {
963
+ const found = pathProblems(props.path);
744
964
  const mode = props.mode ?? DEFAULT_MODE;
745
965
  if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
746
966
  found.push("mode must be 0–0o7777");
@@ -801,20 +1021,29 @@ var readFileAttributes = async (runner, path) => {
801
1021
  uid: stat.uid
802
1022
  };
803
1023
  };
1024
+ var writeOptionsOf = (want) => ({
1025
+ mode: want.mode,
1026
+ ...want.uid === undefined ? {} : { uid: want.uid },
1027
+ ...want.gid === undefined ? {} : { gid: want.gid }
1028
+ });
804
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);
805
1030
  var diffFile = async (runner, news, output) => {
806
- if (news.path !== output.path)
807
- return { action: "replace" };
808
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" });
809
1038
  if (want.sha256 !== output.sha256)
810
- return { action: "update" };
1039
+ return writes({ action: "update" });
811
1040
  const live = await readFileAttributes(runner, news.path);
812
- return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
1041
+ return live !== undefined && matches(live, want) ? { action: "noop" } : writes({ action: "update" });
813
1042
  };
814
- var reconcileFile = async (runner, props, output) => {
1043
+ var reconcileFile = async (runner, props, output, adopt = false) => {
815
1044
  const want = await desiredFile(runner, props);
816
1045
  if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
817
- 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 (privileged: true). This provider never calls sudo.");
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.");
818
1047
  }
819
1048
  const stat = await runner.stat(props.path);
820
1049
  if (stat !== undefined && stat.kind !== "file") {
@@ -823,15 +1052,12 @@ var reconcileFile = async (runner, props, output) => {
823
1052
  const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
824
1053
  const moved = output !== undefined && output.path !== props.path;
825
1054
  const prior = moved ? undefined : output;
826
- if (prior === undefined && before !== undefined && !matches(before, want)) {
1055
+ const takeOver = adopt && output === undefined;
1056
+ if (prior === undefined && before !== undefined && !matches(before, want) && !takeOver) {
827
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.");
828
1058
  }
829
1059
  if (before === undefined || !matches(before, want)) {
830
- await runner.writeFileAtomic(props.path, want.bytes, {
831
- mode: want.mode,
832
- ...want.uid === undefined ? {} : { uid: want.uid },
833
- ...want.gid === undefined ? {} : { gid: want.gid }
834
- });
1060
+ await runner.writeFileAtomic(props.path, want.bytes, writeOptionsOf(want));
835
1061
  }
836
1062
  const after = await readFileAttributes(runner, props.path);
837
1063
  if (after === undefined || !matches(after, want)) {
@@ -856,34 +1082,34 @@ var deleteFile = async (runner, output) => {
856
1082
 
857
1083
  // src/launchd/host-file.ts
858
1084
  var HostFile = Resource2("Host.File");
859
- var HostFileProvider = () => Provider2.effect(HostFile, Effect3.gen(function* () {
1085
+ var HostFileProvider = () => Provider2.effect(HostFile, Effect7.gen(function* () {
860
1086
  const runner = yield* HostRunnerService;
861
1087
  return HostFile.Provider.of({
862
- list: () => Effect3.succeed([]),
1088
+ list: () => Effect7.succeed([]),
863
1089
  read: ({ olds, output }) => lift(async () => {
864
1090
  const found = await readFileAttributes(runner, olds.path);
865
1091
  if (found === undefined)
866
1092
  return;
867
- return output === undefined ? Unowned(found) : found;
1093
+ return output === undefined ? Unowned3(found) : found;
868
1094
  }),
869
- diff: ({ news, output }) => {
1095
+ diff: ({ instanceId, news, output }) => {
870
1096
  if (output === undefined)
871
- return Effect3.succeed(undefined);
1097
+ return noteUnfinished(instanceId);
872
1098
  if (isResolved2(news))
873
1099
  return lift(() => diffFile(runner, news, output));
874
1100
  const path = resolvedString(news, "path");
875
- return Effect3.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
1101
+ return Effect7.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
876
1102
  },
877
- 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))),
878
1104
  delete: ({ output }) => lift(() => deleteFile(runner, output))
879
1105
  });
880
1106
  }));
881
1107
 
882
1108
  // src/caddy/with-file.ts
883
- var caddyWithFile = (id, props) => Effect4.gen(function* () {
1109
+ var caddyWithFile = (id, props) => Effect8.gen(function* () {
884
1110
  const found = configProblems({ caddyfile: props.caddyfile, sourceFile: props.path });
885
1111
  if (found.length > 0) {
886
- return yield* Effect4.fail(new Error(`caddyWithFile ${id}: ${found.join("; ")}`));
1112
+ return yield* Effect8.fail(new Error(`caddyWithFile ${id}: ${found.join("; ")}`));
887
1113
  }
888
1114
  const file = yield* HostFile(`${id}-file`, {
889
1115
  content: props.caddyfile,
@@ -908,5 +1134,5 @@ export {
908
1134
  localCaddyAdmin
909
1135
  };
910
1136
 
911
- //# debugId=205F5FF536DDDFA164756E2164756E21
1137
+ //# debugId=13AB2C6EDF123C4E64756E2164756E21
912
1138
  //# sourceMappingURL=index.js.map