@homeflare/alchemy 0.6.0 → 0.8.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 (219) hide show
  1. package/README.md +74 -41
  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 +91 -0
  13. package/dist/caddy/config-lifecycle.d.ts.map +1 -0
  14. package/dist/caddy/config.d.ts +34 -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 +54 -0
  19. package/dist/caddy/fake-caddy.d.ts.map +1 -0
  20. package/dist/caddy/index.d.ts +22 -0
  21. package/dist/caddy/index.d.ts.map +1 -0
  22. package/dist/caddy/index.js +970 -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 +51 -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 +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/launchd/fake-sudo.d.ts +50 -0
  53. package/dist/launchd/fake-sudo.d.ts.map +1 -0
  54. package/dist/launchd/host-effect.d.ts +18 -9
  55. package/dist/launchd/host-effect.d.ts.map +1 -1
  56. package/dist/launchd/host-file-form.d.ts +5 -0
  57. package/dist/launchd/host-file-form.d.ts.map +1 -1
  58. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  59. package/dist/launchd/index.d.ts +2 -0
  60. package/dist/launchd/index.d.ts.map +1 -1
  61. package/dist/launchd/index.js +342 -8
  62. package/dist/launchd/index.js.map +16 -12
  63. package/dist/launchd/job-preflight.d.ts.map +1 -1
  64. package/dist/launchd/job-validate.d.ts +2 -0
  65. package/dist/launchd/job-validate.d.ts.map +1 -1
  66. package/dist/launchd/local-runner.d.ts +2 -0
  67. package/dist/launchd/local-runner.d.ts.map +1 -1
  68. package/dist/launchd/providers.d.ts +2 -1
  69. package/dist/launchd/providers.d.ts.map +1 -1
  70. package/dist/launchd/runner.d.ts +7 -5
  71. package/dist/launchd/runner.d.ts.map +1 -1
  72. package/dist/launchd/secret-tripwire.d.ts +1 -0
  73. package/dist/launchd/secret-tripwire.d.ts.map +1 -1
  74. package/dist/launchd/sudo-allowlist.d.ts +47 -0
  75. package/dist/launchd/sudo-allowlist.d.ts.map +1 -0
  76. package/dist/launchd/sudo-guard.d.ts +56 -0
  77. package/dist/launchd/sudo-guard.d.ts.map +1 -0
  78. package/dist/launchd/sudo-runner.d.ts +60 -0
  79. package/dist/launchd/sudo-runner.d.ts.map +1 -0
  80. package/dist/launchd/sudo-stage.d.ts +7 -0
  81. package/dist/launchd/sudo-stage.d.ts.map +1 -0
  82. package/dist/openbao/auth-role-form.d.ts +0 -9
  83. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  84. package/dist/openbao/auth-role.d.ts +3 -2
  85. package/dist/openbao/auth-role.d.ts.map +1 -1
  86. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  87. package/dist/openbao/fake-engines-roles.d.ts +32 -0
  88. package/dist/openbao/fake-engines-roles.d.ts.map +1 -0
  89. package/dist/openbao/fake-engines.d.ts +36 -0
  90. package/dist/openbao/fake-engines.d.ts.map +1 -0
  91. package/dist/openbao/fake-families.d.ts +49 -0
  92. package/dist/openbao/fake-families.d.ts.map +1 -0
  93. package/dist/openbao/fake-stack.d.ts +29 -0
  94. package/dist/openbao/fake-stack.d.ts.map +1 -0
  95. package/dist/openbao/host-approles.d.ts.map +1 -1
  96. package/dist/openbao/index.js +393 -234
  97. package/dist/openbao/index.js.map +23 -19
  98. package/dist/openbao/jwt-config.d.ts +5 -0
  99. package/dist/openbao/jwt-config.d.ts.map +1 -1
  100. package/dist/openbao/jwt-role.d.ts +2 -1
  101. package/dist/openbao/jwt-role.d.ts.map +1 -1
  102. package/dist/openbao/kubernetes-role.d.ts +2 -1
  103. package/dist/openbao/kubernetes-role.d.ts.map +1 -1
  104. package/dist/openbao/mfa-enforcement.d.ts +6 -3
  105. package/dist/openbao/mfa-enforcement.d.ts.map +1 -1
  106. package/dist/openbao/mfa-totp.d.ts.map +1 -1
  107. package/dist/openbao/pki-role.d.ts.map +1 -1
  108. package/dist/openbao/plugin.d.ts.map +1 -1
  109. package/dist/openbao/policy.d.ts +2 -0
  110. package/dist/openbao/policy.d.ts.map +1 -1
  111. package/dist/openbao/proxmox-role-form.d.ts +18 -8
  112. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  113. package/dist/openbao/proxmox-role-wire.d.ts +18 -0
  114. package/dist/openbao/proxmox-role-wire.d.ts.map +1 -0
  115. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  116. package/dist/openbao/rename-identity.d.ts +69 -0
  117. package/dist/openbao/rename-identity.d.ts.map +1 -0
  118. package/dist/openbao/rename.d.ts +107 -0
  119. package/dist/openbao/rename.d.ts.map +1 -0
  120. package/dist/openbao/ssh-role-erase.d.ts +11 -0
  121. package/dist/openbao/ssh-role-erase.d.ts.map +1 -0
  122. package/dist/openbao/ssh-role.d.ts.map +1 -1
  123. package/docs/caddy-admin.md +24 -0
  124. package/docs/caddy.md +199 -0
  125. package/docs/launchd-sudo.md +122 -0
  126. package/docs/launchd.md +7 -4
  127. package/docs/mesh-node.md +188 -0
  128. package/docs/peers.md +39 -0
  129. package/package.json +7 -2
  130. package/src/caddy/admin-calls.ts +128 -0
  131. package/src/caddy/admin-guard.ts +170 -0
  132. package/src/caddy/admin.ts +74 -0
  133. package/src/caddy/adopt-scope.test.ts +82 -0
  134. package/src/caddy/caddyfile-tripwire.ts +133 -0
  135. package/src/caddy/config-adopt.test.ts +235 -0
  136. package/src/caddy/config-form.ts +64 -0
  137. package/src/caddy/config-lifecycle.test.ts +196 -0
  138. package/src/caddy/config-lifecycle.ts +239 -0
  139. package/src/caddy/config.ts +153 -0
  140. package/src/caddy/digest.ts +44 -0
  141. package/src/caddy/fake-caddy.ts +179 -0
  142. package/src/caddy/guards.test.ts +156 -0
  143. package/src/caddy/index.ts +26 -0
  144. package/src/caddy/local-admin.test.ts +160 -0
  145. package/src/caddy/local-admin.ts +189 -0
  146. package/src/caddy/providers.test.ts +229 -0
  147. package/src/caddy/providers.ts +17 -0
  148. package/src/caddy/with-file.ts +66 -0
  149. package/src/cloudflare/fake-mesh.ts +181 -0
  150. package/src/cloudflare/index.ts +9 -3
  151. package/src/cloudflare/mesh-node-api.ts +127 -0
  152. package/src/cloudflare/mesh-node-edges.test.ts +103 -0
  153. package/src/cloudflare/mesh-node-form.test.ts +80 -0
  154. package/src/cloudflare/mesh-node-form.ts +115 -0
  155. package/src/cloudflare/mesh-node-harness.ts +124 -0
  156. package/src/cloudflare/mesh-node-lifecycle.ts +187 -0
  157. package/src/cloudflare/mesh-node-policy.test.ts +151 -0
  158. package/src/cloudflare/mesh-node-token.test.ts +117 -0
  159. package/src/cloudflare/mesh-node-token.ts +125 -0
  160. package/src/cloudflare/mesh-node.test.ts +172 -0
  161. package/src/cloudflare/mesh-node.ts +120 -0
  162. package/src/cloudflare/providers.ts +20 -2
  163. package/src/index.ts +1 -0
  164. package/src/launchd/fake-sudo.ts +136 -0
  165. package/src/launchd/host-effect.ts +33 -1
  166. package/src/launchd/host-file-form.ts +10 -2
  167. package/src/launchd/host-file-lifecycle.ts +2 -1
  168. package/src/launchd/index.ts +2 -0
  169. package/src/launchd/job-preflight.ts +3 -2
  170. package/src/launchd/job-validate.ts +1 -1
  171. package/src/launchd/local-runner.ts +6 -3
  172. package/src/launchd/providers.test.ts +3 -0
  173. package/src/launchd/providers.ts +2 -1
  174. package/src/launchd/runner.ts +7 -5
  175. package/src/launchd/secret-tripwire.ts +1 -1
  176. package/src/launchd/sudo-allowlist.test.ts +191 -0
  177. package/src/launchd/sudo-allowlist.ts +213 -0
  178. package/src/launchd/sudo-guard.test.ts +100 -0
  179. package/src/launchd/sudo-guard.ts +151 -0
  180. package/src/launchd/sudo-lifecycle.test.ts +172 -0
  181. package/src/launchd/sudo-runner-files.test.ts +240 -0
  182. package/src/launchd/sudo-runner.test.ts +126 -0
  183. package/src/launchd/sudo-runner.ts +235 -0
  184. package/src/launchd/sudo-stage.ts +28 -0
  185. package/src/openbao/README.md +8 -2
  186. package/src/openbao/REPLACE.md +126 -22
  187. package/src/openbao/auth-role-form.test.ts +2 -16
  188. package/src/openbao/auth-role-form.ts +0 -11
  189. package/src/openbao/auth-role.ts +35 -14
  190. package/src/openbao/cloudflare-role-wire.test.ts +8 -34
  191. package/src/openbao/cloudflare-role.ts +22 -8
  192. package/src/openbao/fake-engines-roles.ts +114 -0
  193. package/src/openbao/fake-engines.ts +126 -0
  194. package/src/openbao/fake-families.ts +182 -0
  195. package/src/openbao/fake-stack.ts +112 -0
  196. package/src/openbao/host-approles.test.ts +15 -2
  197. package/src/openbao/host-approles.ts +19 -4
  198. package/src/openbao/jwt-config.ts +26 -5
  199. package/src/openbao/jwt-role.ts +24 -8
  200. package/src/openbao/kubernetes-role.ts +20 -8
  201. package/src/openbao/mfa-enforcement.ts +23 -8
  202. package/src/openbao/mfa-totp.ts +21 -8
  203. package/src/openbao/pki-role.ts +19 -7
  204. package/src/openbao/plugin.ts +41 -15
  205. package/src/openbao/policy-rename.test.ts +112 -0
  206. package/src/openbao/policy-wire.test.ts +7 -26
  207. package/src/openbao/policy.ts +27 -4
  208. package/src/openbao/proxmox-role-form.ts +27 -8
  209. package/src/openbao/proxmox-role-wire.ts +24 -0
  210. package/src/openbao/proxmox-role.ts +52 -36
  211. package/src/openbao/rename-adoption.test.ts +82 -0
  212. package/src/openbao/rename-families.test.ts +122 -0
  213. package/src/openbao/rename-identity.test.ts +129 -0
  214. package/src/openbao/rename-identity.ts +117 -0
  215. package/src/openbao/rename-occupied.test.ts +166 -0
  216. package/src/openbao/rename.ts +166 -0
  217. package/src/openbao/role-rename.test.ts +205 -0
  218. package/src/openbao/ssh-role-erase.ts +50 -0
  219. package/src/openbao/ssh-role.ts +16 -54
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,12 +93,6 @@ 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
98
  Vault objects as resources — mounts (with `remountFrom` moves), auth methods, policies, AppRole,
