@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
@@ -114,15 +114,222 @@ var R2BucketLockProvider = () => Provider.effect(R2BucketLock, Effect2.gen(funct
114
114
  delete: Effect2.fn(({ olds, output }) => deleteLock(client, output, olds.accountId))
115
115
  });
116
116
  }));
117
- // src/cloudflare/providers.ts
117
+ // src/cloudflare/mesh-node.ts
118
+ import { Credentials } from "@distilled.cloud/cloudflare/Credentials";
119
+ import * as Cloudflare2 from "alchemy/Cloudflare";
120
+ import { isResolved } from "alchemy/Diff";
118
121
  import * as Provider2 from "alchemy/Provider";
122
+ import { Resource as Resource2 } from "alchemy/Resource";
123
+ import * as Context2 from "effect/Context";
124
+ import * as Effect5 from "effect/Effect";
125
+ import * as HttpClient from "effect/unstable/http/HttpClient";
126
+
127
+ // src/cloudflare/mesh-node-api.ts
128
+ import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
129
+ import * as Data from "effect/Data";
130
+ import * as Effect3 from "effect/Effect";
131
+ import * as Stream from "effect/Stream";
132
+
133
+ class MeshNodeError extends Data.TaggedError("MeshNodeError") {
134
+ }
135
+ var STATUSES = ["inactive", "degraded", "healthy", "down"];
136
+ var observe = (raw) => {
137
+ if (typeof raw.id !== "string" || raw.id.length === 0)
138
+ return;
139
+ if (raw.deletedAt !== undefined && raw.deletedAt !== null)
140
+ return;
141
+ const status = typeof raw.status === "string" && STATUSES.includes(raw.status) ? raw.status : undefined;
142
+ return { id: raw.id, name: raw.name ?? "", status };
143
+ };
144
+ var getNode = (accountId, nodeId) => zeroTrust.getTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(Effect3.map(observe), Effect3.catchTag("TunnelNotFound", () => Effect3.succeed(undefined)));
145
+ var findNodeByName = (accountId, name) => zeroTrust.listTunnelWarpConnectors.items({ accountId, name, isDeleted: false }).pipe(Stream.map(observe), Stream.filter((node) => node !== undefined && node.name === name), Stream.runCollect, Effect3.flatMap((chunk) => {
146
+ const matches = Array.from(chunk);
147
+ if (matches.length <= 1)
148
+ return Effect3.succeed(matches[0]);
149
+ const ids = matches.map((node) => node.id).join(", ");
150
+ return Effect3.fail(new MeshNodeError({
151
+ message: `Mesh node name "${name}" matches ${matches.length} live nodes (${ids}). Names are unique per account, so refusing to guess which one is meant.`
152
+ }));
153
+ }));
154
+ var createNode = (accountId, name, ha) => zeroTrust.createTunnelWarpConnector({ accountId, name, ha }).pipe(Effect3.flatMap((raw) => {
155
+ const node = observe(raw);
156
+ return node === undefined ? Effect3.fail(new MeshNodeError({ message: `Creating Mesh node "${name}" returned no id.` })) : Effect3.succeed(node);
157
+ }));
158
+ var renameNode = (accountId, nodeId, name) => zeroTrust.patchTunnelWarpConnector({ accountId, tunnelId: nodeId, name }).pipe(Effect3.map((raw) => observe(raw) ?? { id: nodeId, name, status: undefined }));
159
+ var deleteNode = (accountId, nodeId) => zeroTrust.deleteTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(Effect3.asVoid, Effect3.catchTag("TunnelNotFound", () => Effect3.void));
160
+
161
+ // src/cloudflare/mesh-node-form.ts
162
+ var validateMeshNode = (props) => {
163
+ if (typeof props.name !== "string" || props.name.trim().length === 0) {
164
+ return new MeshNodeError({ message: "MeshNode `name` must be a non-empty string." });
165
+ }
166
+ if (props.name !== props.name.trim()) {
167
+ return new MeshNodeError({
168
+ message: `MeshNode name "${props.name}" has leading or trailing whitespace; adoption matches names exactly.`
169
+ });
170
+ }
171
+ if (typeof props.ha !== "boolean") {
172
+ return new MeshNodeError({
173
+ message: "MeshNode `ha` must be true or false. It is create-only, so it has no default."
174
+ });
175
+ }
176
+ return;
177
+ };
178
+ var diffMeshNode = (news, output, accountId) => {
179
+ if (output === undefined)
180
+ return;
181
+ if (output.accountId !== accountId)
182
+ return { action: "replace" };
183
+ const fields = news;
184
+ const name = typeof fields.name === "string" ? fields.name : undefined;
185
+ if (typeof fields.ha === "boolean" && fields.ha !== output.ha) {
186
+ const sameName = name === undefined || name === output.name;
187
+ return sameName ? { action: "replace", deleteFirst: true } : { action: "replace" };
188
+ }
189
+ if (name !== undefined && name !== output.name)
190
+ return { action: "update" };
191
+ return;
192
+ };
193
+
194
+ // src/cloudflare/mesh-node-lifecycle.ts
195
+ import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
196
+ import * as Effect4 from "effect/Effect";
197
+ var attributesOf = (node, accountId, ha) => ({
198
+ id: node.id,
199
+ accountId,
200
+ name: node.name,
201
+ status: node.status,
202
+ ha
203
+ });
204
+ var readMeshNode = (accountId, olds, output) => Effect4.gen(function* () {
205
+ const account = output?.accountId ?? accountId;
206
+ if (output !== undefined) {
207
+ const node = yield* getNode(account, output.id);
208
+ if (node !== undefined)
209
+ return attributesOf(node, account, output.ha);
210
+ }
211
+ const name = olds?.name ?? output?.name;
212
+ const ha = olds?.ha ?? output?.ha;
213
+ if (name === undefined || ha === undefined)
214
+ return;
215
+ const match = yield* findNodeByName(account, name);
216
+ return match === undefined ? undefined : Unowned2(attributesOf(match, account, ha));
217
+ });
218
+ var nameTaken = (name, holder) => new MeshNodeError({
219
+ message: `A tunnel named "${name}" already exists (${holder}). Names are unique per account, and ` + "this resource never takes over a node it did not create by its own id."
220
+ });
221
+ var REOWN = "drop this resource's state row (alchemy state rm, which leaves the node alone), then deploy " + "under adopt(true)";
222
+ var oldNodeHoldsName = (name, holder) => new MeshNodeError({
223
+ message: `Mesh node "${name}" already exists (${holder}) and is not the node this resource tracks, ` + "so it is left alone: names are unique per account. If this deploy changed `ha` and kept " + "the name, it is this resource's old node, kept by MeshNode's default " + "RemovalPolicy.retain because HA cannot change in place. To replace it (a new id, token and " + "Mesh IP per replica), deploy once with .pipe(RemovalPolicy.destroy()), which deletes it " + `first, or delete it by hand. To keep it, reverting \`ha\` is not enough: ${REOWN}, with ` + "the original `ha`. Otherwise it is another node: do not delete it. Choose another name, " + `or, if it is meant to be this one, check its HA badge, then ${REOWN}.`
224
+ });
225
+ var raced = (accountId, name) => findNodeByName(accountId, name).pipe(Effect4.flatMap((holder) => Effect4.fail(holder === undefined ? nameTaken(name, "not a live Mesh node: another tunnel type, or a just-deleted node") : new MeshNodeError({
226
+ message: `Mesh node "${name}" (${holder.id}) appeared between this deploy's lookup and its ` + "create. The likeliest cause is this deploy itself: the SDK retries a create whose " + "response was lost. If so, check its HA badge matches the declared `ha`, then " + "adopt it: adopt(true) after a first create, but after a replace " + `${REOWN}, because adopt(true) alone does not act on a replace's row. ` + "Otherwise another creator raced this one."
227
+ }))));
228
+ var createFresh = (accountId, news) => Effect4.gen(function* () {
229
+ const existing = yield* findNodeByName(accountId, news.name);
230
+ if (existing !== undefined)
231
+ return yield* Effect4.fail(oldNodeHoldsName(news.name, existing.id));
232
+ const node = yield* createNode(accountId, news.name, news.ha).pipe(Effect4.catchTag("DuplicateTunnelName", () => raced(accountId, news.name)));
233
+ return attributesOf(node, accountId, news.ha);
234
+ });
235
+ var reconcileMeshNode = (accountId, news, output) => Effect4.gen(function* () {
236
+ const invalid = validateMeshNode(news);
237
+ if (invalid !== undefined)
238
+ return yield* Effect4.fail(invalid);
239
+ const owned = output !== undefined && output.accountId === accountId ? output : undefined;
240
+ const live = owned === undefined ? undefined : yield* getNode(accountId, owned.id);
241
+ if (owned === undefined || live === undefined)
242
+ return yield* createFresh(accountId, news);
243
+ if (owned.ha !== news.ha) {
244
+ return yield* Effect4.fail(new MeshNodeError({
245
+ message: `MeshNode "${news.name}": \`ha\` is create-only (stored ${owned.ha}, declared ${news.ha}). Plan again so the change is a replace.`
246
+ }));
247
+ }
248
+ if (live.name === news.name)
249
+ return attributesOf(live, accountId, owned.ha);
250
+ const renamed = yield* renameNode(accountId, live.id, news.name).pipe(Effect4.catchTag("DuplicateTunnelName", () => Effect4.fail(nameTaken(news.name, "held by another tunnel"))));
251
+ return attributesOf(renamed, accountId, owned.ha);
252
+ });
253
+ var deleteMeshNode = (output) => deleteNode(output.accountId, output.id);
254
+
255
+ // src/cloudflare/mesh-node.ts
256
+ var MeshNode = Resource2("Cloudflare.MeshNode", {
257
+ defaultRemovalPolicy: "retain"
258
+ });
259
+ var accountId = Effect5.gen(function* () {
260
+ const resolved = yield* yield* Cloudflare2.CloudflareEnvironment;
261
+ return resolved.accountId;
262
+ });
263
+ var MeshNodeProvider = () => Provider2.effect(MeshNode, Effect5.gen(function* () {
264
+ const services = Context2.make(Cloudflare2.CloudflareEnvironment, yield* Cloudflare2.CloudflareEnvironment).pipe(Context2.add(Credentials, yield* Credentials), Context2.add(HttpClient.HttpClient, yield* HttpClient.HttpClient));
265
+ const run = (effect4) => Effect5.provideContext(effect4, services);
266
+ return MeshNode.Provider.of({
267
+ stables: ["id", "accountId", "ha"],
268
+ list: () => Effect5.succeed([]),
269
+ nuke: { skip: true },
270
+ diff: ({ news, output }) => run(Effect5.gen(function* () {
271
+ const invalid = isResolved(news) ? validateMeshNode(news) : undefined;
272
+ if (invalid !== undefined)
273
+ return yield* Effect5.fail(invalid);
274
+ return diffMeshNode(news, output, yield* accountId);
275
+ })),
276
+ read: ({ olds, output }) => run(Effect5.flatMap(accountId, (account) => readMeshNode(account, olds, output))),
277
+ reconcile: ({ news, output }) => run(Effect5.flatMap(accountId, (account) => reconcileMeshNode(account, news, output))),
278
+ delete: ({ output }) => run(deleteMeshNode(output))
279
+ });
280
+ }));
281
+ // src/cloudflare/mesh-node-token.ts
282
+ import { Credentials as Credentials2 } from "@distilled.cloud/cloudflare/Credentials";
283
+ import * as zeroTrust2 from "@distilled.cloud/cloudflare/zero-trust";
284
+ import * as Effect6 from "effect/Effect";
285
+ import * as Redacted3 from "effect/Redacted";
286
+ var refuseUnsafe = Effect6.gen(function* () {
287
+ if ((globalThis.process?.env?.["DISTILLED_DEBUG_HTTP"] ?? "") !== "") {
288
+ return yield* Effect6.fail(new MeshNodeError({
289
+ message: "Refusing to fetch a Mesh node token while DISTILLED_DEBUG_HTTP is set: the SDK would print the token to stderr. Unset it and run again."
290
+ }));
291
+ }
292
+ const credentials = yield* yield* Credentials2;
293
+ if (credentials.type === "apiKey") {
294
+ return yield* Effect6.fail(new MeshNodeError({
295
+ message: 'Refusing the Global API Key for a Mesh node token. Mint a short-lived API token with "Cloudflare One Connectors Write" and pass it with fromApiToken.'
296
+ }));
297
+ }
298
+ if (credentials.type === "apiToken" && Redacted3.value(credentials.apiToken).length === 0) {
299
+ return yield* Effect6.fail(new MeshNodeError({
300
+ message: "The Cloudflare API token is EMPTY. That is a missing or denied secrets grant, not a Cloudflare permission: re-mint the token before changing any grant."
301
+ }));
302
+ }
303
+ });
304
+ var resolveId = (ref) => {
305
+ if (ref.id !== undefined)
306
+ return Effect6.succeed(ref.id);
307
+ const name = ref.name;
308
+ return Effect6.flatMap(findNodeByName(ref.accountId, name), (node) => node === undefined ? Effect6.fail(new MeshNodeError({ message: `No live Mesh node named "${name}".` })) : Effect6.succeed(node.id));
309
+ };
310
+ var fetchMeshNodeToken = (ref) => Effect6.gen(function* () {
311
+ yield* refuseUnsafe;
312
+ const nodeId = yield* resolveId(ref);
313
+ const token = yield* zeroTrust2.getTunnelWarpConnectorToken({ accountId: ref.accountId, tunnelId: nodeId }).pipe(Effect6.catchTag("TunnelNotFound", () => Effect6.fail(new MeshNodeError({
314
+ message: `Mesh node ${nodeId} does not exist in account ${ref.accountId}.`
315
+ }))), Effect6.catchTag("Forbidden", () => Effect6.fail(new MeshNodeError({
316
+ message: `Reading Mesh node ${nodeId}'s token was refused (403). The endpoint needs a Write permission such as "Cloudflare One Connectors Write", on a token scoped to account ${ref.accountId}.`
317
+ }))));
318
+ if (typeof token !== "string" || token.length === 0) {
319
+ return yield* Effect6.fail(new MeshNodeError({ message: `Mesh node ${nodeId} returned an empty token.` }));
320
+ }
321
+ return Redacted3.make(token);
322
+ });
323
+ // src/cloudflare/providers.ts
324
+ import * as Cloudflare3 from "alchemy/Cloudflare";
325
+ import * as Provider3 from "alchemy/Provider";
119
326
  import * as Layer2 from "effect/Layer";
