@homeflare/alchemy 0.5.0 → 0.7.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 (288) hide show
  1. package/README.md +68 -55
  2. package/dist/caddy/admin-calls.d.ts +48 -0
  3. package/dist/caddy/admin-calls.d.ts.map +1 -0
  4. package/dist/caddy/admin-guard.d.ts +40 -0
  5. package/dist/caddy/admin-guard.d.ts.map +1 -0
  6. package/dist/caddy/admin.d.ts +73 -0
  7. package/dist/caddy/admin.d.ts.map +1 -0
  8. package/dist/caddy/caddyfile-tripwire.d.ts +5 -0
  9. package/dist/caddy/caddyfile-tripwire.d.ts.map +1 -0
  10. package/dist/caddy/config-form.d.ts +27 -0
  11. package/dist/caddy/config-form.d.ts.map +1 -0
  12. package/dist/caddy/config-lifecycle.d.ts +45 -0
  13. package/dist/caddy/config-lifecycle.d.ts.map +1 -0
  14. package/dist/caddy/config.d.ts +30 -0
  15. package/dist/caddy/config.d.ts.map +1 -0
  16. package/dist/caddy/digest.d.ts +7 -0
  17. package/dist/caddy/digest.d.ts.map +1 -0
  18. package/dist/caddy/fake-caddy.d.ts +43 -0
  19. package/dist/caddy/fake-caddy.d.ts.map +1 -0
  20. package/dist/caddy/index.d.ts +21 -0
  21. package/dist/caddy/index.d.ts.map +1 -0
  22. package/dist/caddy/index.js +912 -0
  23. package/dist/caddy/index.js.map +28 -0
  24. package/dist/caddy/local-admin.d.ts +45 -0
  25. package/dist/caddy/local-admin.d.ts.map +1 -0
  26. package/dist/caddy/providers.d.ts +14 -0
  27. package/dist/caddy/providers.d.ts.map +1 -0
  28. package/dist/caddy/with-file.d.ts +47 -0
  29. package/dist/caddy/with-file.d.ts.map +1 -0
  30. package/dist/cloudflare/fake-mesh.d.ts +58 -0
  31. package/dist/cloudflare/fake-mesh.d.ts.map +1 -0
  32. package/dist/cloudflare/index.d.ts +9 -3
  33. package/dist/cloudflare/index.d.ts.map +1 -1
  34. package/dist/cloudflare/index.js +218 -7
  35. package/dist/cloudflare/index.js.map +9 -4
  36. package/dist/cloudflare/mesh-node-api.d.ts +75 -0
  37. package/dist/cloudflare/mesh-node-api.d.ts.map +1 -0
  38. package/dist/cloudflare/mesh-node-form.d.ts +82 -0
  39. package/dist/cloudflare/mesh-node-form.d.ts.map +1 -0
  40. package/dist/cloudflare/mesh-node-harness.d.ts +46 -0
  41. package/dist/cloudflare/mesh-node-harness.d.ts.map +1 -0
  42. package/dist/cloudflare/mesh-node-lifecycle.d.ts +22 -0
  43. package/dist/cloudflare/mesh-node-lifecycle.d.ts.map +1 -0
  44. package/dist/cloudflare/mesh-node-token.d.ts +23 -0
  45. package/dist/cloudflare/mesh-node-token.d.ts.map +1 -0
  46. package/dist/cloudflare/mesh-node.d.ts +48 -0
  47. package/dist/cloudflare/mesh-node.d.ts.map +1 -0
  48. package/dist/cloudflare/providers.d.ts +1 -12
  49. package/dist/cloudflare/providers.d.ts.map +1 -1
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/launchd/fake-runner.d.ts +36 -0
  53. package/dist/launchd/fake-runner.d.ts.map +1 -0
  54. package/dist/launchd/host-effect.d.ts +19 -0
  55. package/dist/launchd/host-effect.d.ts.map +1 -0
  56. package/dist/launchd/host-file-form.d.ts +24 -0
  57. package/dist/launchd/host-file-form.d.ts.map +1 -0
  58. package/dist/launchd/host-file-lifecycle.d.ts +29 -0
  59. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
  60. package/dist/launchd/host-file.d.ts +25 -0
  61. package/dist/launchd/host-file.d.ts.map +1 -0
  62. package/dist/launchd/host-lookup.d.ts +27 -0
  63. package/dist/launchd/host-lookup.d.ts.map +1 -0
  64. package/dist/launchd/index.d.ts +21 -0
  65. package/dist/launchd/index.d.ts.map +1 -0
  66. package/dist/launchd/index.js +969 -0
  67. package/dist/launchd/index.js.map +25 -0
  68. package/dist/launchd/job-form.d.ts +90 -0
  69. package/dist/launchd/job-form.d.ts.map +1 -0
  70. package/dist/launchd/job-lifecycle.d.ts +34 -0
  71. package/dist/launchd/job-lifecycle.d.ts.map +1 -0
  72. package/dist/launchd/job-preflight.d.ts +50 -0
  73. package/dist/launchd/job-preflight.d.ts.map +1 -0
  74. package/dist/launchd/job-validate.d.ts +25 -0
  75. package/dist/launchd/job-validate.d.ts.map +1 -0
  76. package/dist/launchd/job.d.ts +34 -0
  77. package/dist/launchd/job.d.ts.map +1 -0
  78. package/dist/launchd/launchctl.d.ts +62 -0
  79. package/dist/launchd/launchctl.d.ts.map +1 -0
  80. package/dist/launchd/local-runner.d.ts +7 -0
  81. package/dist/launchd/local-runner.d.ts.map +1 -0
  82. package/dist/launchd/plist.d.ts +41 -0
  83. package/dist/launchd/plist.d.ts.map +1 -0
  84. package/dist/launchd/providers.d.ts +15 -0
  85. package/dist/launchd/providers.d.ts.map +1 -0
  86. package/dist/launchd/runner.d.ts +91 -0
  87. package/dist/launchd/runner.d.ts.map +1 -0
  88. package/dist/launchd/secret-tripwire.d.ts +24 -0
  89. package/dist/launchd/secret-tripwire.d.ts.map +1 -0
  90. package/dist/openbao/auth-method-form.d.ts +7 -0
  91. package/dist/openbao/auth-method-form.d.ts.map +1 -1
  92. package/dist/openbao/auth-method-reconcile.d.ts +15 -0
  93. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
  94. package/dist/openbao/auth-method.d.ts +6 -0
  95. package/dist/openbao/auth-method.d.ts.map +1 -1
  96. package/dist/openbao/auth-role-form.d.ts +9 -0
  97. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  98. package/dist/openbao/auth-role.d.ts.map +1 -1
  99. package/dist/openbao/auth-token-form.d.ts +48 -0
  100. package/dist/openbao/auth-token-form.d.ts.map +1 -0
  101. package/dist/openbao/bao-identity.d.ts +81 -0
  102. package/dist/openbao/bao-identity.d.ts.map +1 -0
  103. package/dist/openbao/bao-namespace.d.ts +20 -0
  104. package/dist/openbao/bao-namespace.d.ts.map +1 -0
  105. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  106. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
  107. package/dist/openbao/fake-engines.d.ts +30 -0
  108. package/dist/openbao/fake-engines.d.ts.map +1 -0
  109. package/dist/openbao/fake-stack.d.ts +29 -0
  110. package/dist/openbao/fake-stack.d.ts.map +1 -0
  111. package/dist/openbao/host-approles.d.ts +62 -0
  112. package/dist/openbao/host-approles.d.ts.map +1 -0
  113. package/dist/openbao/index.d.ts +19 -0
  114. package/dist/openbao/index.d.ts.map +1 -1
  115. package/dist/openbao/index.js +1518 -310
  116. package/dist/openbao/index.js.map +40 -17
  117. package/dist/openbao/jwt-config-form.d.ts +51 -0
  118. package/dist/openbao/jwt-config-form.d.ts.map +1 -0
  119. package/dist/openbao/jwt-config.d.ts +25 -0
  120. package/dist/openbao/jwt-config.d.ts.map +1 -0
  121. package/dist/openbao/jwt-role-form.d.ts +91 -0
  122. package/dist/openbao/jwt-role-form.d.ts.map +1 -0
  123. package/dist/openbao/jwt-role.d.ts +31 -0
  124. package/dist/openbao/jwt-role.d.ts.map +1 -0
  125. package/dist/openbao/kubernetes-role-form.d.ts +68 -0
  126. package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
  127. package/dist/openbao/kubernetes-role.d.ts +25 -0
  128. package/dist/openbao/kubernetes-role.d.ts.map +1 -0
  129. package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
  130. package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
  131. package/dist/openbao/mfa-enforcement.d.ts +41 -0
  132. package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
  133. package/dist/openbao/mfa-totp-form.d.ts +52 -0
  134. package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
  135. package/dist/openbao/mfa-totp.d.ts +33 -0
  136. package/dist/openbao/mfa-totp.d.ts.map +1 -0
  137. package/dist/openbao/mfa-wire.d.ts +63 -0
  138. package/dist/openbao/mfa-wire.d.ts.map +1 -0
  139. package/dist/openbao/mount-form.d.ts +7 -0
  140. package/dist/openbao/mount-form.d.ts.map +1 -1
  141. package/dist/openbao/mount-move.d.ts +49 -0
  142. package/dist/openbao/mount-move.d.ts.map +1 -0
  143. package/dist/openbao/mount-path.d.ts +8 -4
  144. package/dist/openbao/mount-path.d.ts.map +1 -1
  145. package/dist/openbao/mount-reconcile.d.ts +16 -0
  146. package/dist/openbao/mount-reconcile.d.ts.map +1 -0
  147. package/dist/openbao/mount.d.ts +9 -0
  148. package/dist/openbao/mount.d.ts.map +1 -1
  149. package/dist/openbao/pki-role-canonical.d.ts +46 -0
  150. package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
  151. package/dist/openbao/pki-role-form.d.ts +38 -30
  152. package/dist/openbao/pki-role-form.d.ts.map +1 -1
  153. package/dist/openbao/pki-role.d.ts.map +1 -1
  154. package/dist/openbao/policy.d.ts +2 -0
  155. package/dist/openbao/policy.d.ts.map +1 -1
  156. package/dist/openbao/proxmox-role-form.d.ts +18 -8
  157. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  158. package/dist/openbao/proxmox-role-wire.d.ts +18 -0
  159. package/dist/openbao/proxmox-role-wire.d.ts.map +1 -0
  160. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  161. package/dist/openbao/remount-wire.d.ts +44 -0
  162. package/dist/openbao/remount-wire.d.ts.map +1 -0
  163. package/dist/openbao/rename.d.ts +106 -0
  164. package/dist/openbao/rename.d.ts.map +1 -0
  165. package/dist/openbao/role-reconcile.d.ts +40 -0
  166. package/dist/openbao/role-reconcile.d.ts.map +1 -0
  167. package/docs/caddy.md +180 -0
  168. package/docs/launchd.md +167 -0
  169. package/docs/mesh-node.md +188 -0
  170. package/package.json +12 -2
  171. package/src/caddy/admin-calls.ts +128 -0
  172. package/src/caddy/admin-guard.ts +170 -0
  173. package/src/caddy/admin.ts +74 -0
  174. package/src/caddy/caddyfile-tripwire.ts +133 -0
  175. package/src/caddy/config-form.ts +64 -0
  176. package/src/caddy/config-lifecycle.test.ts +218 -0
  177. package/src/caddy/config-lifecycle.ts +148 -0
  178. package/src/caddy/config.ts +116 -0
  179. package/src/caddy/digest.ts +44 -0
  180. package/src/caddy/fake-caddy.ts +160 -0
  181. package/src/caddy/guards.test.ts +156 -0
  182. package/src/caddy/index.ts +25 -0
  183. package/src/caddy/local-admin.test.ts +160 -0
  184. package/src/caddy/local-admin.ts +189 -0
  185. package/src/caddy/providers.test.ts +244 -0
  186. package/src/caddy/providers.ts +17 -0
  187. package/src/caddy/with-file.ts +62 -0
  188. package/src/cloudflare/fake-mesh.ts +181 -0
  189. package/src/cloudflare/index.ts +9 -3
  190. package/src/cloudflare/mesh-node-api.ts +127 -0
  191. package/src/cloudflare/mesh-node-edges.test.ts +103 -0
  192. package/src/cloudflare/mesh-node-form.test.ts +80 -0
  193. package/src/cloudflare/mesh-node-form.ts +115 -0
  194. package/src/cloudflare/mesh-node-harness.ts +124 -0
  195. package/src/cloudflare/mesh-node-lifecycle.ts +187 -0
  196. package/src/cloudflare/mesh-node-policy.test.ts +151 -0
  197. package/src/cloudflare/mesh-node-token.test.ts +117 -0
  198. package/src/cloudflare/mesh-node-token.ts +125 -0
  199. package/src/cloudflare/mesh-node.test.ts +172 -0
  200. package/src/cloudflare/mesh-node.ts +120 -0
  201. package/src/cloudflare/providers.ts +20 -2
  202. package/src/index.ts +2 -0
  203. package/src/launchd/fake-runner.ts +176 -0
  204. package/src/launchd/host-effect.ts +28 -0
  205. package/src/launchd/host-file-form.ts +71 -0
  206. package/src/launchd/host-file-lifecycle.test.ts +198 -0
  207. package/src/launchd/host-file-lifecycle.ts +164 -0
  208. package/src/launchd/host-file.ts +64 -0
  209. package/src/launchd/host-lookup.test.ts +92 -0
  210. package/src/launchd/host-lookup.ts +94 -0
  211. package/src/launchd/index.ts +26 -0
  212. package/src/launchd/job-form.test.ts +171 -0
  213. package/src/launchd/job-form.ts +180 -0
  214. package/src/launchd/job-identity.test.ts +147 -0
  215. package/src/launchd/job-lifecycle.test.ts +220 -0
  216. package/src/launchd/job-lifecycle.ts +194 -0
  217. package/src/launchd/job-preflight.ts +152 -0
  218. package/src/launchd/job-validate.ts +134 -0
  219. package/src/launchd/job.ts +96 -0
  220. package/src/launchd/launchctl.test.ts +181 -0
  221. package/src/launchd/launchctl.ts +188 -0
  222. package/src/launchd/local-runner.test.ts +125 -0
  223. package/src/launchd/local-runner.ts +143 -0
  224. package/src/launchd/plist.test.ts +186 -0
  225. package/src/launchd/plist.ts +156 -0
  226. package/src/launchd/providers.test.ts +222 -0
  227. package/src/launchd/providers.ts +21 -0
  228. package/src/launchd/runner.ts +95 -0
  229. package/src/launchd/secret-tripwire.test.ts +110 -0
  230. package/src/launchd/secret-tripwire.ts +81 -0
  231. package/src/openbao/README.md +181 -0
  232. package/src/openbao/REPLACE.md +163 -0
  233. package/src/openbao/auth-method-form.ts +7 -0
  234. package/src/openbao/auth-method-reconcile.ts +74 -0
  235. package/src/openbao/auth-method.ts +20 -41
  236. package/src/openbao/auth-role-form.test.ts +16 -1
  237. package/src/openbao/auth-role-form.ts +11 -0
  238. package/src/openbao/auth-role.ts +12 -0
  239. package/src/openbao/auth-token-form.ts +161 -0
  240. package/src/openbao/bao-identity.test.ts +131 -0
  241. package/src/openbao/bao-identity.ts +188 -0
  242. package/src/openbao/bao-namespace.ts +32 -0
  243. package/src/openbao/cloudflare-role-wire.test.ts +8 -34
  244. package/src/openbao/cloudflare-role.ts +30 -8
  245. package/src/openbao/cloudflare-roles-expand.ts +2 -5
  246. package/src/openbao/fake-engines.ts +119 -0
  247. package/src/openbao/fake-stack.ts +112 -0
  248. package/src/openbao/host-approles.test.ts +121 -0
  249. package/src/openbao/host-approles.ts +126 -0
  250. package/src/openbao/index.ts +19 -0
  251. package/src/openbao/jwt-config-form.ts +161 -0
  252. package/src/openbao/jwt-config.ts +85 -0
  253. package/src/openbao/jwt-role-form.test.ts +130 -0
  254. package/src/openbao/jwt-role-form.ts +231 -0
  255. package/src/openbao/jwt-role.ts +102 -0
  256. package/src/openbao/kubernetes-role-form.test.ts +82 -0
  257. package/src/openbao/kubernetes-role-form.ts +149 -0
  258. package/src/openbao/kubernetes-role.ts +94 -0
  259. package/src/openbao/mfa-enforcement-form.ts +147 -0
  260. package/src/openbao/mfa-enforcement.ts +143 -0
  261. package/src/openbao/mfa-totp-form.ts +155 -0
  262. package/src/openbao/mfa-totp.ts +118 -0
  263. package/src/openbao/mfa-wire.ts +117 -0
  264. package/src/openbao/mfa.test.ts +200 -0
  265. package/src/openbao/mount-form.ts +9 -1
  266. package/src/openbao/mount-move.test.ts +164 -0
  267. package/src/openbao/mount-move.ts +113 -0
  268. package/src/openbao/mount-path.test.ts +11 -1
  269. package/src/openbao/mount-path.ts +17 -11
  270. package/src/openbao/mount-reconcile.ts +75 -0
  271. package/src/openbao/mount.ts +26 -43
  272. package/src/openbao/pki-role-canonical.ts +163 -0
  273. package/src/openbao/pki-role-form.test.ts +112 -0
  274. package/src/openbao/pki-role-form.ts +76 -110
  275. package/src/openbao/pki-role.ts +12 -27
  276. package/src/openbao/policy-rename.test.ts +112 -0
  277. package/src/openbao/policy-wire.test.ts +7 -26
  278. package/src/openbao/policy.ts +27 -4
  279. package/src/openbao/proxmox-role-form.ts +27 -8
  280. package/src/openbao/proxmox-role-wire.ts +24 -0
  281. package/src/openbao/proxmox-role.ts +62 -37
  282. package/src/openbao/remount-wire.test.ts +87 -0
  283. package/src/openbao/remount-wire.ts +112 -0
  284. package/src/openbao/rename-occupied.test.ts +166 -0
  285. package/src/openbao/rename.ts +162 -0
  286. package/src/openbao/role-reconcile.test.ts +104 -0
  287. package/src/openbao/role-reconcile.ts +84 -0
  288. package/src/openbao/role-rename.test.ts +205 -0