@@ -90,9 +119,43 @@ delete-first.
90
119
  - ⛔ **No silent sudo:** the system domain needs a deploy started as root, or a `privileged` runner.
91
120
  - ⚠️ `org.nixos.*` jobs are never adopted. Guide and nix-darwin cutover: [docs/launchd.md](./docs/launchd.md).
92
121
 
122
+ ### Deploying as yourself — `sudoRunner()`
123
+
124
+ ```ts
125
+ launchdProviders(sudoRunner({ prefixes: ['/Library/LaunchDaemons', '/opt/example'] }));
126
+ ```
127
+
128
+ The deploy runs as you. Only `launchctl bootstrap | bootout | kickstart` in the system domain, and
129
+ `install` / `rm` of a file under a declared prefix, go through `sudo -n`, in exact argv shapes. Each
130
+ one is logged (argv only, never content) before it runs. A plan never calls sudo.
131
+
132
+ - ⛔ **Opt-in, never a fallback**, and it never prompts: if sudo needs a password, the call fails at
133
+ once and says to run `sudo -v`, or to grant exactly these commands `NOPASSWD`.
134
+ - ⚠️ Refused before sudo is asked: any other argv, a prefix that is not a real directory only root
135
+ may write, a path outside every prefix that needs root, a symlink on the way, a file you could
136
+ not read back, and another user's `gui/<uid>`.
137
+ - Full list, sudoers cautions and limits: [docs/launchd-sudo.md](./docs/launchd-sudo.md).
138
+
139
+ ## Caddy — `@homeflare/alchemy/caddy`
140
+
141
+ `CaddyConfig` declares a running Caddy's config as Caddyfile text, applied through Caddy's own admin
142
+ API: `POST /load` (graceful reload; Caddy keeps the old config if it refuses the new one, and the
143
+ deploy says why), `GET /config/` for drift. `caddyWithFile()` also writes the file Caddy starts from,
144
+ with `HostFile`; `caddyProviders()` provides the transport, `http://127.0.0.1:2019` by default.
145
+
146
+ - ⛔ **Nothing is adopted silently:** a running Caddy whose config is not the declared one plans as
147
+ `Unowned`, and the deploy needs `--adopt`.
148
+ - ⛔ **No secrets in the Caddyfile** (state is unencrypted): `{env.NAME}` / `{file./path}` placeholders.
149
+ - ⛔ **The admin API stays on loopback or a unix socket**, and a Caddyfile that would move it is refused.
150
+ - ⛔ **Delete never unloads or stops Caddy.** Order of file and load: [docs/caddy.md](./docs/caddy.md).
151
+ - ★ **Managed Caddies run `--resume` with their own `XDG_CONFIG_HOME`**, so a restart runs the last
152
+ config Caddy accepted — and after one, SIGUSR1 has no file to reload. Why, and the rest: same doc.
153
+
93
154
  ## Credentials