120
- class Providers extends Provider2.ProviderCollection()("HomeflareCloudflare") {
327
+ class Providers extends Provider3.ProviderCollection()("HomeflareCloudflare") {
121
328
  }
122
- var providers = () => Layer2.effect(Providers, Provider2.collection([R2BucketLock])).pipe(Layer2.provide(R2BucketLockProvider()), Layer2.provideMerge(CloudflareApiLive));
329
+ var providers = () => Layer2.effect(Providers, Provider3.collection([R2BucketLock, MeshNode])).pipe(Layer2.provide(Layer2.mergeAll(R2BucketLockProvider(), MeshNodeProvider().pipe(Layer2.provide(Cloudflare3.CloudflareApiLive()), Layer2.orDie))), Layer2.provideMerge(CloudflareApiLive));
123
330
  // src/cloudflare/website.ts
124
331
  import * as AdoptPolicy from "alchemy/AdoptPolicy";
125
- import * as Cloudflare2 from "alchemy/Cloudflare";
332
+ import * as Cloudflare4 from "alchemy/Cloudflare";
126
333
  import * as RemovalPolicy from "alchemy/RemovalPolicy";
127
334
  var ASTRO_FLAGS = ["nodejs_compat", "disable_nodejs_process_v2"];
128
335
  function astroWebsite(name, props) {
@@ -133,18 +340,22 @@ function astroWebsite(name, props) {
133
340
  sessionKVBindingName: props.sessionKVBindingName ?? false,
134
341
  compatibility: { ...props.compatibility, flags: [...flags] }
135
342
  };
136
- return Cloudflare2.Website.Astro(name, merged).pipe(AdoptPolicy.adopt(true), RemovalPolicy.retain());
343
+ return Cloudflare4.Website.Astro(name, merged).pipe(AdoptPolicy.adopt(true), RemovalPolicy.retain());
137
344
  }
138
345
  function viteWebsite(name, props) {
139
346
  const merged = { ...props, name: props?.name ?? name };
140
- return Cloudflare2.Website.Vite(name, merged).pipe(AdoptPolicy.adopt(true), RemovalPolicy.retain());
347
+ return Cloudflare4.Website.Vite(name, merged).pipe(AdoptPolicy.adopt(true), RemovalPolicy.retain());
141
348
  }
142
349
  export {
350
+ MeshNode,
351
+ MeshNodeError,
352
+ MeshNodeProvider,
143
353
  R2BucketLock,
144
354
  astroWebsite,
355
+ fetchMeshNodeToken,
145
356
  providers,
146
357
  viteWebsite
147
358
  };
148
359
 
149
- //# debugId=715592491E7374E164756E2164756E21
360
+ //# debugId=8A155CE09F54FFB164756E2164756E21
150
361
  //# sourceMappingURL=index.js.map
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/cloudflare/r2-bucket-lock.ts", "../src/cloudflare/client.ts", "../src/cloudflare/lock-rules.ts", "../src/cloudflare/providers.ts", "../src/cloudflare/website.ts"],
3
+ "sources": ["../src/cloudflare/r2-bucket-lock.ts", "../src/cloudflare/client.ts", "../src/cloudflare/lock-rules.ts", "../src/cloudflare/mesh-node.ts", "../src/cloudflare/mesh-node-api.ts", "../src/cloudflare/mesh-node-form.ts", "../src/cloudflare/mesh-node-lifecycle.ts", "../src/cloudflare/mesh-node-token.ts", "../src/cloudflare/providers.ts", "../src/cloudflare/website.ts"],
4
4
  "sourcesContent": [
5
5
  "import { Unowned } from 'alchemy/AdoptPolicy';\nimport * as Provider from 'alchemy/Provider';\nimport { Resource, type ResourceClass } from 'alchemy/Resource';\n/**\n * `Cloudflare.R2BucketLock` — the lock rule set on one R2 bucket, declared rather than applied by\n * hand. A bucket lock is a RETENTION FLOOR: while a rule covers an object, no API call, no\n * lifecycle rule and no credential can delete it.\n *\n * 🔴 WHY THIS EXISTS AT ALL, AND WHAT IT REPLACES. a consuming stack shipped the retention floor as a\n * documented `wrangler r2 bucket lock add …` step in the runbook, on the measured ground that\n * `alchemy@2.0.0-beta.77` has no lock property anywhere in its R2 namespace. That measurement is\n * still true and it was the wrong conclusion: a step a human runs once is a step an Alchemy plan\n * can never show missing, so removing the floor by hand leaves that stack, its plan and\n * every gate completely green. The operator's rule (2026-09-15) is to cover such a gap with a\n * custom resource over the vendor's SDK, which is what this is.\n *\n * ⛔ THE SDK ALREADY HAD IT, AND THAT IS WORTH SAYING BECAUSE IT DECIDED THE SHAPE. Measured\n * 2026-09-15 in `cloudflare@4.5.0`, `resources/r2/buckets/locks.d.ts`:\n *\n * update(bucketName: string, params: LockUpdateParams, options?): APIPromise<…>\n * get(bucketName: string, params: LockGetParams, options?): APIPromise<LockGetResponse>\n *\n * over `PUT`/`GET /accounts/{account_id}/r2/buckets/{bucket_name}/lock` with the jurisdiction\n * carried as the `cf-r2-jurisdiction` header (`locks.mjs:17` and `:41`). So there is no raw\n * `client.put` fallback here and no path string this package invented — the fallback the rule\n * allows was not needed.\n *\n * ⚠️ THE RULE SET IS A REPLACE, NOT A MERGE. `PUT` takes the WHOLE list, exactly like R2's\n * lifecycle rules — which is the trap a consuming stack records for\n * `homeflare-git`. Declaring this resource on a bucket whose live rules nobody has read would\n * drop them, so `read` fetches them first and `reconcile` refuses to write when they already\n * match.\n */\nimport type Cloudflare from 'cloudflare';\nimport { NotFoundError } from 'cloudflare';\nimport * as Effect from 'effect/Effect';\nimport { CloudflareApi } from './client.ts';\nimport {\n type Jurisdiction,\n type R2LockRule,\n jurisdictionOf,\n rulesEqual,\n toBody,\n} from './lock-rules.ts';\nimport type { Providers } from './providers.ts';\n\nexport interface R2BucketLockProps {\n readonly accountId: string;\n readonly bucketName: string;\n /** ⚠️ Part of the bucket's IDENTITY, not a preference — a bucket in another jurisdiction is another bucket. */\n readonly jurisdiction?: Jurisdiction;\n /**\n * ⛔ REQUIRED, AND ITS EMPTY VALUE IS THE DANGEROUS ONE. `rules: []` means NO FLOOR: it is what\n * `delete` sends to take a lock off deliberately. An optional property would let a declaration\n * that simply forgot it send `[]` and silently unlock the repository, reporting success — the\n * same reasoning `LiteLLM.McpServer.allowedTools` is required for.\n */\n readonly rules: readonly R2LockRule[];\n}\n\nexport type R2BucketLockAttributes = {\n readonly bucketName: string;\n readonly jurisdiction: Jurisdiction;\n readonly rules: readonly R2LockRule[];\n};\n\nexport interface R2BucketLock extends Resource<\n 'Cloudflare.R2BucketLock',\n R2BucketLockProps,\n R2BucketLockAttributes,\n never,\n Providers\n> {}\n\n// ⚠️ EXPLICIT ANNOTATION, REQUIRED BY isolatedDeclarations. This file was internal to a\n// monorepo before it was published; a published package emits its .d.ts from a separate\n// tsc pass, so every exported symbol must state its type rather than infer it.\nexport const R2BucketLock: ResourceClass<R2BucketLock> = Resource<R2BucketLock>(\n 'Cloudflare.R2BucketLock',\n {\n /**\n * ⛔ RETAIN, AND ON THIS RESOURCE IT IS NOT THE ESTATE'S HABIT BUT THE WHOLE POINT. A lock is a\n * safety floor: removing the declaration must not remove the floor, because the state it\n * protects is the only physical backup of the estate's one Postgres. `delete` below is\n * implemented — a provider that cannot delete cannot be tested or corrected — and it is\n * reachable only by an operator running the CLI directly, past a deploy wrapper.\n */\n defaultRemovalPolicy: 'retain',\n },\n);\n\n/**\n * 🔴 A 404 HERE IS \"NOTHING TO ADOPT\", AND THE FIRST PLAN PROVED IT IS THE ORDINARY CASE.\n * Measured 2026-09-15, an alchemy plan --detailed on a bucket that does not\n * exist yet: `404 {\"success\":false,\"errors\":[{\"code\":10006,\"message\":\"The specified bucket does\n * not exist.\"}]}` — raised from `read`, during PLANNING, before anything is created. Alchemy\n * calls `read` for a resource with no prior state, so on every first deploy the lock is asked\n * about a bucket the same plan is about to create. Letting that throw makes the resource\n * impossible to plan; swallowing it with `catchAll` would also hide a 403 from a mis-scoped\n * token, which is the failure this estate spends the most time on. So: 404 → absent, everything\n * else propagates.\n *\n * ⛔ AN EMPTY LIVE RULE SET IS \"NO LOCK\", NOT \"A LOCK WITH NOTHING IN IT\", so it reads as absent\n * and the resource CREATES. The API answers `{}` for a bucket that has never been locked and\n * `{ rules: [] }` for one whose rules were removed; both mean the floor is gone.\n * ⚠️ `Unowned` UNTIL THIS STACK HAS WRITTEN IT. A lock someone applied by hand — which is exactly\n * what the retired runbook step did — must not be silently rewritten by a mistyped declaration;\n * the planner refuses until the resource is piped through `adopt(true)`, which is one line an\n * operator reads in the diff. An adopted rule set that already MATCHES then costs no request at\n * all, because `reconcile` below returns before writing.\n */\nexport const readLock = (\n client: Cloudflare,\n props: { accountId: string; bucketName: string; jurisdiction?: Jurisdiction },\n written: boolean,\n): Effect.Effect<R2BucketLockAttributes | undefined> =>\n Effect.gen(function* () {\n const jurisdiction = jurisdictionOf(props);\n const live = yield* Effect.tryPromise({\n try: () =>\n client.r2.buckets.locks.get(props.bucketName, {\n account_id: props.accountId,\n jurisdiction,\n }),\n catch: (cause) => cause,\n }).pipe(\n Effect.catchIf(\n (cause) => cause instanceof NotFoundError,\n () => Effect.succeed(undefined),\n ),\n Effect.orDie,\n );\n if (live === undefined) return undefined;\n const rules = (live.rules ?? []) as readonly R2LockRule[];\n if (rules.length === 0) return undefined;\n const attributes: R2BucketLockAttributes = {\n bucketName: props.bucketName,\n jurisdiction,\n rules,\n };\n return written ? attributes : Unowned(attributes);\n });\n\n/**\n * ⛔ A NO-OP MAKES NO REQUEST, AND ON A LOCK THAT MATTERS MORE THAN TIDINESS. `PUT` replaces the\n * rule set; re-sending an identical one on every deploy is a write against the only thing\n * standing between this repository and a delete, for no change. `output` is what the last deploy\n * persisted (or what `read` just adopted), so the comparison costs nothing either.\n */\nexport const reconcileLock = (\n client: Cloudflare,\n news: R2BucketLockProps,\n output: R2BucketLockAttributes | undefined,\n): Effect.Effect<R2BucketLockAttributes> =>\n Effect.gen(function* () {\n const jurisdiction = jurisdictionOf(news);\n if (output !== undefined && rulesEqual(output.rules, news.rules)) return output;\n yield* Effect.promise(() =>\n client.r2.buckets.locks.update(news.bucketName, {\n account_id: news.accountId,\n jurisdiction,\n ...toBody(news.rules),\n }),\n );\n return { bucketName: news.bucketName, jurisdiction, rules: news.rules };\n });\n\n/**\n * ⛔ DELETING THIS RESOURCE MEANS PUTTING AN EMPTY RULE SET, WHICH IS UNLOCKING THE BUCKET. It is\n * implemented because the retirement path is real — R2 refuses to empty a bucket while any lock\n * rule is configured, so retiring the repository has to start here — and it is behind\n * `defaultRemovalPolicy: 'retain'` and the estate CLI's refusal of `destroy` precisely because\n * no ordinary deploy should ever reach it.\n */\nexport const deleteLock = (\n client: Cloudflare,\n output: R2BucketLockAttributes,\n accountId: string,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n yield* Effect.promise(() =>\n client.r2.buckets.locks.update(output.bucketName, {\n account_id: accountId,\n jurisdiction: output.jurisdiction,\n rules: [],\n }),\n );\n });\n\nexport const R2BucketLockProvider = () =>\n Provider.effect(\n R2BucketLock,\n Effect.gen(function* () {\n const client = yield* CloudflareApi;\n return R2BucketLock.Provider.of({\n /**\n * ⛔ EMPTY, AND ALCHEMY'S OWN CONTRACT ASKS FOR EXACTLY THAT. `ProviderService.list`:\n * \"Resources with no native enumeration API (… sub-resources keyed entirely by a parent)\n * should return an empty array rather than throwing.\" There is no list-locks endpoint —\n * a lock is reachable only through the bucket that owns it — so enumerating would mean\n * listing every bucket in the account and GETting each one's lock.\n * ⛔⛔ AND `nuke.singleton` IS WHY THAT IS NOT MERELY UNAVAILABLE BUT UNWANTED. `list`\n * feeds `alchemy unsafe nuke`, which lists and then deletes; `delete` here is an UNLOCK.\n * A working enumeration would hand one command the ability to strip the retention floor\n * off every bucket in the account — the precise event a floor exists to make impossible.\n * `singleton` is the documented word for it: always-present configuration whose delete\n * resets rather than removes.\n */\n list: () => Effect.succeed([]),\n nuke: { singleton: true },\n read: Effect.fn(function* ({ olds, output }) {\n const props = olds ?? undefined;\n if (props === undefined) return undefined;\n return yield* readLock(client, props, output !== undefined);\n }),\n reconcile: Effect.fn(({ news, output }) => reconcileLock(client, news, output)),\n delete: Effect.fn(({ olds, output }) => deleteLock(client, output, olds.accountId)),\n });\n }),\n );\n",
6
6
  "/**\n * The Cloudflare REST API as an Effect service, over `cloudflare-typescript`. Codex standard 5:\n * the vendor's SDK, never a hand-rolled client — and never a hand-rolled `fetch` either.\n *\n * ⛔ THE SDK WAS ALREADY IN THE STORE AND DECLARED BY NOBODY. Measured 2026-09-15:\n * `node_modules/.pnpm/cloudflare@4.5.0` exists as a transitive dependency of `alchemy`, and\n * `pnpm ls cloudflare --depth 0` at the root and in every workspace package returns nothing.\n * `house/local/scripts/vendor-sdk-ratchet.ts` row R2 says exactly that — \"not declared anywhere\n * in the workspace yet … the row adds the dependency once, then converts the TS callers\" — so\n * this package is that row's first half, and the twenty ratcheted offences keep their rows.\n *\n * ⛔ AND IT IS PINNED EXACTLY, NOT `catalog:`. There is no catalog entry for `cloudflare`, and the\n * version that resolves today is the one alchemy pulls in. Pinning `4.5.0` means a future\n * alchemy bump moves alchemy's copy and not this package's contract — the alternative is a\n * provider whose request shape changes because an unrelated dependency was upgraded.\n *\n * ⚠️ `fetch` IS A SEAM, NOT A CONVENIENCE — the same argument `packages/litellm-client` makes. The\n * SDK hands its custom fetch the URL and init it built, so a test double conforming to\n * `Core.Fetch` exercises the real path assembly, the real body serialisation and the real header\n * set. A test that stubbed `client.r2.buckets.locks.update` would prove only that a stub was\n * called, which is precisely the class of check this estate keeps finding green over nothing.\n */\nimport Cloudflare from 'cloudflare';\nimport * as Config from 'effect/Config';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\n\n/** What the SDK accepts for `fetch`, restated so tests can name it without importing SDK internals. */\nexport type ClientFetch = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\nexport class CloudflareApi extends Context.Service<CloudflareApi, Cloudflare>()(\n 'homeflare/CloudflareApi',\n) {}\n\n/**\n * ⚠️ THE FAKE FETCH IS THE ONLY REASON THIS TAKES OPTIONS AT ALL. Everything else the SDK reads\n * from the environment the estate CLI already populates.\n */\nexport const makeClient = (options: { apiToken: string; fetch?: ClientFetch }): Cloudflare =>\n new Cloudflare({\n apiToken: options.apiToken,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n\n/**\n * ⛔ FAIL CLOSED ON THE VALUE, NEVER ON THE VARIABLE EXISTING. AGENTS.md \"the four that cost the\n * most\" §4: a denied OpenBao path renders the template EMPTY rather than erroring, and every\n * Cloudflare call then answers 403 exactly like a revoked token — which sends the operator to\n * Cloudflare to rotate a credential that was fine. Your deploy wrapper should write\n * CLOUDFLARE_API_TOKEN from the stack's declared role, short-lived and scoped.\n */\nconst MISSING_TOKEN =\n 'CLOUDFLARE_API_TOKEN is not set. Mint a short-lived, scoped token and export it before ' +\n 'role named in estate.stack.ts; running `alchemy` directly skips that and there is no default.';\n\nconst EMPTY_TOKEN =\n 'CLOUDFLARE_API_TOKEN is set but EMPTY. That is a denied or missing OpenBao grant on the ' +\n 'stack’s role, not a rotated token — re-mint it rather than rotating anything at Cloudflare.';\n\n/**\n * ⛔ A LAYER'S ERROR CHANNEL MUST BE `never`, so a ConfigError becomes a defect carrying a sentence\n * rather than a type error pushed up into `Stack`. Same decision, same words, as `LiteLLMLive`.\n */\nexport const CloudflareApiLive = Layer.effect(\n CloudflareApi,\n Effect.gen(function* () {\n const token = yield* Config.Redacted('CLOUDFLARE_API_TOKEN').pipe(\n Effect.catchTag('ConfigError', () => Effect.die(new Error(MISSING_TOKEN))),\n );\n const value = Redacted.value(token);\n if (value.length === 0) return yield* Effect.die(new Error(EMPTY_TOKEN));\n return makeClient({ apiToken: value });\n }),\n);\n",
7
7
  "/**\n * The vocabulary of an R2 lock rule, and the comparison that decides whether to write one.\n *\n * ★ ITS OWN MODULE FOR THE REASON `house/litellm/src/equal.ts` IS ONE: the comparison is the\n * riskiest function in the provider and it deserves to be readable and testable without the\n * lifecycle around it. An equality that returned true for everything would make every reconcile\n * a no-op and the resource would silently never write anything — which looks exactly like a\n * converged stack.\n */\n/** Jurisdictions the API accepts, as the SDK declares them. */\nexport type Jurisdiction = 'default' | 'eu' | 'fedramp';\n\n/**\n * One lock rule. ⚠️ `prefix` OMITTED MEANS THE WHOLE BUCKET — the SDK documents an empty prefix as\n * the way to scope a rule to every object, and omitting the key is how that is spelled on the wire.\n */\nexport type R2LockRule = {\n readonly id: string;\n readonly enabled: boolean;\n readonly prefix?: string;\n readonly condition:\n | { readonly type: 'Age'; readonly maxAgeSeconds: number }\n | { readonly type: 'Date'; readonly date: string }\n | { readonly type: 'Indefinite' };\n};\n\n/** ⚠️ `undefined` and `'default'` are the same jurisdiction; normalise so a diff never sees both. */\nexport const jurisdictionOf = (props: { readonly jurisdiction?: Jurisdiction }): Jurisdiction =>\n props.jurisdiction ?? 'default';\n\n/**\n * ⛔ FIELD BY FIELD, IN ORDER, AND NOT `JSON.stringify` OF EITHER SIDE. The API returns rules in\n * its own key order and omits `prefix` where this repo may spell it `''`; a stringify compare\n * would report drift on every plan and PUT an identical body each deploy. Order across the list\n * IS compared, because the rule set is a replace and the list is what gets sent.\n */\nexport function rulesEqual(a: readonly R2LockRule[], b: readonly R2LockRule[]): boolean {\n if (a.length !== b.length) return false;\n return a.every((left, index) => {\n const right = b[index];\n if (right === undefined) return false;\n if (left.id !== right.id || left.enabled !== right.enabled) return false;\n if ((left.prefix ?? '') !== (right.prefix ?? '')) return false;\n const [lc, rc] = [left.condition, right.condition];\n if (lc.type !== rc.type) return false;\n if (lc.type === 'Age') return rc.type === 'Age' && lc.maxAgeSeconds === rc.maxAgeSeconds;\n if (lc.type === 'Date') return rc.type === 'Date' && lc.date === rc.date;\n return true;\n });\n}\n\n/**\n * The body the SDK sends. Split out so the test can assert the wire shape against one function.\n *\n * ⚠️ IT TAKES THE RULES, NOT THE PROPS, so this module names nothing from the resource — the\n * comparison and the vocabulary stay readable without the lifecycle around them.\n */\nexport const toBody = (rules: readonly R2LockRule[]): { rules: R2LockRule[] } => ({\n rules: rules.map((rule) => ({ ...rule })),\n});\n",
8
- "/**\n * The HomeFlare Cloudflare provider collection, in the shape `LiteLLM.providers()` has.\n *\n * ★ ONE LAYER, BUILT ONCE, so the single `Config.Redacted('CLOUDFLARE_API_TOKEN')` read is shared\n * by every resource in the stack rather than repeated per resource.\n *\n * ⚠️ THIS IS NOT `alchemy/Cloudflare`'s COLLECTION AND DOES NOT REPLACE IT. A stack that declares\n * both a `Cloudflare.R2.Bucket` and this lock merges the two:\n * `Layer.mergeAll(Cloudflare.providers(), HomeflareCloudflare.providers())` — the shape\n * a consuming stack names for one talking to more than one account.\n */\nimport * as Provider from 'alchemy/Provider';\nimport * as Layer from 'effect/Layer';\nimport { CloudflareApiLive } from './client.ts';\nimport { R2BucketLock, R2BucketLockProvider } from './r2-bucket-lock.ts';\n\nexport class Providers extends Provider.ProviderCollection<Providers>()('HomeflareCloudflare') {}\n\nexport const providers = () =>\n Layer.effect(Providers, Provider.collection([R2BucketLock])).pipe(\n Layer.provide(R2BucketLockProvider()),\n Layer.provideMerge(CloudflareApiLive),\n );\n",
8
+ "/**\n * `Cloudflare.MeshNode` — a Cloudflare Mesh node (a `warp_connector` object), declared without\n * its token ever touching Alchemy state. Usage and the enrolment step: docs/mesh-node.md.\n *\n * 🔴 WHY THIS EXISTS ALONGSIDE `Cloudflare.Tunnel.WarpConnector`. A Mesh node that fronts a\n * private service needs a node record in the graph, so the Gateway rules and DNS records that\n * name it deploy together. Alchemy beta.79's WarpConnector would put the node token — the\n * credential that lets any Linux host join the account's Mesh as that node — into plaintext\n * state on every read, and it cannot create an HA node. See mesh-node-form.ts.\n *\n * ★ REMOVAL POLICY: `retain` BY DEFAULT — see the ★ on `MeshNode` below, and what it does to an\n * `ha` replace in mesh-node-lifecycle.ts.\n */\nimport { Credentials } from '@distilled.cloud/cloudflare/Credentials';\nimport * as Cloudflare from 'alchemy/Cloudflare';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport { Resource, type ResourceClass } from 'alchemy/Resource';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport {\n type MeshNodeAttributes,\n type MeshNodeProps,\n diffMeshNode,\n validateMeshNode,\n} from './mesh-node-form.ts';\nimport { deleteMeshNode, readMeshNode, reconcileMeshNode } from './mesh-node-lifecycle.ts';\nimport type { Providers } from './providers.ts';\n\nexport interface MeshNode extends Resource<\n 'Cloudflare.MeshNode',\n MeshNodeProps,\n MeshNodeAttributes,\n never,\n Providers\n> {}\n\n/**\n * ★ `defaultRemovalPolicy: 'retain'`, LIKE EVERY KIT RESOURCE WHOSE DELETION BREAKS ITS CONSUMERS\n * (the `Bao.*` families, `R2BucketLock`, the Proxmox storage families; the reasoning is written\n * once in proxmox/resource.ts). Deleting a node cuts every enrolled replica off the Mesh, and\n * nothing brings it back: a new node has a new id, a new token and a new Mesh IP per replica, so\n * each host must be re-enrolled by hand and every Gateway rule or allow-list that pins the old\n * Mesh IP breaks. So dropping the declaration, or `alchemy destroy`, leaves the node live.\n * ⚠️ IT ALSO SKIPS THE OLD NODE'S DELETE IN A REPLACE (Apply.ts, `deleteOldGenerations` and the\n * GC's `retainOldGeneration`, beta.79). A same-name `ha` change therefore REFUSES, deleting\n * nothing, until that deploy opts in with `.pipe(RemovalPolicy.destroy())`; a create-first\n * replace leaves the old node live and unmanaged. Both measured in mesh-node-policy.test.ts.\n * ⛔ `delete` STAYS FULLY IMPLEMENTED. Retain is Terraform's `prevent_destroy`, not a stub: it runs\n * the moment a declaration opts in with `.pipe(RemovalPolicy.destroy())`.\n */\nexport const MeshNode: ResourceClass<MeshNode> = Resource<MeshNode>('Cloudflare.MeshNode', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** What every call below needs: the account, credentials and HTTP — Alchemy's own trio. */\nexport type MeshNodeServices =\n | Cloudflare.CloudflareEnvironment\n | Credentials\n | HttpClient.HttpClient;\n\n/** The account id, resolved the way every `alchemy/Cloudflare` provider resolves it. */\nconst accountId = Effect.gen(function* () {\n const resolved = yield* yield* Cloudflare.CloudflareEnvironment;\n return resolved.accountId;\n});\n\n/**\n * ★ `Provider.effect`, CAPTURING THE THREE SERVICES AT BUILD. `Provider.succeed` (what Alchemy's\n * own providers use) leaves each handler to find CloudflareEnvironment and Credentials in the\n * engine's context at call time — which works for `Cloudflare.providers()` only because it\n * `provideMerge`s them into the stack. Capturing them here keeps this provider correct whether\n * or not the stack also merges `Cloudflare.providers()`.\n */\nexport const MeshNodeProvider = () =>\n Provider.effect(\n MeshNode,\n Effect.gen(function* () {\n const services = Context.make(\n Cloudflare.CloudflareEnvironment,\n yield* Cloudflare.CloudflareEnvironment,\n ).pipe(\n Context.add(Credentials, yield* Credentials),\n Context.add(HttpClient.HttpClient, yield* HttpClient.HttpClient),\n );\n const run = <A, E>(effect: Effect.Effect<A, E, MeshNodeServices>) =>\n Effect.provideContext(effect, services);\n\n return MeshNode.Provider.of({\n stables: ['id', 'accountId', 'ha'],\n /**\n * ⛔ EMPTY, AND `nuke.skip`. Alchemy's own `Cloudflare.Tunnel.WarpConnector` already lists\n * every `warp_connector` in the account for `alchemy unsafe nuke`; a second listing here\n * would make nuke delete each node twice. `read` does not depend on this.\n */\n list: () => Effect.succeed([]),\n nuke: { skip: true },\n /**\n * ⛔ VALIDATE AT PLAN TIME, NOT ONLY IN `reconcile`. An `ha` replace is delete-first: the\n * engine deletes the old node BEFORE the new generation's reconcile runs, so a\n * declaration refused there would leave no node at all. launchd's job-preflight.ts\n * refuses at plan time for the same reason.\n */\n diff: ({ news, output }) =>\n run(\n Effect.gen(function* () {\n const invalid = isResolved(news) ? validateMeshNode(news) : undefined;\n if (invalid !== undefined) return yield* Effect.fail(invalid);\n return diffMeshNode(news, output, yield* accountId);\n }),\n ),\n read: ({ olds, output }) =>\n run(Effect.flatMap(accountId, (account) => readMeshNode(account, olds, output))),\n reconcile: ({ news, output }) =>\n run(Effect.flatMap(accountId, (account) => reconcileMeshNode(account, news, output))),\n delete: ({ output }) => run(deleteMeshNode(output)),\n });\n }),\n );\n",
9
+ "/**\n * The `warp_connector` calls a Cloudflare Mesh node needs, and nothing else.\n *\n * ★ A MESH NODE IS A `warp_connector` OBJECT. Cloudflare's Mesh docs create one with\n * `POST /accounts/{account_id}/warp_connector` and fetch its enrolment token from\n * `GET …/warp_connector/{id}/token` (developers.cloudflare.com/mesh/get-started, read\n * 2026-09-21). There is no separate \"mesh node\" endpoint.\n *\n * ★ OVER `@distilled.cloud/cloudflare`, THE SDK ALCHEMY'S OWN CLOUDFLARE PROVIDERS USE, not the\n * `cloudflare` npm SDK the R2 lock uses. Measured 2026-09-21: `cloudflare@4.5.0`'s\n * `WARPConnectorCreateParams` has only `account_id` and `name`, so it cannot create an HA node.\n * distilled `1.0.0-rc.12` (the version alchemy@2.0.0-beta.79 pins) has `ha?: boolean` on\n * `CreateTunnelWarpConnectorRequest`, typed errors (`TunnelNotFound` code 1002 / 404,\n * `DuplicateTunnelName` code 1013), and resolves credentials and the account exactly the way\n * `Cloudflare.Tunnel.WarpConnector` and `Cloudflare.Gateway.Rule` do — so the node and the Gateway\n * rules that pin its Mesh IP always land in the same account.\n *\n * ⛔ NO FUNCTION IN THIS FILE READS THE NODE TOKEN, and `observe` copies named fields only. The\n * HA docs say the create response carries a `token` field; distilled's decoder copies only the\n * fields in its schema (protocol.ts, `getProps(outputAst)`), and `observe` below would drop it\n * again if it ever got through. The one token read lives in mesh-node-token.ts, on demand.\n */\nimport * as zeroTrust from '@distilled.cloud/cloudflare/zero-trust';\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\n\n/** `inactive` (never run), `degraded`, `healthy` or `down` — Cloudflare's own four. */\nexport type MeshNodeStatus = 'inactive' | 'degraded' | 'healthy' | 'down';\n\n/** A live node as this package sees it. ⛔ There is no token field here, on purpose. */\nexport interface ObservedNode {\n readonly id: string;\n readonly name: string;\n readonly status: MeshNodeStatus | undefined;\n}\n\n/** A refusal or failure, carrying the sentence an operator needs. Never a token. */\nexport class MeshNodeError extends Data.TaggedError('MeshNodeError')<{\n readonly message: string;\n}> {}\n\ntype Raw = {\n readonly id?: string | null;\n readonly name?: string | null;\n readonly status?: string | null;\n readonly deletedAt?: string | null;\n};\n\nconst STATUSES: readonly string[] = ['inactive', 'degraded', 'healthy', 'down'];\n\n/**\n * ⚠️ A DELETED NODE IS NOT A NODE. Cloudflare keeps deleted tunnels (every response carries\n * `deleted_at`, and the list takes `is_deleted`), so a `GET` by id can answer for one. Treating\n * that as live would \"adopt\" a tombstone and PATCH it.\n */\nexport const observe = (raw: Raw): ObservedNode | undefined => {\n if (typeof raw.id !== 'string' || raw.id.length === 0) return undefined;\n if (raw.deletedAt !== undefined && raw.deletedAt !== null) return undefined;\n const status =\n typeof raw.status === 'string' && STATUSES.includes(raw.status)\n ? (raw.status as MeshNodeStatus)\n : undefined;\n return { id: raw.id, name: raw.name ?? '', status };\n};\n\n/** The node with this id, or `undefined` when it is gone (404 / code 1002, or soft-deleted). */\nexport const getNode = (accountId: string, nodeId: string) =>\n zeroTrust.getTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(\n Effect.map(observe),\n Effect.catchTag('TunnelNotFound', () => Effect.succeed(undefined)),\n );\n\n/**\n * The live node with exactly this name, or `undefined`.\n *\n * ★ `name` is also sent as the list filter, but the match is re-checked here: the filter's\n * exactness is not documented, and a prefix match would adopt the wrong node.\n * ⛔ Two live matches is refused rather than resolved. Names are unique per account (the create\n * answers code 1013 otherwise), so two means something this package does not understand.\n */\nexport const findNodeByName = (accountId: string, name: string) =>\n zeroTrust.listTunnelWarpConnectors.items({ accountId, name, isDeleted: false }).pipe(\n Stream.map(observe),\n Stream.filter((node): node is ObservedNode => node !== undefined && node.name === name),\n Stream.runCollect,\n Effect.flatMap((chunk) => {\n const matches = Array.from(chunk);\n if (matches.length <= 1) return Effect.succeed(matches[0]);\n const ids = matches.map((node) => node.id).join(', ');\n return Effect.fail(\n new MeshNodeError({\n message: `Mesh node name \"${name}\" matches ${matches.length} live nodes (${ids}). Names are unique per account, so refusing to guess which one is meant.`,\n }),\n );\n }),\n );\n\n/**\n * ⛔ `ha` IS SENT ON CREATE AND NOWHERE ELSE. Cloudflare's HA page: \"High availability is set at\n * node creation time and cannot be changed afterward.\" The PATCH body (`PatchTunnelWarpConnector\n * Request`) has only `name` and `tunnelSecret`.\n */\nexport const createNode = (accountId: string, name: string, ha: boolean) =>\n zeroTrust.createTunnelWarpConnector({ accountId, name, ha }).pipe(\n Effect.flatMap((raw) => {\n const node = observe(raw);\n return node === undefined\n ? Effect.fail(\n new MeshNodeError({ message: `Creating Mesh node \"${name}\" returned no id.` }),\n )\n : Effect.succeed(node);\n }),\n );\n\n/** ★ A rename is a PATCH: same id, same token, same enrolled replicas and Mesh IP. */\nexport const renameNode = (accountId: string, nodeId: string, name: string) =>\n zeroTrust\n .patchTunnelWarpConnector({ accountId, tunnelId: nodeId, name })\n .pipe(Effect.map((raw) => observe(raw) ?? { id: nodeId, name, status: undefined }));\n\n/** Idempotent: a node already gone (404 / code 1002) is a successful delete. */\nexport const deleteNode = (accountId: string, nodeId: string) =>\n zeroTrust.deleteTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(\n Effect.asVoid,\n Effect.catchTag('TunnelNotFound', () => Effect.void),\n );\n",
10
+ "/**\n * `Cloudflare.MeshNode` — its props, its attributes, and the pure `diff` that decides between\n * update and replace.\n *\n * ⛔ THE ATTRIBUTES NEVER HOLD THE NODE TOKEN. That is the whole reason this resource exists:\n * Alchemy beta.79's `Cloudflare.Tunnel.WarpConnector` calls `getTunnelWarpConnectorToken` in\n * `toAttributes`, so EVERY read, reconcile and list fetches the token, and Alchemy state (which\n * is not encrypted) stores it — `Redacted` hides it from logs, not from the state store. It also\n * has no `ha` prop. The token is a runtime credential: fetch it on demand with\n * `fetchMeshNodeToken` (mesh-node-token.ts) and write it straight to the host.\n * ★ A BONUS OF NOT READING IT: the token endpoint needs a Write permission (\"Cloudflare One\n * Connectors Write\", per its API reference, read 2026-09-21), so a plan over this resource can\n * run on a read-only token. WarpConnector's cannot.\n */\nimport type { Diff } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport { MeshNodeError, type MeshNodeStatus } from './mesh-node-api.ts';\n\nexport interface MeshNodeProps {\n /**\n * The node's name in the account — and its identity when adopting (names are unique per\n * account). ★ Changing it RENAMES the node in place (`PATCH`): the id, the token, the\n * enrolled replicas and their Mesh IPs all survive, so a rename never re-enrols a host.\n */\n readonly name: string;\n /**\n * High availability (active-passive replicas sharing one node token).\n *\n * ⛔ CREATE-ONLY, AND REQUIRED FOR THAT REASON. Cloudflare's Mesh HA page: \"High availability\n * is set at node creation time and cannot be changed afterward.\" The PATCH body carries no\n * `ha`. So a change REPLACES the node — a new id, a new token, a new Mesh IP per replica —\n * and a default would silently fix an irreversible choice. The API's own default is `false`;\n * the dashboard's is `true`. Say which one you mean.\n * ★ A NODE WITH NO ROUTES — A DOOR — IS `ha: false`, AND A SECOND DOOR IS A SECOND NODE. HA\n * fails over the CIDR routes a node advertises; \"Nodes without routes do not benefit from HA\n * failover\", because the node's Mesh IP is tied to the individual replica (same page, \"When to\n * use\" and \"Considerations\", read 2026-09-21). After a failover the promoted replica answers\n * on its own Mesh IP, so nothing that dials a door's first replica follows it. Declare the\n * second door as its own `MeshNode` (own name, token, Mesh IP) and have callers list both.\n * ⚠️ `ha` NEEDS MASQUE on the node's device profile (same page).\n */\n readonly ha: boolean;\n}\n\nexport interface MeshNodeAttributes {\n /** The node's UUID (the `warp_connector` tunnel id). Pass it to `fetchMeshNodeToken`. */\n readonly id: string;\n /** The account the provider environment resolved when the node was written. */\n readonly accountId: string;\n readonly name: string;\n /** As of the last read or write; it moves on its own as replicas connect and drop. */\n readonly status: MeshNodeStatus | undefined;\n /**\n * The value the node was CREATED with (or declared with when adopted — see mesh-node.md).\n * ⚠️ No documented read returns it: `GET /warp_connector/{id}` has no `ha` field (API\n * reference, read 2026-09-21), so drift here cannot be observed, only prevented.\n */\n readonly ha: boolean;\n}\n\n/** ⛔ Fail closed on a declaration the API would accept but nobody meant. */\nexport const validateMeshNode = (props: MeshNodeProps): MeshNodeError | undefined => {\n if (typeof props.name !== 'string' || props.name.trim().length === 0) {\n return new MeshNodeError({ message: 'MeshNode `name` must be a non-empty string.' });\n }\n if (props.name !== props.name.trim()) {\n return new MeshNodeError({\n message: `MeshNode name \"${props.name}\" has leading or trailing whitespace; adoption matches names exactly.`,\n });\n }\n if (typeof props.ha !== 'boolean') {\n return new MeshNodeError({\n message: 'MeshNode `ha` must be true or false. It is create-only, so it has no default.',\n });\n }\n return undefined;\n};\n\n/**\n * update or replace, from the declaration and what was last written.\n *\n * - Account changed → `replace`, create-first: names are unique per ACCOUNT, so the two\n * generations cannot collide.\n * - `ha` changed → `replace`. ⛔ `deleteFirst` when the name stays the same: the new node needs\n * the name the old one holds, so create-first would fail with code 1013 (or, worse, a\n * create path that \"converged\" on the old node would record an `ha` it does not have).\n * Create-first when the name changes in the same edit, because then nothing collides.\n * ⚠️ UNDER THE DEFAULT `retain` (mesh-node.ts) the engine skips the old node's delete in both\n * orders: delete-first then REFUSES at the create (mesh-node-lifecycle.ts) until the deploy\n * opts in with `RemovalPolicy.destroy()`, and create-first leaves the old node live, unmanaged.\n * The answer here is the same under both policies: `diff` is not handed the policy (Provider.ts,\n * its input), and delete-first is exactly what lets the opted-in deploy reuse the name.\n * - Name changed alone → `update` (a PATCH rename).\n * - Otherwise → no opinion; the engine compares props.\n *\n * ★ An unresolved `ha` gives no answer here; `reconcile` re-checks it once resolved and refuses\n * rather than updating across a create-only change. An unresolved `name` next to a changed `ha`\n * counts as \"unchanged\", which picks the delete-first order that cannot collide.\n */\nexport const diffMeshNode = (\n news: Input<MeshNodeProps>,\n output: MeshNodeAttributes | undefined,\n accountId: string,\n): Diff | undefined => {\n if (output === undefined) return undefined;\n if (output.accountId !== accountId) return { action: 'replace' };\n const fields = news as { readonly name?: unknown; readonly ha?: unknown };\n const name = typeof fields.name === 'string' ? fields.name : undefined;\n if (typeof fields.ha === 'boolean' && fields.ha !== output.ha) {\n const sameName = name === undefined || name === output.name;\n return sameName ? { action: 'replace', deleteFirst: true } : { action: 'replace' };\n }\n if (name !== undefined && name !== output.name) return { action: 'update' };\n return undefined;\n};\n",
11
+ "/**\n * read / reconcile / delete for `Cloudflare.MeshNode`, as plain Effects over the SDK calls in\n * mesh-node-api.ts. mesh-node.ts only wires them into Alchemy.\n */\nimport { Unowned } from 'alchemy/AdoptPolicy';\nimport * as Effect from 'effect/Effect';\nimport {\n MeshNodeError,\n type ObservedNode,\n createNode,\n deleteNode,\n findNodeByName,\n getNode,\n renameNode,\n} from './mesh-node-api.ts';\nimport { type MeshNodeAttributes, type MeshNodeProps, validateMeshNode } from './mesh-node-form.ts';\n\nconst attributesOf = (node: ObservedNode, accountId: string, ha: boolean): MeshNodeAttributes => ({\n id: node.id,\n accountId,\n name: node.name,\n status: node.status,\n ha,\n});\n\n/**\n * Owned: refresh by the stored id. Cold: find the live node by exact name and hand it back\n * `Unowned`, so Alchemy refuses to take it over until the stack says `adopt(true)` — the same\n * gate `Cloudflare.Tunnel.WarpConnector` uses, because a `warp_connector` carries no ownership\n * marker to prove it was ours.\n *\n * ⚠️ ADOPTION RECORDS THE DECLARED `ha`, BECAUSE NOTHING ELSE CAN. No documented read returns a\n * node's HA flag. Adopting an existing node with the wrong `ha` records the wrong value, and no\n * later plan can notice. Check the node's HA badge in the dashboard before adopting.\n */\nexport const readMeshNode = (\n accountId: string,\n olds: MeshNodeProps | undefined,\n output: MeshNodeAttributes | undefined,\n) =>\n Effect.gen(function* () {\n const account = output?.accountId ?? accountId;\n if (output !== undefined) {\n const node = yield* getNode(account, output.id);\n if (node !== undefined) return attributesOf(node, account, output.ha);\n }\n const name = olds?.name ?? output?.name;\n const ha = olds?.ha ?? output?.ha;\n if (name === undefined || ha === undefined) return undefined;\n const match = yield* findNodeByName(account, name);\n return match === undefined ? undefined : Unowned(attributesOf(match, account, ha));\n });\n\n/**\n * ⛔ NEVER CONVERGE ON A NODE THIS RESOURCE DID NOT FIND BY ITS OWN ID. Alchemy's WarpConnector\n * answers `DuplicateTunnelName` by re-reading the node that holds the name and returning it.\n * Here that would be wrong in exactly the case that matters: an `ha` replace whose old node is\n * still alive (the default `retain` skips the delete-first teardown, mesh-node.ts) would\n * \"create\" by silently re-using the old node and record the new `ha` on it. Adoption belongs to\n * `read` and `adopt(true)`. An interrupted create is recovered through `read` too, which returns\n * the node `Unowned`, so the engine asks for `adopt(true)` there as well (as for WarpConnector).\n */\nconst nameTaken = (name: string, holder: string) =>\n new MeshNodeError({\n message:\n `A tunnel named \"${name}\" already exists (${holder}). Names are unique per account, and ` +\n 'this resource never takes over a node it did not create by its own id.',\n });\n\n/**\n * ★ HOW TO TAKE A NODE OVER WHILE THIS RESOURCE HAS A STATE ROW, WRITTEN ONCE. The plan asks\n * `read` to adopt only for a resource with NO row (Plan.ts, `oldState === undefined`) or for an\n * interrupted create's `creating` row. A replace's `replacing` row gets neither, so `adopt(true)`\n * alone changes nothing there and the create refuses again (measured in mesh-node-policy.test.ts).\n * `alchemy state rm` is Alchemy's \"Delete state records without deleting cloud resources\".\n */\nconst REOWN =\n \"drop this resource's state row (alchemy state rm, which leaves the node alone), then deploy \" +\n 'under adopt(true)';\n\n/**\n * 🔴 THE REFUSAL AN `ha` CHANGE MEETS UNDER THE DEFAULT, SO IT NAMES EVERY WAY ON. The plan says\n * `replace` (delete-first), the engine skips that delete because of `retain`, and the old node\n * still holds the name here. A node made outside the stack is caught earlier, by the plan's\n * adoption probe in `read`.\n * ⛔ BUT THE HOLDER IS NOT ALWAYS THE OLD NODE, AND `create` CANNOT TELL: the engine passes it no\n * `olds` (Apply.ts, the replace branch). An `ha` + `name` change is a create-first replace, and\n * there the holder of the NEW name is some other node, which `RemovalPolicy.destroy()` never\n * touches. So the old-node advice is conditional, and the other case says not to delete it: a\n * sentence that called any holder \"the old node\" would send an operator to delete a live node\n * and cut its replicas off the Mesh (measured 2026-09-21 in mesh-node-policy.test.ts).\n * ⚠️ REVERTING `ha` DOES NOT UNDO IT. The failed deploy leaves a `replacing` row, and the next\n * plan resumes that replace (Plan.ts, the `replacing` branch), so it refuses again. Dropping the\n * row and adopting is the way back. All measured in mesh-node-policy.test.ts.\n */\nconst oldNodeHoldsName = (name: string, holder: string) =>\n new MeshNodeError({\n message:\n `Mesh node \"${name}\" already exists (${holder}) and is not the node this resource tracks, ` +\n 'so it is left alone: names are unique per account. If this deploy changed `ha` and kept ' +\n \"the name, it is this resource's old node, kept by MeshNode's default \" +\n 'RemovalPolicy.retain because HA cannot change in place. To replace it (a new id, token and ' +\n 'Mesh IP per replica), deploy once with .pipe(RemovalPolicy.destroy()), which deletes it ' +\n `first, or delete it by hand. To keep it, reverting \\`ha\\` is not enough: ${REOWN}, with ` +\n 'the original `ha`. Otherwise it is another node: do not delete it. Choose another name, ' +\n `or, if it is meant to be this one, check its HA badge, then ${REOWN}.`,\n });\n\n/**\n * 🔴 A 1013 AFTER A CLEAN LOOKUP IS USUALLY THIS DEPLOY'S OWN NODE. distilled retries a POST on\n * a transport error or a 5xx (core `isTransientError`, measured in its source 2026-09-21), so a\n * create that succeeded but lost its response is sent again and answered 1013 by the node the\n * first attempt made. The lookup is repeated so the sentence names that node instead of blaming\n * another tunnel type; it is still not adopted here (see above), because a concurrent creator\n * looks the same and its `ha` is unknown.\n * ⚠️ `adopt(true)` ALONE ONLY RECOVERS A FIRST CREATE. Inside a replace (any `ha` change, either\n * order) the row is `replacing`, which the plan never probes, so the sentence names `REOWN` for\n * that case (measured in mesh-node-policy.test.ts).\n */\nconst raced = (accountId: string, name: string) =>\n findNodeByName(accountId, name).pipe(\n Effect.flatMap((holder) =>\n Effect.fail(\n holder === undefined\n ? // ⚠️ No live warp_connector of that name: another tunnel TYPE holds it (a cloudflared\n // tunnel), or — UNMEASURED — a node deleted moments ago (a delete-first `ha`\n // replace) still reserves it.\n nameTaken(name, 'not a live Mesh node: another tunnel type, or a just-deleted node')\n : new MeshNodeError({\n message:\n `Mesh node \"${name}\" (${holder.id}) appeared between this deploy's lookup and its ` +\n 'create. The likeliest cause is this deploy itself: the SDK retries a create whose ' +\n 'response was lost. If so, check its HA badge matches the declared `ha`, then ' +\n 'adopt it: adopt(true) after a first create, but after a replace ' +\n `${REOWN}, because adopt(true) alone does not act on a replace's row. ` +\n 'Otherwise another creator raced this one.',\n }),\n ),\n ),\n );\n\nconst createFresh = (accountId: string, news: MeshNodeProps) =>\n Effect.gen(function* () {\n const existing = yield* findNodeByName(accountId, news.name);\n if (existing !== undefined) return yield* Effect.fail(oldNodeHoldsName(news.name, existing.id));\n const node = yield* createNode(accountId, news.name, news.ha).pipe(\n Effect.catchTag('DuplicateTunnelName', () => raced(accountId, news.name)),\n );\n return attributesOf(node, accountId, news.ha);\n });\n\nexport const reconcileMeshNode = (\n accountId: string,\n news: MeshNodeProps,\n output: MeshNodeAttributes | undefined,\n) =>\n Effect.gen(function* () {\n const invalid = validateMeshNode(news);\n if (invalid !== undefined) return yield* Effect.fail(invalid);\n const owned = output !== undefined && output.accountId === accountId ? output : undefined;\n const live = owned === undefined ? undefined : yield* getNode(accountId, owned.id);\n // ★ Gone out of band (or a replace's new generation): create, under the same refusals.\n if (owned === undefined || live === undefined) return yield* createFresh(accountId, news);\n if (owned.ha !== news.ha) {\n // ⛔ The plan would have said `replace`; it can only reach here with `ha` unresolved then.\n return yield* Effect.fail(\n new MeshNodeError({\n message: `MeshNode \"${news.name}\": \\`ha\\` is create-only (stored ${owned.ha}, declared ${news.ha}). Plan again so the change is a replace.`,\n }),\n );\n }\n if (live.name === news.name) return attributesOf(live, accountId, owned.ha);\n const renamed = yield* renameNode(accountId, live.id, news.name).pipe(\n Effect.catchTag('DuplicateTunnelName', () =>\n Effect.fail(nameTaken(news.name, 'held by another tunnel')),\n ),\n );\n return attributesOf(renamed, accountId, owned.ha);\n });\n\n/**\n * ⚠️ Cloudflare may refuse to delete a node while replicas are connected (Alchemy's WarpConnector\n * notes \"Deleting requires all connector sessions to be down\"; unmeasured here). Stop the\n * client on every replica first. A node already gone is a successful delete.\n */\nexport const deleteMeshNode = (output: MeshNodeAttributes) =>\n deleteNode(output.accountId, output.id);\n",
12
+ "/**\n * `fetchMeshNodeToken` — a Mesh node's enrolment token, fetched on demand for a one-off Bun\n * `mesh-enroll` step. ⛔ Not a Resource, and never an attribute: see mesh-node-form.ts for why\n * the token must stay out of Alchemy state.\n *\n * ⛔ WHERE THE VALUE MAY GO. Straight into a root-owned `0600` file on the node's host, which the\n * enrolment reads (`warp-cli --accept-tos connector new <token>`, per the Mesh get-started page,\n * read 2026-09-21). Nowhere else: not Alchemy state, not a secrets store (a runtime credential\n * belongs to the host that runs it), not a log line, not a CI variable. It is returned\n * `Redacted`, so an accidental `console.log` or `Effect.log` prints `<redacted>`; call\n * `Redacted.value` only at the write.\n * ⚠️ `warp-cli` takes the token as an ARGUMENT, so it is visible in the host's process list for\n * the life of that command. Run the enrolment as root on the node itself, never over a shell\n * whose history is kept.\n * ⚠️ THE ENDPOINT NEEDS A WRITE PERMISSION. Its API reference (read 2026-09-21) accepts\n * \"Cloudflare One Connectors Write\", \"Cloudflare One Connector: cloudflared Write\" or\n * \"Cloudflare Tunnel Write\"; the Mesh get-started guide also names \"Cloudflare One Connector:\n * WARP Write\". Connectors Write is in both lists. Mint a short-lived token for this step alone;\n * a read-only plan token gets a 403 here.\n *\n * ★ Requires distilled `Credentials` and an `HttpClient`, not Alchemy's CloudflareEnvironment: a\n * Bun script has no Alchemy profile, so the account id is an argument. Provide\n * `Credentials.fromApiToken({ apiToken })` (from `@distilled.cloud/cloudflare/Credentials`) and\n * `FetchHttpClient.layer`. ⛔ Not `Credentials.fromEnv()`: it falls back to the Global API Key\n * (`CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL`) when no token is set — and `refuseUnsafe` below\n * refuses that key rather than trusting the caller to have read this.\n */\nimport { Credentials } from '@distilled.cloud/cloudflare/Credentials';\nimport * as zeroTrust from '@distilled.cloud/cloudflare/zero-trust';\nimport * as Effect from 'effect/Effect';\nimport * as Redacted from 'effect/Redacted';\nimport { MeshNodeError, findNodeByName } from './mesh-node-api.ts';\n\n/** Which node: by id (the `MeshNode` attribute) or by its exact name. */\nexport type MeshNodeRef = { readonly accountId: string } & (\n | { readonly id: string; readonly name?: undefined }\n | { readonly name: string; readonly id?: undefined }\n);\n\n/**\n * ⛔ REFUSED BEFORE ANY REQUEST IS SENT:\n * - `DISTILLED_DEBUG_HTTP` set. distilled's Cloudflare protocol then `console.error`s the first\n * 400 characters of EVERY response body (protocol.ts, measured in 1.0.0-rc.12) — the token\n * response is `{\"success\":true,…,\"result\":\"<token>\"}`, so the whole token lands on stderr and in\n * whatever captures it. The check reads `process.env` because that is what distilled reads.\n * - The Global API Key (`apiKey` credentials). Account-wide and long-lived: the one credential a\n * one-off enrolment must never use.\n * - An EMPTY API token. A denied secrets-store grant renders empty, and Cloudflare then answers\n * like a bad permission — which would send the operator to widen a grant that was fine\n * (client.ts says the same for `CLOUDFLARE_API_TOKEN`).\n */\nconst refuseUnsafe = Effect.gen(function* () {\n if ((globalThis.process?.env?.['DISTILLED_DEBUG_HTTP'] ?? '') !== '') {\n return yield* Effect.fail(\n new MeshNodeError({\n message:\n 'Refusing to fetch a Mesh node token while DISTILLED_DEBUG_HTTP is set: the SDK would print the token to stderr. Unset it and run again.',\n }),\n );\n }\n const credentials = yield* yield* Credentials;\n if (credentials.type === 'apiKey') {\n return yield* Effect.fail(\n new MeshNodeError({\n message:\n 'Refusing the Global API Key for a Mesh node token. Mint a short-lived API token with \"Cloudflare One Connectors Write\" and pass it with fromApiToken.',\n }),\n );\n }\n if (credentials.type === 'apiToken' && Redacted.value(credentials.apiToken).length === 0) {\n return yield* Effect.fail(\n new MeshNodeError({\n message:\n 'The Cloudflare API token is EMPTY. That is a missing or denied secrets grant, not a Cloudflare permission: re-mint the token before changing any grant.',\n }),\n );\n }\n});\n\nconst resolveId = (ref: MeshNodeRef) => {\n if (ref.id !== undefined) return Effect.succeed(ref.id);\n const name = ref.name;\n return Effect.flatMap(findNodeByName(ref.accountId, name), (node) =>\n node === undefined\n ? Effect.fail(new MeshNodeError({ message: `No live Mesh node named \"${name}\".` }))\n : Effect.succeed(node.id),\n );\n};\n\n/**\n * ⛔ An EMPTY token fails closed: `warp-cli connector new \"\"` would not enrol, and a script that\n * wrote an empty file would report success over a host that never joined.\n * ⛔ The failures made here name the node and account and quote no response body, because the\n * success body IS the token. Other SDK errors pass through; they come only from non-2xx\n * responses, which carry an error envelope, not a token.\n */\nexport const fetchMeshNodeToken = (ref: MeshNodeRef) =>\n Effect.gen(function* () {\n yield* refuseUnsafe;\n const nodeId = yield* resolveId(ref);\n const token = yield* zeroTrust\n .getTunnelWarpConnectorToken({ accountId: ref.accountId, tunnelId: nodeId })\n .pipe(\n Effect.catchTag('TunnelNotFound', () =>\n Effect.fail(\n new MeshNodeError({\n message: `Mesh node ${nodeId} does not exist in account ${ref.accountId}.`,\n }),\n ),\n ),\n Effect.catchTag('Forbidden', () =>\n Effect.fail(\n new MeshNodeError({\n message: `Reading Mesh node ${nodeId}'s token was refused (403). The endpoint needs a Write permission such as \"Cloudflare One Connectors Write\", on a token scoped to account ${ref.accountId}.`,\n }),\n ),\n ),\n );\n if (typeof token !== 'string' || token.length === 0) {\n return yield* Effect.fail(\n new MeshNodeError({ message: `Mesh node ${nodeId} returned an empty token.` }),\n );\n }\n return Redacted.make(token);\n });\n",
13
+ "/**\n * The HomeFlare Cloudflare provider collection, in the shape `LiteLLM.providers()` has.\n *\n * ★ ONE LAYER, BUILT ONCE, so the single `Config.Redacted('CLOUDFLARE_API_TOKEN')` read is shared\n * by every resource in the stack rather than repeated per resource.\n *\n * ⚠️ THIS IS NOT `alchemy/Cloudflare`'s COLLECTION AND DOES NOT REPLACE IT. A stack that declares\n * both a `Cloudflare.R2.Bucket` and this lock merges the two:\n * `Layer.mergeAll(Cloudflare.providers(), HomeflareCloudflare.providers())` — the shape\n * a consuming stack names for one talking to more than one account.\n *\n * ★ TWO CREDENTIAL PATHS, ONE PER SDK. `R2BucketLock` uses the `cloudflare` SDK through\n * `CloudflareApiLive` (`CLOUDFLARE_API_TOKEN`, fail-closed). `MeshNode` uses Alchemy's own SDK\n * and resolves credentials and the account through `Cloudflare.CloudflareApiLive()` — the same\n * source `Cloudflare.providers()` uses (an Alchemy profile, or `CLOUDFLARE_API_TOKEN` +\n * `CLOUDFLARE_ACCOUNT_ID` in CI), so a node lands in the account its Gateway rules do.\n * ⚠️ That second layer is built again here even when the stack also merges\n * `Cloudflare.providers()`. `Layer.provide`, not `provideMerge`: it feeds MeshNode only and adds\n * nothing to the stack's context.\n */\nimport * as Cloudflare from 'alchemy/Cloudflare';\nimport * as Provider from 'alchemy/Provider';\nimport * as Layer from 'effect/Layer';\nimport { CloudflareApiLive } from './client.ts';\nimport { MeshNode, MeshNodeProvider } from './mesh-node.ts';\nimport { R2BucketLock, R2BucketLockProvider } from './r2-bucket-lock.ts';\n\nexport class Providers extends Provider.ProviderCollection<Providers>()('HomeflareCloudflare') {}\n\nexport const providers = () =>\n Layer.effect(Providers, Provider.collection([R2BucketLock, MeshNode])).pipe(\n Layer.provide(\n Layer.mergeAll(\n R2BucketLockProvider(),\n // ⛔ `orDie`, as `Cloudflare.providers()` ends with: a stack's providers layer must not\n // fail, and a profile that cannot resolve is a defect carrying Alchemy's own sentence.\n MeshNodeProvider().pipe(Layer.provide(Cloudflare.CloudflareApiLive()), Layer.orDie),\n ),\n ),\n Layer.provideMerge(CloudflareApiLive),\n );\n",
9
14
  "/**\n * House defaults on Alchemy's Website stacks — Astro and Vite, which is what we ship.\n *\n * ★ NOT Nextjs. The blog measured Website.Nextjs planning as **create** because it\n * hashes source; adopt is `Worker`, not Nextjs. Do not add a Nextjs helper until an\n * app actually deploys that way.\n *\n * ⛔ `disable_nodejs_process_v2` ON ASTRO IS LOAD-BEARING. Workerd process-v2 (compat\n * date 2025-09-15) makes every Astro page return `[object Object]`. Vite/TanStack\n * Start does not need that flag; it needs `nodejs_compat` (Alchemy already defaults\n * it on Vite).\n *\n * ⛔ Alchemy injects `@alchemy.run/frontend-frameworks/astro`. Do not add\n * `@astrojs/cloudflare` — that is the wrangler path.\n *\n * ★ Alchemy's Website props are an Effect-wrapped InputProps union. We do not re-export\n * that type — isolatedDeclarations cannot name it. Extra Alchemy fields still pass\n * through via the rest bag; the assertion is only at the call into Alchemy.\n */\nimport * as AdoptPolicy from 'alchemy/AdoptPolicy';\nimport * as Cloudflare from 'alchemy/Cloudflare';\nimport * as RemovalPolicy from 'alchemy/RemovalPolicy';\n\nconst ASTRO_FLAGS: readonly string[] = ['nodejs_compat', 'disable_nodejs_process_v2'];\n\nexport interface AstroWebsiteProps {\n readonly rootDir?: string;\n readonly name?: string;\n readonly sessionKVBindingName?: string | false;\n readonly compatibility?: { readonly flags?: readonly string[]; readonly date?: string };\n}\n\nexport interface ViteWebsiteProps {\n readonly rootDir?: string;\n readonly name?: string;\n}\n\n/**\n * `Cloudflare.Website.Astro` with house flags, no auto session KV, adopt+retain.\n *\n * const site = yield* astroWebsite('subnetcalc', { rootDir });\n */\nexport function astroWebsite(\n name: string,\n props: AstroWebsiteProps,\n): ReturnType<typeof Cloudflare.Website.Astro> {\n const flags = props.compatibility?.flags ?? ASTRO_FLAGS;\n const merged = {\n ...props,\n name: props.name ?? name,\n sessionKVBindingName: props.sessionKVBindingName ?? false,\n compatibility: { ...props.compatibility, flags: [...flags] },\n };\n return Cloudflare.Website.Astro(name, merged as never).pipe(\n AdoptPolicy.adopt(true),\n RemovalPolicy.retain(),\n );\n}\n\n/**\n * `Cloudflare.Website.Vite` — TanStack Start, SolidStart, static Vite.\n * `nodejs_compat` is Alchemy's Vite default; we do not add process-v2 here.\n *\n * const app = yield* viteWebsite('aimto', { rootDir: webRoot });\n */\nexport function viteWebsite(\n name: string,\n props?: ViteWebsiteProps,\n): ReturnType<typeof Cloudflare.Website.Vite> {\n const merged = { ...props, name: props?.name ?? name };\n return Cloudflare.Website.Vite(name, merged as never).pipe(\n AdoptPolicy.adopt(true),\n RemovalPolicy.retain(),\n );\n}\n"
10
15
  ],
11
- "mappings": "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",
12
- "debugId": "715592491E7374E164756E2164756E21",
16
+ "mappings": "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",
17
+ "debugId": "8A155CE09F54FFB164756E2164756E21",
13
18
  "names": []
14
19
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The `warp_connector` calls a Cloudflare Mesh node needs, and nothing else.
3
+ *
4
+ * ★ A MESH NODE IS A `warp_connector` OBJECT. Cloudflare's Mesh docs create one with
5
+ * `POST /accounts/{account_id}/warp_connector` and fetch its enrolment token from
6
+ * `GET …/warp_connector/{id}/token` (developers.cloudflare.com/mesh/get-started, read
7
+ * 2026-09-21). There is no separate "mesh node" endpoint.
8
+ *
9
+ * ★ OVER `@distilled.cloud/cloudflare`, THE SDK ALCHEMY'S OWN CLOUDFLARE PROVIDERS USE, not the
10
+ * `cloudflare` npm SDK the R2 lock uses. Measured 2026-09-21: `cloudflare@4.5.0`'s
11
+ * `WARPConnectorCreateParams` has only `account_id` and `name`, so it cannot create an HA node.
12
+ * distilled `1.0.0-rc.12` (the version alchemy@2.0.0-beta.79 pins) has `ha?: boolean` on
13
+ * `CreateTunnelWarpConnectorRequest`, typed errors (`TunnelNotFound` code 1002 / 404,
14
+ * `DuplicateTunnelName` code 1013), and resolves credentials and the account exactly the way
15
+ * `Cloudflare.Tunnel.WarpConnector` and `Cloudflare.Gateway.Rule` do — so the node and the Gateway
16
+ * rules that pin its Mesh IP always land in the same account.
17
+ *
18
+ * ⛔ NO FUNCTION IN THIS FILE READS THE NODE TOKEN, and `observe` copies named fields only. The
19
+ * HA docs say the create response carries a `token` field; distilled's decoder copies only the
20
+ * fields in its schema (protocol.ts, `getProps(outputAst)`), and `observe` below would drop it
21
+ * again if it ever got through. The one token read lives in mesh-node-token.ts, on demand.
22
+ */
23
+ import * as zeroTrust from '@distilled.cloud/cloudflare/zero-trust';
24
+ import * as Effect from 'effect/Effect';
25
+ /** `inactive` (never run), `degraded`, `healthy` or `down` — Cloudflare's own four. */
26
+ export type MeshNodeStatus = 'inactive' | 'degraded' | 'healthy' | 'down';
27
+ /** A live node as this package sees it. ⛔ There is no token field here, on purpose. */
28
+ export interface ObservedNode {
29
+ readonly id: string;
30
+ readonly name: string;
31
+ readonly status: MeshNodeStatus | undefined;
32
+ }
33
+ declare const MeshNodeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
34
+ readonly _tag: "MeshNodeError";
35
+ } & Readonly<A>;
36
+ /** A refusal or failure, carrying the sentence an operator needs. Never a token. */
37
+ export declare class MeshNodeError extends MeshNodeError_base<{
38
+ readonly message: string;
39
+ }> {
40
+ }
41
+ type Raw = {
42
+ readonly id?: string | null;
43
+ readonly name?: string | null;
44
+ readonly status?: string | null;
45
+ readonly deletedAt?: string | null;
46
+ };
47
+ /**
48
+ * ⚠️ A DELETED NODE IS NOT A NODE. Cloudflare keeps deleted tunnels (every response carries
49
+ * `deleted_at`, and the list takes `is_deleted`), so a `GET` by id can answer for one. Treating
50
+ * that as live would "adopt" a tombstone and PATCH it.
51
+ */
52
+ export declare const observe: (raw: Raw) => ObservedNode | undefined;
53
+ /** The node with this id, or `undefined` when it is gone (404 / code 1002, or soft-deleted). */
54
+ export declare const getNode: (accountId: string, nodeId: string) => Effect.Effect<ObservedNode | undefined, zeroTrust.Forbidden | zeroTrust.CloudflareOpError, zeroTrust.CloudflareOpContext>;
55
+ /**
56
+ * The live node with exactly this name, or `undefined`.
57
+ *
58
+ * ★ `name` is also sent as the list filter, but the match is re-checked here: the filter's
59
+ * exactness is not documented, and a prefix match would adopt the wrong node.
60
+ * ⛔ Two live matches is refused rather than resolved. Names are unique per account (the create
61
+ * answers code 1013 otherwise), so two means something this package does not understand.
62
+ */
63
+ export declare const findNodeByName: (accountId: string, name: string) => Effect.Effect<ObservedNode | undefined, MeshNodeError | zeroTrust.ListTunnelWarpConnectorsError, zeroTrust.CloudflareOpContext>;
64
+ /**
65
+ * ⛔ `ha` IS SENT ON CREATE AND NOWHERE ELSE. Cloudflare's HA page: "High availability is set at
66
+ * node creation time and cannot be changed afterward." The PATCH body (`PatchTunnelWarpConnector
67
+ * Request`) has only `name` and `tunnelSecret`.
68
+ */
69
+ export declare const createNode: (accountId: string, name: string, ha: boolean) => Effect.Effect<ObservedNode, MeshNodeError | zeroTrust.CreateTunnelWarpConnectorError, zeroTrust.CloudflareOpContext>;
70
+ /** ★ A rename is a PATCH: same id, same token, same enrolled replicas and Mesh IP. */
71
+ export declare const renameNode: (accountId: string, nodeId: string, name: string) => Effect.Effect<ObservedNode, zeroTrust.PatchTunnelWarpConnectorError, zeroTrust.CloudflareOpContext>;
72
+ /** Idempotent: a node already gone (404 / code 1002) is a successful delete. */
73
+ export declare const deleteNode: (accountId: string, nodeId: string) => Effect.Effect<void, zeroTrust.Forbidden | zeroTrust.CloudflareOpError, zeroTrust.CloudflareOpContext>;
74
+ export {};
75
+ //# sourceMappingURL=mesh-node-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-node-api.d.ts","sourceRoot":"","sources":["../../src/cloudflare/mesh-node-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,SAAS,MAAM,wCAAwC,CAAC;AAEpE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,uFAAuF;AACvF,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;AAE1E,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;CAC7C;;;;AAED,oFAAoF;AACpF,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;AAEL,KAAK,GAAG,GAAG;IACT,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,OAAO,QAAS,GAAG,KAAG,YAAY,GAAG,SAQjD,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,OAAO,cAAe,MAAM,UAAU,MAAM,8HAItD,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,cAAe,MAAM,QAAQ,MAAM,oIAe3D,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,UAAU,cAAe,MAAM,QAAQ,MAAM,MAAM,OAAO,yHAUpE,CAAC;AAEJ,sFAAsF;AACtF,eAAO,MAAM,UAAU,cAAe,MAAM,UAAU,MAAM,QAAQ,MAAM,wGAGa,CAAC;AAExF,gFAAgF;AAChF,eAAO,MAAM,UAAU,cAAe,MAAM,UAAU,MAAM,0GAIzD,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `Cloudflare.MeshNode` — its props, its attributes, and the pure `diff` that decides between
3
+ * update and replace.
4
+ *
5
+ * ⛔ THE ATTRIBUTES NEVER HOLD THE NODE TOKEN. That is the whole reason this resource exists:
6
+ * Alchemy beta.79's `Cloudflare.Tunnel.WarpConnector` calls `getTunnelWarpConnectorToken` in
7
+ * `toAttributes`, so EVERY read, reconcile and list fetches the token, and Alchemy state (which
8
+ * is not encrypted) stores it — `Redacted` hides it from logs, not from the state store. It also
9
+ * has no `ha` prop. The token is a runtime credential: fetch it on demand with
10
+ * `fetchMeshNodeToken` (mesh-node-token.ts) and write it straight to the host.
11
+ * ★ A BONUS OF NOT READING IT: the token endpoint needs a Write permission ("Cloudflare One
12
+ * Connectors Write", per its API reference, read 2026-09-21), so a plan over this resource can
13
+ * run on a read-only token. WarpConnector's cannot.
14
+ */
15
+ import type { Diff } from 'alchemy/Diff';
16
+ import type { Input } from 'alchemy/Input';
17
+ import { MeshNodeError, type MeshNodeStatus } from './mesh-node-api.ts';
18
+ export interface MeshNodeProps {
19
+ /**
20
+ * The node's name in the account — and its identity when adopting (names are unique per
21
+ * account). ★ Changing it RENAMES the node in place (`PATCH`): the id, the token, the
22
+ * enrolled replicas and their Mesh IPs all survive, so a rename never re-enrols a host.
23
+ */
24
+ readonly name: string;
25
+ /**
26
+ * High availability (active-passive replicas sharing one node token).
27
+ *
28
+ * ⛔ CREATE-ONLY, AND REQUIRED FOR THAT REASON. Cloudflare's Mesh HA page: "High availability
29
+ * is set at node creation time and cannot be changed afterward." The PATCH body carries no
30
+ * `ha`. So a change REPLACES the node — a new id, a new token, a new Mesh IP per replica —
31
+ * and a default would silently fix an irreversible choice. The API's own default is `false`;
32
+ * the dashboard's is `true`. Say which one you mean.
33
+ * ★ A NODE WITH NO ROUTES — A DOOR — IS `ha: false`, AND A SECOND DOOR IS A SECOND NODE. HA
34
+ * fails over the CIDR routes a node advertises; "Nodes without routes do not benefit from HA
35
+ * failover", because the node's Mesh IP is tied to the individual replica (same page, "When to
36
+ * use" and "Considerations", read 2026-09-21). After a failover the promoted replica answers
37
+ * on its own Mesh IP, so nothing that dials a door's first replica follows it. Declare the
38
+ * second door as its own `MeshNode` (own name, token, Mesh IP) and have callers list both.
39
+ * ⚠️ `ha` NEEDS MASQUE on the node's device profile (same page).
40
+ */
41
+ readonly ha: boolean;
42
+ }
43
+ export interface MeshNodeAttributes {
44
+ /** The node's UUID (the `warp_connector` tunnel id). Pass it to `fetchMeshNodeToken`. */
45
+ readonly id: string;
46
+ /** The account the provider environment resolved when the node was written. */
47
+ readonly accountId: string;
48
+ readonly name: string;
49
+ /** As of the last read or write; it moves on its own as replicas connect and drop. */
50
+ readonly status: MeshNodeStatus | undefined;
51
+ /**
52
+ * The value the node was CREATED with (or declared with when adopted — see mesh-node.md).
53
+ * ⚠️ No documented read returns it: `GET /warp_connector/{id}` has no `ha` field (API
54
+ * reference, read 2026-09-21), so drift here cannot be observed, only prevented.
55
+ */
56
+ readonly ha: boolean;
57
+ }
58
+ /** ⛔ Fail closed on a declaration the API would accept but nobody meant. */
59
+ export declare const validateMeshNode: (props: MeshNodeProps) => MeshNodeError | undefined;
60
+ /**
61
+ * update or replace, from the declaration and what was last written.
62
+ *
63
+ * - Account changed → `replace`, create-first: names are unique per ACCOUNT, so the two
64
+ * generations cannot collide.
65
+ * - `ha` changed → `replace`. ⛔ `deleteFirst` when the name stays the same: the new node needs
66
+ * the name the old one holds, so create-first would fail with code 1013 (or, worse, a
67
+ * create path that "converged" on the old node would record an `ha` it does not have).
68
+ * Create-first when the name changes in the same edit, because then nothing collides.
69
+ * ⚠️ UNDER THE DEFAULT `retain` (mesh-node.ts) the engine skips the old node's delete in both
70
+ * orders: delete-first then REFUSES at the create (mesh-node-lifecycle.ts) until the deploy
71
+ * opts in with `RemovalPolicy.destroy()`, and create-first leaves the old node live, unmanaged.
72
+ * The answer here is the same under both policies: `diff` is not handed the policy (Provider.ts,
73
+ * its input), and delete-first is exactly what lets the opted-in deploy reuse the name.
74
+ * - Name changed alone → `update` (a PATCH rename).
75
+ * - Otherwise → no opinion; the engine compares props.
76
+ *
77
+ * ★ An unresolved `ha` gives no answer here; `reconcile` re-checks it once resolved and refuses
78
+ * rather than updating across a create-only change. An unresolved `name` next to a changed `ha`
79
+ * counts as "unchanged", which picks the delete-first order that cannot collide.
80
+ */
81
+ export declare const diffMeshNode: (news: Input<MeshNodeProps>, output: MeshNodeAttributes | undefined, accountId: string) => Diff | undefined;
82
+ //# sourceMappingURL=mesh-node-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mesh-node-form.d.ts","sourceRoot":"","sources":["../../src/cloudflare/mesh-node-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,yFAAyF;IACzF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,UAAW,aAAa,KAAG,aAAa,GAAG,SAevE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY,SACjB,KAAK,CAAC,aAAa,CAAC,UAClB,kBAAkB,GAAG,SAAS,aAC3B,MAAM,KAChB,IAAI,GAAG,SAWT,CAAC"}