package/README.md CHANGED
@@ -4,7 +4,8 @@ Custom [Alchemy](https://alchemy.run) providers for gaps the vendor SDK leaves.
4
4
 
5
5
  ```sh
6
6
  bun add @homeflare/alchemy alchemy@2.0.0-beta.79 effect@4.0.0-rc.115 \
7
- @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0
7
+ @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0 \
8
+ @distilled.cloud/cloudflare@1.0.0-rc.12
8
9
  ```
9
10
 
10
11
  ⛔ **Every one of those is required, and you also need an `overrides` block** — see
@@ -23,8 +24,7 @@ An R2 bucket's lock rules, declared rather than applied by hand. A lock rule is
23
24
  credential can delete it.
24
25
 
25
26
  ```ts
26
- import { R2BucketLock } from '@homeflare/alchemy';
27
- import { providers } from '@homeflare/alchemy/cloudflare';
27
+ import { R2BucketLock } from '@homeflare/alchemy/cloudflare';
28
28
 
29
29
  export class BackupLock extends R2BucketLock('backup-lock', {
30
30
  bucketName: 'my-backups',
@@ -40,6 +40,41 @@ import { providers } from '@homeflare/alchemy/cloudflare';
40
40
  // …then provide `providers()` alongside Cloudflare.providers()
41
41
  ```
42
42
 
43
+ ⚠️ **The rule set is REPLACE, not merge.** The API `PUT`s the whole set, so a rule omitted
44
+ from `rules` is a rule deleted. That is the same shape as the Cloudflare API itself.
45
+
46
+ ⛔ **It retains on destroy.** Removing a lock is removing a retention floor, which is the one
47
+ operation this resource exists to make hard: dropping the declaration leaves the lock in place,
48
+ and only an explicit `RemovalPolicy.destroy()` reaches the unlocking `delete`.
49
+
50
+ ## MeshNode
51
+
52
+ A Cloudflare Mesh node (a `warp_connector`), declared **without its token in state**. Alchemy's
53
+ `Cloudflare.Tunnel.WarpConnector` stores the node token in plaintext state on every read and has
54
+ no `ha`; this one never reads the token, and `ha` is a create-only prop.
55
+
56
+ ```ts
57
+ import { MeshNode, providers } from '@homeflare/alchemy/cloudflare';
58
+
59
+ const door = yield * MeshNode('vault-door', { name: 'door-a', ha: false });
60
+ // door.id → the node id for the Gateway rules and for fetchMeshNodeToken
61
+ ```
62
+
63
+ - **`name`** renames in place (`PATCH`); **`ha`** is required and replaces the node (delete-first
64
+ while the name stays). Existing nodes are adopted by exact name, `Unowned` until `adopt(true)`.
65
+ - ⛔ **It retains on destroy** (deleting a node cuts every replica off the Mesh), so a same-name
66
+ `ha` change refuses, writing nothing, until that deploy pipes `RemovalPolicy.destroy()`.
67
+ - ★ **A door (a node with no routes) is `ha: false`.** HA fails over routes, and each replica has
68
+ its own Mesh IP; a second door is a second `MeshNode`.
69
+ - The account and credentials come from Alchemy's own Cloudflare environment, the same as
70
+ `Cloudflare.providers()`.
71
+ - **`fetchMeshNodeToken({ accountId, id })`** returns the token `Redacted`, on demand, for a
72
+ one-off enrolment step. ⛔ Write it to a root-owned `0600` file on the node and nowhere else.
73
+ It refuses the Global API Key, an empty API token, and a set `DISTILLED_DEBUG_HTTP` (distilled
74
+ would print the token).
75
+
76
+ Guide, the enrolment step and every replace case: [docs/mesh-node.md](./docs/mesh-node.md).
77
+
43
78
  ## Website.Astro / Website.Vite
44
79
 
45
80
  House flags on Alchemy's own stacks — the two this estate actually ships.
@@ -58,72 +93,49 @@ const app = yield * viteWebsite('aimto', { rootDir: webRoot });
58
93
  ⛔ Not Nextjs. `Website.Nextjs` hashes source and plans as **create** against a live
59
94
  Worker. Adopt that shape with `Worker`, not a helper here.
60
95
 
61
- ⚠️ **The rule set is REPLACE, not merge.** The API `PUT`s the whole set, so a rule omitted
62
- from `rules` is a rule deleted. That is the same shape as the Cloudflare API itself.
63
-
64
- ⛔ **Deletion is refused by design.** Removing a lock is removing a retention floor, which
65
- is the one operation this resource exists to make hard. It retains on destroy.
66
-
67
96
  ## OpenBao — `@homeflare/alchemy/openbao`
68
97
 
69
- Vault objects as resources — `BaoMount`, `BaoAuthMethod`, `BaoPolicy`, `BaoAuthRole`,
70
- `BaoPkiRole`, `BaoSshRole`, `BaoCloudflareRole`, `BaoProxmoxRole`, `BaoPlugin` — plus an AppRole
71
- login for scripts. Every call resolves `BAO_ADDR` / `BAO_AGENT_ADDR` / `BAO_NAMESPACE` /
72
- `BAO_TOKEN` (or the `VAULT_*` twins) the way the `bao` CLI does. A stack provides each
98
+ Vault objects as resources — mounts (with `remountFrom` moves), auth methods, policies, AppRole,
99
+ JWT/OIDC and Kubernetes roles, JWT auth config, login MFA (TOTP method + enforcement), PKI, SSH,
100
+ Cloudflare and Proxmox roles, plugins — plus `assertBaoIdentity` (call it first), the pure
101
+ `hostAppRoles` generator, and an AppRole login for scripts. A stack provides each
73
102
  `Bao*Provider()` it uses, plus `FetchHttpClient.layer`.
74
103
 
75
- ⛔ **Metadata only.** Alchemy stores props and attributes unencrypted, so no secret, CA key or
76
- plugin `env` is declarable. Every family defaults to `retain` on destroy.
104
+ ⛔ **Metadata only.** Alchemy stores props and attributes unencrypted, so no secret, CA key,
105
+ plugin `env` or OIDC client secret is declarable. Every family defaults to `retain` on destroy.
77
106
 
78
- ### appRoleLogin / revokeSelf
107
+ ★ **Usage lives beside the code**, so it ships in the tarball with it:
108
+ [src/openbao/README.md](./src/openbao/README.md). What each resource does on a rename is in
109
+ [src/openbao/REPLACE.md](./src/openbao/REPLACE.md) — read it before changing a path or name.
79
110
 
80
- ```ts
81
- import { BaoLoginError, appRoleLogin, revokeSelf } from '@homeflare/alchemy/openbao';
111
+ ## launchd — `@homeflare/alchemy/launchd`
82
112
 
83
- const login = await appRoleLogin({ roleId, secretId }); // mount defaults to `approle`
84
- try {
85
- use(login.clientToken, login.accessor, login.policies, login.leaseDurationSeconds);
86
- } finally {
87
- await revokeSelf(login.clientToken);
88
- }
89
- ```
113
+ `LaunchdJob` and `HostFile` declare a Mac host's daemons and their config files. launchd has no SDK,
114
+ so the provider renders the plist and drives `launchctl`, all through one injectable `HostRunner`;
115
+ `launchdProviders()` provides both. An update restarts the job; a `label`/`domain` change replaces it
116
+ delete-first.
90
117
 
91
- - ⛔ The login never sends `BAO_TOKEN`, and a `BaoLoginError` never contains the credential.
92
- OpenBao can echo a secret_id back in an error, so every error string is redacted.
93
- - ⚠️ `clientToken` is a getter over a private field: `console.log(login)` prints `[Getter]` at
94
- most (Bun) or nothing (Node), and JSON, a spread and `structuredClone` carry no token.
95
- - ⚠️ A value with leading or trailing whitespace is refused, not trimmed. `Bun.file().text()`
96
- keeps a file's trailing newline.
97
- - `reason` is `input`, `refused`, `unreachable` or `response`. Pass `env` to use something other
98
- than `process.env`. `appRoleLoginEffect` / `revokeSelfEffect` are the same calls as Effects.
118
+ - ⛔ **No secrets in props** (state is unencrypted): declare the path a secret renderer writes.
119
+ - ⛔ **No silent sudo:** the system domain needs a deploy started as root, or a `privileged` runner.
120
+ - ⚠️ `org.nixos.*` jobs are never adopted. Guide and nix-darwin cutover: [docs/launchd.md](./docs/launchd.md).
99
121
 
100
- ### BaoPlugin
122
+ ## Caddy — `@homeflare/alchemy/caddy`
101
123
 
102
- ```ts
103
- import { BaoPlugin } from '@homeflare/alchemy/openbao';
104
- import * as Effect from 'effect/Effect';
105
-
106
- export const plugins = Effect.gen(function* () {
107
- yield* BaoPlugin('plugin-example', {
108
- name: 'openbao-plugin-secrets-example',
109
- type: 'secret', // 'secret' | 'auth' | 'database'
110
- command: 'openbao-plugin-secrets-example', // a bare file name in plugin_directory
111
- sha256: '<hex sha256 of that file>',
112
- version: 'v0.1.2', // canonical semver
113
- });
114
- });
115
- ```
124
+ `CaddyConfig` declares a running Caddy's config as Caddyfile text, applied through Caddy's own admin
125
+ API: `POST /load` (graceful reload; Caddy keeps the old config if it refuses the new one, and the
126
+ deploy says why), `GET /config/` for drift. A running Caddy is adopted on first read.
127
+ `caddyWithFile()` also writes the file Caddy starts from, with `HostFile`; `caddyProviders()` provides
128
+ the transport, `http://127.0.0.1:2019` by default.
116
129
 
117
- - ⛔ It registers a binary that is already in `plugin_directory`. It does not copy the binary.
118
- - ⚠️ **If the binary reports its own version, declare exactly that one.** OpenBao refuses any
119
- other ("plugin version mismatch"), and files an unversioned registration under the reported
120
- one, so reconcile then refuses because the read-back finds nothing.
121
- - ⚠️ A new `sha256` does not restart running mounts. Reload with `sys/plugins/reload/backend`.
122
- - The deploying token needs `sudo` on `sys/plugins/catalog/*`.
130
+ - ⛔ **No secrets in the Caddyfile** (state is unencrypted): `{env.NAME}` / `{file./path}` placeholders.
131
+ - ⛔ **The admin API stays on loopback or a unix socket**, and a Caddyfile that would move it is refused.
132
+ - ⛔ **Delete never unloads or stops Caddy.** Order of file and load, `--resume`: [docs/caddy.md](./docs/caddy.md).
123
133
 
124
134
  ## Credentials
125
135
 
126
136
  `CLOUDFLARE_API_TOKEN` is read from the environment at call time, never at module scope.
137
+ `MeshNode` instead resolves credentials and the account the way `Cloudflare.providers()` does
138
+ (an Alchemy profile, or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` in CI).
127
139
 
128
140
  ⛔ **Mint a short-lived, scoped token** — do not reuse a long-lived one, and never a Global
129
141
  API Key. An empty value fails closed with a message saying so, because an empty render is
@@ -134,7 +146,8 @@ like a bad credential.
134
146
 
135
147
  ```sh
136
148
  bun add @homeflare/alchemy alchemy@2.0.0-beta.79 effect@4.0.0-rc.115 \
137
- @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0
149
+ @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0 \
150
+ @distilled.cloud/cloudflare@1.0.0-rc.12
138
151
  ```
139
152
 
140
153
  ⚠️ Peers, not dependencies: Alchemy's resource registry and Effect's context both break if
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The three admin API calls the provider makes, each classified into a value or a readable Error
3
+ * that carries Caddy's own message.
4
+ *
5
+ * Read in caddyserver/caddy v2.11.4 (and the API docs, caddyserver.com/docs/api):
6
+ * · `POST /adapt` — adapts without running (load.go handleAdapt): 200 `{warnings?, result}`,
7
+ * or 400 `{"error": …}`. Side-effect free, so it is what plan-time validation uses.
8
+ * · `POST /load` — Content-Type `text/caddyfile` picks the adapter (load.go adaptByContentType);
9
+ * "if the new config fails for any reason, the old config is rolled back into place without
10
+ * downtime" (docs; caddy.go changeConfig restores the previous raw config). An identical
11
+ * config is a no-op without `Cache-Control: must-revalidate`.
12
+ * · `GET /config/` — the running config as JSON, `null` when empty (admin.go handleConfig).
13
+ * ⛔ A FAILED LOAD CAN ANSWER 200. load.go writes the adapter's WARNINGS to the body before it
14
+ * runs the config; if the run then fails, the error JSON is appended to a response whose status
15
+ * is already 200 (Go's first Write sends 200; the later WriteHeader(400) is ignored). The
16
+ * Caddyfile adapter warns on every unformatted input (adapter.go FormattingDifference), so this
17
+ * is the COMMON failure shape, not an edge. Hence: look for the error in the body whatever the
18
+ * status — and the lifecycle reads the config back after every load anyway.
19
+ * MEASURED 2026-09-21 on a throwaway Caddy 2.11.4 (loopback ports, isolated XDG dirs): an
20
+ * unformatted Caddyfile whose port was taken answered `200` with body
21
+ * `[{"file":"Caddyfile","line":8,"message":"Caddyfile input is not formatted; …"}]{"error":"loading
22
+ * config: … bind: address already in use"}`, and Caddy kept serving the previous config.
23
+ */
24
+ import type { CaddyAdmin } from './admin.ts';
25
+ export declare class CaddyAdminError extends Error {
26
+ readonly status: number;
27
+ readonly method: string;
28
+ readonly path: string;
29
+ constructor(status: number, method: string, path: string, reason: string);
30
+ }
31
+ export type Adapted = {
32
+ readonly config: unknown;
33
+ readonly warnings: readonly string[];
34
+ };
35
+ /** Adapt a Caddyfile to JSON on the running Caddy, without loading it. */
36
+ export declare const adaptCaddyfile: (admin: CaddyAdmin, caddyfile: string) => Promise<Adapted>;
37
+ /** The running config, parsed; `null` when Caddy runs with none. */
38
+ export declare const readRunningConfig: (admin: CaddyAdmin) => Promise<unknown>;
39
+ /**
40
+ * Apply a Caddyfile with `POST /load`; resolves with the adapter's warnings.
41
+ *
42
+ * ★ `sourceFile` RIDES AS `Caddy-Config-Source-File` (+ `-Adapter: caddyfile`), the headers
43
+ * `caddy reload` sends (cmd/commandfuncs.go). A load WITHOUT them makes Caddy forget the file it
44
+ * was started with (caddy.go ClearLastConfigIfDifferent), and SIGUSR1 then no longer reloads
45
+ * from that file. With them, and the same path, it keeps it.
46
+ */
47
+ export declare const loadCaddyfile: (admin: CaddyAdmin, caddyfile: string, sourceFile?: string) => Promise<readonly string[]>;
48
+ //# sourceMappingURL=admin-calls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-calls.d.ts","sourceRoot":"","sources":["../../src/caddy/admin-calls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAsB,MAAM,YAAY,CAAC;AAGjE,qBAAa,eAAgB,SAAQ,KAAK;IAEtC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EAIf;CACF;AAwCD,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzF,0EAA0E;AAC1E,eAAO,MAAM,cAAc,UAAiB,UAAU,aAAa,MAAM,KAAG,OAAO,CAAC,OAAO,CAY1F,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,UAAiB,UAAU,KAAG,OAAO,CAAC,OAAO,CAI1E,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,UACjB,UAAU,aACN,MAAM,eACJ,MAAM,KAClB,OAAO,CAAC,SAAS,MAAM,EAAE,CAe3B,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Refuse a config whose `admin` block would cut the provider off from Caddy, or expose the admin
3
+ * API — checked on the ADAPTED JSON (`POST /adapt`), before anything is loaded.
4
+ *
5
+ * ⛔ A Caddyfile can move its own admin endpoint. After `POST /load`, Caddy swaps the admin server
6
+ * to the new config's (admin.go replaceLocalAdminServer), so each of these would succeed ONCE and
7
+ * then strand the resource — no read-back, no drift check, no next deploy — or worse:
8
+ * · `admin off` (`disabled`): nothing can manage this Caddy through its API again.
9
+ * · a `listen` off loopback (`:2019`, `0.0.0.0:2019`, a LAN address): the unauthenticated API is
10
+ * on the network. Decision 23: never expose :2019.
11
+ * · a `listen` on another port, socket or loopback ADDRESS than the transport's: the provider
12
+ * loses Caddy. Go binds exactly the literal address given (`[::1]:2019` is not reachable at
13
+ * `127.0.0.1:2019`), and a hostname binds its first IPv4 address (net ListenConfig.Listen,
14
+ * `addrs.first(isIPv4)`), so `localhost:2019` is `127.0.0.1:2019`. MEASURED 2026-09-21 on a
15
+ * throwaway Caddy 2.11.4: after loading `admin [::1]:<p>`, `127.0.0.1:<p>` refused the
16
+ * connection; after `admin localhost:<p>`, `[::1]:<p>` did.
17
+ * · NO `admin` address at all: Caddy falls back to DefaultAdminListen — `localhost:2019`, or
18
+ * `$CADDY_ADMIN` in ITS environment (admin.go replaceLocalAdminServer), which this side cannot
19
+ * see. A Caddy reached any other way (a socket, another port) moves there on the first load.
20
+ * Refused unless the transport is at that default; declaring `admin <address>` is the fix.
21
+ * · a Host the new config does not allow: `origins` without it, or — with no `origins` — a Host
22
+ * outside Caddy's loopback defaults (`localhost`, `[::1]`, `127.0.0.1` at the listen port).
23
+ * Every later call answers 403 (admin.go allowedOrigins / checkHost; MEASURED on the mini's
24
+ * Caddy 2.11.4 — `host not allowed`). The transport also sends `Origin: http://<Host>`, which
25
+ * Caddy checks against the same list (originAllowed), so one check covers both.
26
+ * · `enforce_origin` over a unix socket: this transport, like Caddy's CLI (cmd/commandfuncs.go
27
+ * AdminAPIRequest), sends no Origin there, and a unix listener has no default origins — so
28
+ * checkOrigin answers 403 to every call. MEASURED 2026-09-21 on a throwaway Caddy 2.11.4:
29
+ * `client is not allowed to access from origin ''`, and that Caddy could not be managed again.
30
+ * · `remote`: a second, network-facing admin listener (mTLS on :2021) — declare that deliberately,
31
+ * not through this resource.
32
+ * · `config.load`: Caddy pulls its config from elsewhere, replacing this one on a timer.
33
+ * ★ Read in caddyserver/caddy v2.11.4: admin.go AdminConfig / allowedOrigins / originAllowed, and
34
+ * httpcaddyfile/options.go parseOptAdmin (an `admin { … }` block with no address still emits
35
+ * `listen` as the default, so a present `listen` is the norm, not a change).
36
+ */
37
+ import type { CaddyAdminListener } from './admin.ts';
38
+ /** Refusals for the adapted config's `admin` block, given where the transport reaches Caddy. */
39
+ export declare const adminProblems: (adapted: unknown, transport: CaddyAdminListener) => string[];
40
+ //# sourceMappingURL=admin-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-guard.d.ts","sourceRoot":"","sources":["../../src/caddy/admin-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgGrD,gGAAgG;AAChG,eAAO,MAAM,aAAa,YAAa,OAAO,aAAa,kBAAkB,KAAG,MAAM,EAoCrF,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The one seam between the Caddy provider and a running Caddy: every call to Caddy's admin API goes
3
+ * through a CaddyAdmin, and nothing in this directory reaches Caddy any other way.
4
+ *
5
+ * ★ CADDY's OWN MANAGEMENT API, NOT A FILE AND A SIGNAL. Decision 23 (vault consolidation plan,
6
+ * 2026-09-21): `POST /load` applies a Caddyfile with a graceful reload and rolls back a config
7
+ * Caddy refuses; `GET /config/` reports what is running. Read in caddyserver/caddy v2.11.4,
8
+ * caddyconfig/load.go and admin.go, and in the API docs (caddyserver.com/docs/api).
9
+ * ★ A SEAM, THE SAME SHAPE AS launchd's HostRunner, so the lifecycle runs against a fake admin
10
+ * server in tests and a consumer can plug in another route to the API (an SSH-forwarded socket
11
+ * today; a remote runner later) without touching the provider. PROMISES, NOT EFFECTS, for the
12
+ * same reason as HostRunner: a consumer writing their own implements plain async functions.
13
+ * ⛔ THE ADMIN API HAS NO AUTHENTICATION. Anyone who reaches it can replace every site. It stays on
14
+ * loopback or a unix socket; localCaddyAdmin() refuses anything else (local-admin.ts).
15
+ */
16
+ import * as Context from 'effect/Context';
17
+ import * as Layer from 'effect/Layer';
18
+ /** One admin API exchange. ⛔ `body` may hold the running config — never log it whole. */
19
+ export type CaddyAdminResponse = {
20
+ readonly status: number;
21
+ readonly body: string;
22
+ };
23
+ export type CaddyAdminRequest = {
24
+ readonly method: 'GET' | 'POST';
25
+ /** Absolute API path: `/load`, `/adapt`, `/config/`. */
26
+ readonly path: string;
27
+ readonly headers?: Readonly<Record<string, string>>;
28
+ readonly body?: string;
29
+ };
30
+ /**
31
+ * Where the admin listener is, in the terms Caddy's own `admin` option uses — so a Caddyfile that
32
+ * would move the listener away from where the provider talks to it can be refused (admin-guard.ts).
33
+ */
34
+ export type CaddyAdminListener = {
35
+ readonly kind: 'tcp';
36
+ readonly hostHeader: string;
37
+ readonly port: number;
38
+ } | {
39
+ readonly kind: 'unix';
40
+ readonly path: string;
41
+ };
42
+ export interface CaddyAdmin {
43
+ /** Where requests go, for error messages and the resource's attributes. Never a credential. */
44
+ readonly endpoint: string;
45
+ /**
46
+ * The listener as Caddy sees it. `hostHeader` is the Host every request carries: Caddy checks it
47
+ * against `admin.origins` (admin.go checkHost) on any loopback TCP listener.
48
+ */
49
+ readonly listener: CaddyAdminListener;
50
+ /**
51
+ * Send one request. Resolves with ANY status — classifying it is the caller's job
52
+ * (admin-calls.ts). Rejects only when there was no HTTP exchange at all: with a
53
+ * CaddyUnreachableError when nothing accepted the connection, any other Error otherwise.
54
+ */
55
+ request(request: CaddyAdminRequest): Promise<CaddyAdminResponse>;
56
+ }
57
+ /**
58
+ * Nothing accepted the connection — Caddy is not running, or not listening yet. A transport rejects
59
+ * with THIS only when the request provably never reached Caddy (refused, no socket file), because
60
+ * the provider treats it differently from every other failure: a stopped Caddy must not fail the
61
+ * PLAN that would bring it back (its launchd job in the same stack) — see config.ts.
62
+ */
63
+ export declare class CaddyUnreachableError extends Error {
64
+ readonly name = "CaddyUnreachableError";
65
+ }
66
+ declare const CaddyAdminService_base: Context.ServiceClass<CaddyAdminService, "homeflare/caddy/CaddyAdmin", CaddyAdmin>;
67
+ /** The Effect service the Caddy provider reads its admin transport from. */
68
+ export declare class CaddyAdminService extends CaddyAdminService_base {
69
+ }
70
+ /** Provide a transport to the provider: `Layer.provide(caddyAdminLayer(localCaddyAdmin()))`. */
71
+ export declare const caddyAdminLayer: (admin: CaddyAdmin) => Layer.Layer<CaddyAdminService>;
72
+ export {};
73
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/caddy/admin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,yFAAyF;AACzF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,MAAM,WAAW,UAAU;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClE;AAED;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;;AAED,4EAA4E;AAC5E,qBAAa,iBAAkB,SAAQ,sBAEtC;CAAG;AAEJ,gGAAgG;AAChG,eAAO,MAAM,eAAe,UAAW,UAAU,KAAG,KAAK,CAAC,KAAK,CAAC,iBAAiB,CACxC,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** One line's tokens: whitespace-separated, "double-quoted" and `backquoted` kept whole, # comments dropped. */
2
+ export declare const tokensOf: (line: string) => string[];
3
+ /** Refusals for a Caddyfile's text. Empty means nothing tripped. */
4
+ export declare const caddyfileSecretProblems: (caddyfile: string) => string[];
5
+ //# sourceMappingURL=caddyfile-tripwire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caddyfile-tripwire.d.ts","sourceRoot":"","sources":["../../src/caddy/caddyfile-tripwire.ts"],"names":[],"mappings":"AAgEA,gHAAgH;AAChH,eAAO,MAAM,QAAQ,SAAU,MAAM,KAAG,MAAM,EAuB7C,CAAC;AAoCF,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,cAAe,MAAM,KAAG,MAAM,EAOjE,CAAC"}
@@ -0,0 +1,27 @@
1
+ export interface CaddyConfigProps {
2
+ /**
3
+ * The whole Caddyfile. Applied with `POST /load` (Content-Type `text/caddyfile`); it REPLACES the
4
+ * running config. ⛔ Never a secret. ⚠️ `import` paths resolve against Caddy's working directory
5
+ * (the API adapts with no file name), so keep them absolute.
6
+ */
7
+ caddyfile: string;
8
+ /**
9
+ * The file Caddy was started with (`caddy run --config <this>`), when the same Caddyfile is also
10
+ * written there — `caddyWithFile()` sets it from its HostFile. Sent as
11
+ * `Caddy-Config-Source-File` so a load keeps Caddy's SIGUSR1 reload-from-file working.
12
+ */
13
+ sourceFile?: string;
14
+ }
15
+ export interface CaddyConfigAttributes {
16
+ /**
17
+ * SHA-256 of the ADAPTED JSON Caddy reported after the last apply (digest.ts canonical form) —
18
+ * never the config itself. Drift is the live config hashing to anything else.
19
+ */
20
+ configSha256: string;
21
+ /** The admin endpoint the config was applied through (no credential; the API has none). */
22
+ endpoint: string;
23
+ /** The `sourceFile` prop as applied. */
24
+ sourceFile?: string;
25
+ }
26
+ export declare const configProblems: (props: CaddyConfigProps) => string[];
27
+ //# sourceMappingURL=config-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-form.d.ts","sourceRoot":"","sources":["../../src/caddy/config-form.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,eAAO,MAAM,cAAc,UAAW,gBAAgB,KAAG,MAAM,EAiB9D,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CaddyConfig's read / diff / reconcile as plain async functions over a CaddyAdmin, so the
3
+ * lifecycle runs against a fake admin server in tests.
4
+ *
5
+ * The comparison is always between DIGESTS OF ADAPTED JSON (digest.ts): what `POST /adapt` makes
6
+ * of the declared Caddyfile, what `GET /config/` reports, and what was stored after the last
7
+ * apply. Three digests, because they answer different questions:
8
+ * · declared ≠ stored — the declaration changed.
9
+ * · live ≠ stored — DRIFT: a hand `curl` to the API, or a restart that loaded a different file
10
+ * (or an `autosave.json` holding someone else's change, under `--resume`).
11
+ * · declared = live — nothing to load, whatever the state says.
12
+ */
13
+ import type { Diff } from 'alchemy/Diff';
14
+ import type { CaddyAdmin } from './admin.ts';
15
+ import { type CaddyConfigAttributes, type CaddyConfigProps } from './config-form.ts';
16
+ type Desired = {
17
+ readonly digest: string;
18
+ readonly warnings: readonly string[];
19
+ };
20
+ /**
21
+ * Validate the props, adapt the Caddyfile on the running Caddy, and check its `admin` block.
22
+ * ★ Every refusal happens HERE, before anything is loaded — and diff calls this at plan time, so a
23
+ * Caddyfile that does not adapt, carries a literal secret, or would strand the admin API fails the
24
+ * PLAN, before any resource (a HostFile holding the same text included) is applied.
25
+ */
26
+ export declare const desiredConfig: (admin: CaddyAdmin, props: CaddyConfigProps) => Promise<Desired>;
27
+ /** What is running now, as attributes. `sourceFile` is carried over, never discovered. */
28
+ export declare const readLive: (admin: CaddyAdmin, sourceFile?: string) => Promise<CaddyConfigAttributes>;
29
+ /**
30
+ * ★ NEVER `replace`. Nothing in the props names a different object: a new Caddyfile is a reload of
31
+ * the same Caddy, and `sourceFile` is a header. (Which Caddy is the transport's business — see
32
+ * the ⚠️ on `endpoint` in the diff.)
33
+ */
34
+ export declare const diffConfig: (admin: CaddyAdmin, news: CaddyConfigProps, output: CaddyConfigAttributes) => Promise<Diff>;
35
+ export type Applied = {
36
+ readonly attributes: CaddyConfigAttributes;
37
+ /** The adapter's warnings (e.g. "Caddyfile input is not formatted") — log them, never refuse. */
38
+ readonly warnings: readonly string[];
39
+ /** Whether a `POST /load` was sent (false when Caddy already ran this config). */
40
+ readonly loaded: boolean;
41
+ };
42
+ /** Load the Caddyfile unless Caddy already runs it, then read it back and insist it matches. */
43
+ export declare const reconcileConfig: (admin: CaddyAdmin, props: CaddyConfigProps) => Promise<Applied>;
44
+ export {};
45
+ //# sourceMappingURL=config-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-lifecycle.d.ts","sourceRoot":"","sources":["../../src/caddy/config-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EAEtB,MAAM,kBAAkB,CAAC;AAQ1B,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAcjF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,UACjB,UAAU,SACV,gBAAgB,KACtB,OAAO,CAAC,OAAO,CAUjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,UACZ,UAAU,eACJ,MAAM,KAClB,OAAO,CAAC,qBAAqB,CAI9B,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,UACd,UAAU,QACX,gBAAgB,UACd,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAWd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,eAAe,UACnB,UAAU,SACV,gBAAgB,KACtB,OAAO,CAAC,OAAO,CAmCjB,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `Caddy.Config` — a running Caddy's whole config, declared as Caddyfile text and applied through
3
+ * Caddy's own admin API (decision 23).
4
+ *
5
+ * - create / update — `POST /adapt` (validate), `POST /load` with `text/caddyfile` (a graceful
6
+ * reload; Caddy keeps the old config if it refuses the new one, and the error names why), then
7
+ * `GET /config/` and insist it matches. Skipped when Caddy already runs this config.
8
+ * - read — the digest of `GET /config/`. ★ WITH NO STATE, A RUNNING CADDY IS ADOPTED: its live
9
+ * config becomes the starting point, and the deploy's forced update loads the declared one.
10
+ * - diff — declared, live and stored digests (config-lifecycle.ts). Never `replace`.
11
+ * - delete — ⛔ NOTHING. See below.
12
+ *
13
+ * ⛔ DELETE NEVER TOUCHES CADDY, UNDER EITHER REMOVAL POLICY. Removing this declaration must not take
14
+ * every site down: there is no "less config" to fall back to, and an empty config stops all
15
+ * servers. `retain` is the default (the engine never calls delete); with `destroy` the handler
16
+ * still only forgets. It never calls `/stop` (which exits the process) or `DELETE /config/`.
17
+ * To empty a Caddy, do it by hand at its admin API.
18
+ * ⛔ `caddyfile` IS NEVER A SECRET — see config-form.ts; refused before anything is sent.
19
+ * ⚠️ WHICH CADDY is the transport's (providers.ts), not a prop: one CaddyAdmin per stack.
20
+ */
21
+ import { Resource } from 'alchemy';
22
+ import * as Provider from 'alchemy/Provider';
23
+ import { CaddyAdminService } from './admin.ts';
24
+ import type { CaddyConfigAttributes, CaddyConfigProps } from './config-form.ts';
25
+ export type { CaddyConfigAttributes, CaddyConfigProps } from './config-form.ts';
26
+ export interface CaddyConfig extends Resource<'Caddy.Config', CaddyConfigProps, CaddyConfigAttributes> {
27
+ }
28
+ export declare const CaddyConfig: import("alchemy").ResourceClass<CaddyConfig>;
29
+ export declare const CaddyConfigProvider: () => import("effect/Layer").Layer<Provider.Provider<CaddyConfig>, never, CaddyAdminService>;
30
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/caddy/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,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,8FAsD7B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** Parse a config body Caddy returned. ⚠️ An empty config is the JSON `null`, not an error. */
2
+ export declare const parseConfig: (text: string) => unknown;
3
+ /** The canonical text of a parsed config: sorted keys, no whitespace. */
4
+ export declare const canonicalConfig: (config: unknown) => string;
5
+ /** SHA-256 of the canonical form — what the resource stores and compares, never the config. */
6
+ export declare const configDigest: (config: unknown) => string;
7
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/caddy/digest.ts"],"names":[],"mappings":"AAmCA,+FAA+F;AAC/F,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,OAAsC,CAAC;AAElF,yEAAyE;AACzE,eAAO,MAAM,eAAe,WAAY,OAAO,KAAG,MACA,CAAC;AAEnD,+FAA+F;AAC/F,eAAO,MAAM,YAAY,WAAY,OAAO,KAAG,MAA4C,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A fake Caddy admin API for the tests beside it — `Bun.serve` on an ephemeral 127.0.0.1 port (or a
3
+ * unix socket), answering `/adapt`, `/load` and `/config/` the way caddyserver/caddy v2.11.4 does.
4
+ *
5
+ * ⛔ TEST-ONLY. No provider imports this file, and it never forwards anywhere.
6
+ * ★ WHAT IS FAITHFUL, because the provider depends on it (see admin-calls.ts and digest.ts):
7
+ * · `/adapt` answers the adapter's STRUCT key order (unsorted); `/config/` answers SORTED keys,
8
+ * `<` escaped as `\u003c`, and a trailing newline — the differences the digest must erase.
9
+ * · A refused `/load` keeps the old config. With adapter warnings, the refusal comes in a 200
10
+ * whose body is the warnings followed by the error (load.go).
11
+ * · The Host check (admin.go checkHost) and the Origin check when one is sent.
12
+ * ★ THE TOY ADAPTER. A Caddyfile here is lines; each becomes a route. Keywords steer the fake:
13
+ * `SYNTAX_ERROR` fails the adapt; `PROVISION_ERROR` adapts but fails the load; a TAB anywhere is
14
+ * "not formatted" (the warning real Caddy gives unformatted input); `admin <listen>`,
15
+ * `admin off` and `origins <a> <b>` set the adapted `admin` block. With no site lines there are
16
+ * no apps at all, as real Caddy adapts a Caddyfile of only comments or global options.
17
+ * ★ `listenPort` is the port Caddy BELIEVES it listens on — its Host check uses it — while the
18
+ * fake really listens on an ephemeral one: a Caddy on its default :2019 behind a forward.
19
+ */
20
+ export type Seen = {
21
+ readonly method: string;
22
+ readonly path: string;
23
+ readonly headers: Headers;
24
+ readonly body: string;
25
+ };
26
+ export type FakeCaddy = {
27
+ /** An address localCaddyAdmin() takes. */
28
+ readonly address: string;
29
+ readonly port: number;
30
+ readonly seen: Seen[];
31
+ /** The running config as a parsed object (null when empty). */
32
+ running: unknown;
33
+ /** Adapt with the toy adapter — to stage a "restart with a different file". */
34
+ adapt(caddyfile: string): unknown;
35
+ stop(): void;
36
+ };
37
+ export declare const fakeCaddy: (options?: {
38
+ unix?: string;
39
+ running?: unknown;
40
+ origins?: string[];
41
+ listenPort?: number;
42
+ }) => FakeCaddy;
43
+ //# sourceMappingURL=fake-caddy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-caddy.d.ts","sourceRoot":"","sources":["../../src/caddy/fake-caddy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IACtB,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,IAAI,IAAI,IAAI,CAAC;CACd,CAAC;AAqDF,eAAO,MAAM,SAAS,aACX;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KACrF,SAoEF,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Caddy for Alchemy — a running Caddy's config, declared as Caddyfile text and applied through
3
+ * Caddy's own admin API (`POST /load`, `GET /config/`).
4
+ *
5
+ * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. The lifecycle,
6
+ * the digest, the guards and the admin calls are internals; an `export *` would publish them.
7
+ * ⛔ THE ADMIN API STAYS ON LOOPBACK OR A UNIX SOCKET. It has no authentication; localCaddyAdmin()
8
+ * refuses any other address, and a Caddyfile that would move or expose it is refused before load.
9
+ * Guide, order of file and load, `--resume` and secrets: docs/caddy.md.
10
+ */
11
+ export type { CaddyAdmin, CaddyAdminListener, CaddyAdminRequest, CaddyAdminResponse, } from './admin.ts';
12
+ export { CaddyAdminService, CaddyUnreachableError, caddyAdminLayer } from './admin.ts';
13
+ export { CaddyAdminError } from './admin-calls.ts';
14
+ export type { CaddyConfigAttributes, CaddyConfigProps } from './config.ts';
15
+ export { CaddyConfig, CaddyConfigProvider } from './config.ts';
16
+ export type { LocalCaddyAdminOptions } from './local-admin.ts';
17
+ export { DEFAULT_ADMIN_ADDRESS, localCaddyAdmin } from './local-admin.ts';
18
+ export { caddyProviders } from './providers.ts';
19
+ export type { CaddyWithFileProps } from './with-file.ts';
20
+ export { caddyWithFile } from './with-file.ts';
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/caddy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}