94
155
 
95
156
  `CLOUDFLARE_API_TOKEN` is read from the environment at call time, never at module scope.
157
+ `MeshNode` instead resolves credentials and the account the way `Cloudflare.providers()` does
158
+ (an Alchemy profile, or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` in CI).
96
159
 
97
160
  ⛔ **Mint a short-lived, scoped token** — do not reuse a long-lived one, and never a Global
98
161
  API Key. An empty value fails closed with a message saying so, because an empty render is
@@ -101,11 +164,6 @@ like a bad credential.
101
164
 
102
165
  ## Peers — and one override you need
103
166
 
104
- ```sh
105
- bun add @homeflare/alchemy alchemy@2.0.0-beta.79 effect@4.0.0-rc.115 \
106
- @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0
107
- ```
108
-
109
167
  ⚠️ Peers, not dependencies: Alchemy's resource registry and Effect's context both break if
110
168
  two copies load in one process.
111
169
 
@@ -123,33 +181,8 @@ two copies load in one process.
123
181
  }
124
182
  ```
125
183
 
126
- 🔴 **Why, measured 2026-09-16 on 0.1.0 and re-checked 2026-09-17 against Alchemy 78.**
127
- Effect's `rc` line is not semver-compatible with itself. Alchemy 78's peer is
128
- `effect >= 4.0.0-rc.115`; an unlocked `@effect/platform-node-shared` still floats to the
129
- next rc and breaks at import. Pin the whole set.
130
-
131
- | what resolves | what happens |
132
- | ------------------------------------------------------------- | ------------------------------------------------------ |
133
- | Alchemy 77 + `effect` → rc.115 (measured 2026-09-16) | `TypeError: Config.string is not a function` at import |
134
- | `@effect/platform-node-shared` newer than the pinned `effect` | `Cannot find module 'effect/ByteSize'` |
135
- | `rolldown` → 1.2.9 via vite's `~1.2.6` (measured 2026-09-16) | `GET …/rolldown-1.2.9.tgz - 404` at `bun add` |
136
- | no `mime` (measured 2026-09-17 against Alchemy 78) | `Cannot find package 'mime'` from cloudflare-runtime |
137
-
138
- Alchemy 78 adapted to rc.115 — that first row is why we used to pin 112, not a reason to
139
- stay there. The override is still the only thing that holds the set together.
140
-
141
- ⚠️ `@effect/platform-node` is **required, not optional**: Alchemy's module graph reaches
142
- `Cloudflare/Workers/WorkerBridge → @effect/platform-node/NodeServices` even when you only
143
- import the Proxmox subpath.
144
-
145
- ⚠️ So is `mime`. Alchemy 78's `@alchemy.run/cloudflare-runtime` imported it without
146
- declaring it (measured 2026-09-17). 79 declares it; the peer stays so a consumer
147
- that followed the 78 README does not drop a required line.
148
-
149
- ⚠️ So is `cloudflare`. It was marked optional in 0.1.1, which claimed the `/cloudflare`
150
- subpath would degrade without it — measured 2026-09-16, the subpath does not load at all:
151
- `Cannot find package 'cloudflare'`. An optional peer should mean a feature is absent, not
152
- that an import fails.
184
+ Why each peer is required and each pin exists — the measurements, and the error a missing one
185
+ produces: [docs/peers.md](./docs/peers.md).
153
186
 
154
187
  ## License
155
188
 
@@ -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,91 @@
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
+ * With NO state there is no stored digest, and the question is whose config is running: see
13
+ * `claimable` below, which both the adoption probe and the apply ask.
14
+ */
15
+ import type { Diff } from 'alchemy/Diff';
16
+ import { type CaddyAdmin } from './admin.ts';
17
+ import { type CaddyConfigAttributes, type CaddyConfigProps } from './config-form.ts';
18
+ type Desired = {
19
+ readonly digest: string;
20
+ readonly warnings: readonly string[];
21
+ };
22
+ /**
23
+ * Validate the props, adapt the Caddyfile on the running Caddy, and check its `admin` block.
24
+ * ★ Every refusal happens HERE, before anything is loaded — and diff calls this at plan time, so a
25
+ * Caddyfile that does not adapt, carries a literal secret, or would strand the admin API fails the
26
+ * PLAN, before any resource (a HostFile holding the same text included) is applied.
27
+ */
28
+ export declare const desiredConfig: (admin: CaddyAdmin, props: CaddyConfigProps) => Promise<Desired>;
29
+ /** What is running now, as attributes. `sourceFile` is carried over, never discovered. */
30
+ export declare const readLive: (admin: CaddyAdmin, sourceFile?: string) => Promise<CaddyConfigAttributes>;
31
+ /**
32
+ * ⛔ WITH NO STATE, A RUNNING CONFIG IS THIS STACK'S ONLY IF IT IS THE DECLARED ONE (decision,
33
+ * 2026-09-21: CaddyConfig must not adopt silently — the house rule HostFile and LaunchdJob keep).
34
+ * Anything else was put there by a person, another tool or another stack, and loading over it
35
+ * replaces every site it serves. ★ A config that serves nothing (`null`, or no apps) is claimable
36
+ * too: there is nothing to take over, as an empty R2 lock rule set reads as no lock.
37
+ */
38
+ export declare const claimable: (running: unknown, declaredDigest: string) => boolean;
39
+ export type Probe = {
40
+ readonly attributes: CaddyConfigAttributes;
41
+ /** The live config IS the declared one: adopting it changes nothing. */
42
+ readonly ours: boolean;
43
+ /** Why the declaration could not be compared, when it could not — the Caddy is then not ours. */
44
+ readonly unchecked?: string;
45
+ };
46
+ /**
47
+ * The adoption probe — `read` with no state: `undefined` when Caddy serves nothing (plan a create),
48
+ * else the live attributes and whether they are provably ours (the provider brands the rest
49
+ * `Unowned`, and the plan refuses them without `--adopt`).
50
+ * ⛔ IT NEVER THROWS OVER THE DECLARATION. The engine also runs this read to recover an interrupted
51
+ * create, with THAT deploy's props (Plan.ts, `status: "creating"`) — so a Caddyfile that failed to
52
+ * adapt then would fail every later plan, the one carrying the fix included. What cannot be
53
+ * compared is "not proven ours", with the reason; the next diff or apply validates the Caddyfile.
54
+ */
55
+ export declare const probeLive: (admin: CaddyAdmin, caddyfile: string | undefined, sourceFile?: string) => Promise<Probe | undefined>;
56
+ /**
57
+ * ★ NEVER `replace`. Nothing in the props names a different object: a new Caddyfile is a reload of
58
+ * the same Caddy, and `sourceFile` is a header. (Which Caddy is the transport's business — see
59
+ * the ⚠️ on `endpoint` in the diff.)
60
+ */
61
+ export declare const diffConfig: (admin: CaddyAdmin, news: CaddyConfigProps, output: CaddyConfigAttributes) => Promise<Diff>;
62
+ export type Applied = {
63
+ readonly attributes: CaddyConfigAttributes;
64
+ /** The adapter's warnings (e.g. "Caddyfile input is not formatted") — log them, never refuse. */
65
+ readonly warnings: readonly string[];
66
+ /** Whether a `POST /load` was sent (false when Caddy already ran this config). */
67
+ readonly loaded: boolean;
68
+ };
69
+ /**
70
+ * What this apply may load over — config.ts decides from the state and the adopt setting:
71
+ * · `takeOver` — any running config: the state was applied to THIS Caddy (an update, drift
72
+ * correction, or a create the engine adopted), or adoption is on (`--adopt`, or the
73
+ * resource's own `adopt(true)` — host-effect.ts adoptEnabled).
74
+ * · otherwise only a claimable one — plus, as `stored`, the digest the state last recorded: a
75
+ * Caddy reached at a NEW endpoint that runs exactly that is the same config under another name.
76
+ */
77
+ export type Authority = {
78
+ readonly takeOver: boolean;
79
+ readonly stored?: string;
80
+ };
81
+ /**
82
+ * Load the Caddyfile unless Caddy already runs it, then read it back and insist it matches.
83
+ * ⛔ WITHOUT `takeOver`, A CONFIG THE STACK CANNOT CLAIM IS NEVER LOADED OVER. The engine's adoption
84
+ * probe cannot guard every create: it is skipped while `news` holds an Output — ALWAYS on
85
+ * caddyWithFile's first deploy (`sourceFile` is its HostFile's path) — and a Caddy that was down at
86
+ * plan time read as nothing. Nor does state vouch for a Caddy the transport NOW reaches at another
87
+ * endpoint (diff plans that as an update). Refused here, before any `/load`, like HostFile's.
88
+ */
89
+ export declare const reconcileConfig: (admin: CaddyAdmin, props: CaddyConfigProps, authority: Authority) => Promise<Applied>;
90
+ export {};
91
+ //# 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;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,KAAK,UAAU,EAAyB,MAAM,YAAY,CAAC;AAQpE,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;AAYF,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,UACZ,UAAU,eACJ,MAAM,KAClB,OAAO,CAAC,qBAAqB,CACiC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,YAAa,OAAO,kBAAkB,MAAM,KAAG,OACD,CAAC;AAErE,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,iGAAiG;IACjG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,UACb,UAAU,aACN,MAAM,GAAG,SAAS,eAChB,MAAM,KAClB,OAAO,CAAC,KAAK,GAAG,SAAS,CAmB3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,UACd,UAAU,QACX,gBAAgB,UACd,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAYd,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;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,UACnB,UAAU,SACV,gBAAgB,aACZ,SAAS,KACnB,OAAO,CAAC,OAAO,CA8CjB,CAAC"}
@@ -0,0 +1,34 @@
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 it is the adoption probe (below).
9
+ * - diff — declared, live and stored digests (config-lifecycle.ts). Never `replace`.
10
+ * - delete — ⛔ NOTHING. See below.
11
+ *
12
+ * ⛔ DELETE NEVER TOUCHES CADDY, UNDER EITHER REMOVAL POLICY. Removing this declaration must not take
13
+ * every site down: there is no "less config" to fall back to, and an empty config stops all
14
+ * servers. `retain` is the default (the engine never calls delete); with `destroy` the handler
15
+ * still only forgets. It never calls `/stop` (which exits the process) or `DELETE /config/`.
16
+ * To empty a Caddy, do it by hand at its admin API.
17
+ * ⛔ NOTHING IS ADOPTED SILENTLY (decision, 2026-09-21 — the house rule HostFile and LaunchdJob
18
+ * keep). With no state, a running Caddy whose live config IS the declared one reads as ours (the
19
+ * adoption changes nothing); any other config reads as `Unowned`, and the plan refuses it unless
20
+ * the deploy runs with `--adopt`. A Caddy serving nothing reads as absent: a create. Where the
21
+ * engine skips that probe, reconcile refuses the same takeover (config-lifecycle.ts).
22
+ * ⛔ `caddyfile` IS NEVER A SECRET — see config-form.ts; refused before anything is sent.
23
+ * ⚠️ WHICH CADDY is the transport's (providers.ts), not a prop: one CaddyAdmin per stack.
24
+ */
25
+ import { Resource } from 'alchemy';
26
+ import * as Provider from 'alchemy/Provider';
27
+ import { CaddyAdminService } from './admin.ts';
28
+ import type { CaddyConfigAttributes, CaddyConfigProps } from './config-form.ts';
29
+ export type { CaddyConfigAttributes, CaddyConfigProps } from './config-form.ts';
30
+ export interface CaddyConfig extends Resource<'Caddy.Config', CaddyConfigProps, CaddyConfigAttributes> {
31
+ }
32
+ export declare const CaddyConfig: import("alchemy").ResourceClass<CaddyConfig>;
33
+ export declare const CaddyConfigProvider: () => import("effect/Layer").Layer<Provider.Provider<CaddyConfig>, never, CaddyAdminService>;
34
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/caddy/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAyB,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGhF,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,gBAAgB,EAChB,qBAAqB,CACtB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAEtB,CAAC;AAsBH,eAAO,MAAM,mBAAmB,8FAsF7B,CAAC"}
@@ -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,54 @@
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
+ import type { CaddyAdmin } from './admin.ts';
21
+ export type Seen = {
22
+ readonly method: string;
23
+ readonly path: string;
24
+ readonly headers: Headers;
25
+ readonly body: string;
26
+ };
27
+ export type FakeCaddy = {
28
+ /** An address localCaddyAdmin() takes. */
29
+ readonly address: string;
30
+ readonly port: number;
31
+ readonly seen: Seen[];
32
+ /** The running config as a parsed object (null when empty). */
33
+ running: unknown;
34
+ /** Adapt with the toy adapter — to stage a "restart with a different file". */
35
+ adapt(caddyfile: string): unknown;
36
+ stop(): void;
37
+ };
38
+ export declare const fakeCaddy: (options?: {
39
+ unix?: string;
40
+ running?: unknown;
41
+ origins?: string[];
42
+ listenPort?: number;
43
+ }) => FakeCaddy;
44
+ /**
45
+ * A fake Caddy on its DEFAULT :2019 behind a forward (`hostHeader`), and the transport to it.
46
+ * ★ Addressed that way so a Caddyfile with no `admin` line — the common case — passes the guard.
47
+ * A Caddy reached on any other port needs `admin <address>` declared, or the first load would
48
+ * move it (admin-guard.ts).
49
+ */
50
+ export declare const fakeDefaultCaddy: (running?: unknown) => {
51
+ admin: CaddyAdmin;
52
+ caddy: FakeCaddy;
53
+ };
54
+ //# 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,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,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;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,aAAc,OAAO,KAAG;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAQzF,CAAC"}
@@ -0,0 +1,22 @@
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
+ * ⛔ NOTHING IS ADOPTED SILENTLY: a running config that is not the declared one needs `--adopt`.
10
+ * Guide, order of file and load, adoption, `--resume` and secrets: docs/caddy.md.
11
+ */
12
+ export type { CaddyAdmin, CaddyAdminListener, CaddyAdminRequest, CaddyAdminResponse, } from './admin.ts';
13
+ export { CaddyAdminService, CaddyUnreachableError, caddyAdminLayer } from './admin.ts';
14
+ export { CaddyAdminError } from './admin-calls.ts';
15
+ export type { CaddyConfigAttributes, CaddyConfigProps } from './config.ts';
16
+ export { CaddyConfig, CaddyConfigProvider } from './config.ts';
17
+ export type { LocalCaddyAdminOptions } from './local-admin.ts';
18
+ export { DEFAULT_ADMIN_ADDRESS, localCaddyAdmin } from './local-admin.ts';
19
+ export { caddyProviders } from './providers.ts';
20
+ export type { CaddyWithFileProps } from './with-file.ts';
21
+ export { caddyWithFile } from './with-file.ts';
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/caddy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;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"}