@homeflare/alchemy 0.19.1 → 0.21.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 (198) hide show
  1. package/README.md +25 -0
  2. package/dist/caddy/access-forward-auth.d.ts +56 -0
  3. package/dist/caddy/access-forward-auth.d.ts.map +1 -0
  4. package/dist/caddy/index.d.ts +6 -0
  5. package/dist/caddy/index.d.ts.map +1 -1
  6. package/dist/caddy/index.js +169 -75
  7. package/dist/caddy/index.js.map +10 -7
  8. package/dist/cloudflare/mcp-server-api.d.ts +135 -0
  9. package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
  10. package/dist/cloudflare/mcp-server-form.d.ts +116 -0
  11. package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/launchd/file-converge.d.ts +70 -0
  15. package/dist/launchd/file-converge.d.ts.map +1 -0
  16. package/dist/launchd/file-identity.d.ts +32 -0
  17. package/dist/launchd/file-identity.d.ts.map +1 -0
  18. package/dist/launchd/host-file-form.d.ts +2 -0
  19. package/dist/launchd/host-file-form.d.ts.map +1 -1
  20. package/dist/launchd/host-file-lifecycle.d.ts +11 -24
  21. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  22. package/dist/launchd/host-file.d.ts.map +1 -1
  23. package/dist/launchd/index.js +173 -120
  24. package/dist/launchd/index.js.map +12 -10
  25. package/dist/launchd/local-runner.d.ts.map +1 -1
  26. package/dist/launchd/runner.d.ts +17 -2
  27. package/dist/launchd/runner.d.ts.map +1 -1
  28. package/dist/launchd/sudo-runner.d.ts +11 -0
  29. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  30. package/dist/linux/index.js +37 -11
  31. package/dist/linux/index.js.map +10 -10
  32. package/dist/linux/region.d.ts +11 -0
  33. package/dist/linux/region.d.ts.map +1 -1
  34. package/dist/linux/remote-file-lifecycle.d.ts.map +1 -1
  35. package/dist/linux/remote-file-plan.d.ts +22 -0
  36. package/dist/linux/remote-file-plan.d.ts.map +1 -1
  37. package/dist/linux/remote-file.d.ts +4 -1
  38. package/dist/linux/remote-file.d.ts.map +1 -1
  39. package/dist/linux/sudo-listing.d.ts +47 -0
  40. package/dist/linux/sudo-listing.d.ts.map +1 -0
  41. package/dist/linux/unit-handlers.d.ts.map +1 -1
  42. package/dist/linux/unit-preflight.d.ts +10 -1
  43. package/dist/linux/unit-preflight.d.ts.map +1 -1
  44. package/dist/proxmox/credential-literals.d.ts +1 -1
  45. package/dist/proxmox/credential-literals.d.ts.map +1 -1
  46. package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
  47. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
  48. package/dist/proxmox/generated/pbs.d.ts +7 -0
  49. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  50. package/dist/proxmox/generated/pve.d.ts +7 -0
  51. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  52. package/dist/proxmox/index.d.ts +16 -4
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +5 -2
  55. package/dist/proxmox/index.js.map +9 -9
  56. package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
  57. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
  58. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  59. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
  60. package/dist/release/archive.d.ts +11 -0
  61. package/dist/release/archive.d.ts.map +1 -0
  62. package/dist/release/binary-claim.d.ts +53 -0
  63. package/dist/release/binary-claim.d.ts.map +1 -0
  64. package/dist/release/binary-diff.d.ts +38 -0
  65. package/dist/release/binary-diff.d.ts.map +1 -0
  66. package/dist/release/binary-form.d.ts +118 -0
  67. package/dist/release/binary-form.d.ts.map +1 -0
  68. package/dist/release/binary-lifecycle.d.ts +55 -0
  69. package/dist/release/binary-lifecycle.d.ts.map +1 -0
  70. package/dist/release/binary-read.d.ts +11 -0
  71. package/dist/release/binary-read.d.ts.map +1 -0
  72. package/dist/release/binary.d.ts +68 -0
  73. package/dist/release/binary.d.ts.map +1 -0
  74. package/dist/release/catalog.d.ts +76 -0
  75. package/dist/release/catalog.d.ts.map +1 -0
  76. package/dist/release/checksums.d.ts +22 -0
  77. package/dist/release/checksums.d.ts.map +1 -0
  78. package/dist/release/declared-pins.d.ts +4 -0
  79. package/dist/release/declared-pins.d.ts.map +1 -0
  80. package/dist/release/download.d.ts +44 -0
  81. package/dist/release/download.d.ts.map +1 -0
  82. package/dist/release/fake-deploy.d.ts +48 -0
  83. package/dist/release/fake-deploy.d.ts.map +1 -0
  84. package/dist/release/fake-engine.d.ts +8 -0
  85. package/dist/release/fake-engine.d.ts.map +1 -0
  86. package/dist/release/fake-install.d.ts +39 -0
  87. package/dist/release/fake-install.d.ts.map +1 -0
  88. package/dist/release/fake-provider.d.ts +58 -0
  89. package/dist/release/fake-provider.d.ts.map +1 -0
  90. package/dist/release/fake-release.d.ts +95 -0
  91. package/dist/release/fake-release.d.ts.map +1 -0
  92. package/dist/release/index.d.ts +24 -0
  93. package/dist/release/index.d.ts.map +1 -0
  94. package/dist/release/index.js +1365 -0
  95. package/dist/release/index.js.map +35 -0
  96. package/dist/release/providers.d.ts +19 -0
  97. package/dist/release/providers.d.ts.map +1 -0
  98. package/dist/release/refused.d.ts +39 -0
  99. package/dist/release/refused.d.ts.map +1 -0
  100. package/dist/release/tar.d.ts +14 -0
  101. package/dist/release/tar.d.ts.map +1 -0
  102. package/dist/release/victoria.d.ts +36 -0
  103. package/dist/release/victoria.d.ts.map +1 -0
  104. package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
  105. package/docs/adopted-deploys.md +13 -0
  106. package/docs/linux-host.md +5 -1
  107. package/docs/provider-standard.md +138 -0
  108. package/docs/release-binary-catalogs.md +113 -0
  109. package/docs/release-binary-measured.md +163 -0
  110. package/docs/release-binary-refusals.md +57 -0
  111. package/docs/release-binary-upstream.md +163 -0
  112. package/docs/release-binary.md +154 -0
  113. package/docs/upstream-conformance.md +164 -0
  114. package/package.json +5 -1
  115. package/src/caddy/access-forward-auth.test.ts +63 -0
  116. package/src/caddy/access-forward-auth.ts +120 -0
  117. package/src/caddy/index.ts +10 -0
  118. package/src/caddy/providers.test.ts +4 -0
  119. package/src/cloudflare/mcp-server-api.ts +294 -0
  120. package/src/cloudflare/mcp-server-form.ts +219 -0
  121. package/src/index.ts +1 -0
  122. package/src/launchd/file-converge.ts +233 -0
  123. package/src/launchd/file-identity.ts +69 -0
  124. package/src/launchd/host-file-alias.test.ts +65 -0
  125. package/src/launchd/host-file-form.ts +2 -1
  126. package/src/launchd/host-file-lifecycle.ts +25 -144
  127. package/src/launchd/host-file.ts +8 -2
  128. package/src/launchd/local-runner.test.ts +12 -1
  129. package/src/launchd/local-runner.ts +2 -0
  130. package/src/launchd/runner.ts +17 -2
  131. package/src/launchd/sudo-runner.ts +11 -0
  132. package/src/linux/region.ts +15 -0
  133. package/src/linux/remote-file-lifecycle.test.ts +0 -88
  134. package/src/linux/remote-file-lifecycle.ts +25 -7
  135. package/src/linux/remote-file-plan.ts +37 -1
  136. package/src/linux/remote-file-region.test.ts +181 -0
  137. package/src/linux/remote-file.ts +4 -1
  138. package/src/linux/sudo-listing.ts +122 -0
  139. package/src/linux/unit-handlers.ts +12 -5
  140. package/src/linux/unit-preflight.ts +20 -4
  141. package/src/linux/unit-rename.test.ts +76 -0
  142. package/src/proxmox/ceph-adopt.test.ts +210 -0
  143. package/src/proxmox/credential-literals.ts +1 -1
  144. package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
  145. package/src/proxmox/generated/pbs.ts +7 -0
  146. package/src/proxmox/generated/pve.ts +7 -0
  147. package/src/proxmox/index.test.ts +7 -4
  148. package/src/proxmox/index.ts +16 -4
  149. package/src/proxmox/pbs-notification-target-form.ts +1 -1
  150. package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
  151. package/src/proxmox/pbs-notification-target-wire.ts +1 -1
  152. package/src/proxmox/pbs-notification-target.ts +1 -1
  153. package/src/release/adopt-parity.test.ts +106 -0
  154. package/src/release/apply-pins.test.ts +128 -0
  155. package/src/release/archive.ts +94 -0
  156. package/src/release/binary-alias.test.ts +109 -0
  157. package/src/release/binary-claim.ts +80 -0
  158. package/src/release/binary-claims.test.ts +47 -0
  159. package/src/release/binary-diff.ts +73 -0
  160. package/src/release/binary-form.ts +229 -0
  161. package/src/release/binary-lifecycle.test.ts +90 -0
  162. package/src/release/binary-lifecycle.ts +202 -0
  163. package/src/release/binary-read.ts +55 -0
  164. package/src/release/binary-refusals.test.ts +192 -0
  165. package/src/release/binary-sudo.test.ts +93 -0
  166. package/src/release/binary.ts +181 -0
  167. package/src/release/catalog.test.ts +96 -0
  168. package/src/release/catalog.ts +175 -0
  169. package/src/release/checksums.test.ts +35 -0
  170. package/src/release/checksums.ts +43 -0
  171. package/src/release/declared-pins.ts +39 -0
  172. package/src/release/download.test.ts +138 -0
  173. package/src/release/download.ts +110 -0
  174. package/src/release/error-channel.test.ts +69 -0
  175. package/src/release/fake-deploy.ts +87 -0
  176. package/src/release/fake-engine.ts +50 -0
  177. package/src/release/fake-install.ts +59 -0
  178. package/src/release/fake-provider.ts +68 -0
  179. package/src/release/fake-release.ts +185 -0
  180. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
  181. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
  182. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
  183. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
  184. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
  185. package/src/release/index.ts +41 -0
  186. package/src/release/plan.test.ts +189 -0
  187. package/src/release/provider-refusals.test.ts +48 -0
  188. package/src/release/providers.test.ts +174 -0
  189. package/src/release/providers.ts +24 -0
  190. package/src/release/recovery-read.test.ts +44 -0
  191. package/src/release/refused.ts +40 -0
  192. package/src/release/tar.test.ts +142 -0
  193. package/src/release/tar.ts +195 -0
  194. package/src/release/victoria.test.ts +108 -0
  195. package/src/release/victoria.ts +154 -0
  196. package/src/{proxmox → secrets}/write-only.ts +6 -0
  197. /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
  198. /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
package/README.md CHANGED
@@ -164,6 +164,31 @@ SystemdUnit('thing', { name: 'thing.service', sections: [...], restartOn: [confi
164
164
  - ⛔ **The directive set is systemd's:** unit files render verbatim; nothing here invents a schema.
165
165
  - Guide, the measured `systemctl` shapes and the limits: [docs/linux-host.md](./docs/linux-host.md).
166
166
 
167
+ ## Release binaries — `@homeflare/alchemy/release`
168
+
169
+ `ReleaseBinary` installs one binary out of a pinned release archive into a directory you declare,
170
+ verified twice. The pins are props; each vendor's pinned versions are a data set beside it
171
+ (`VICTORIA_RELEASES` is the first). `releaseProviders(runner)` provides it.
172
+
173
+ ```ts
174
+ const request = {
175
+ package: 'vmutils',
176
+ version: '1.151.0',
177
+ platform: 'darwin-arm64',
178
+ binary: 'vmalert',
179
+ };
180
+ ReleaseBinary('vmalert', { ...catalogBinary(VICTORIA_RELEASES, request), directory: dir.path });
181
+ ```
182
+
183
+ - ⛔ **It installs; it never starts.** Put `binary.path` in your job's argv; that orders the two.
184
+ - ⛔ **Pinned in code, never fetched:** a version the data set does not pin fails the plan. Exact
185
+ asset names, the archive verified before unpacking and the binary before writing, only the
186
+ declared member extracted, one writer (`HostRunner.writeFileAtomic`).
187
+ - Guide: [docs/release-binary.md](./docs/release-binary.md) · data sets and adding a vendor:
188
+ [docs/release-binary-catalogs.md](./docs/release-binary-catalogs.md) · measured:
189
+ [docs/release-binary-measured.md](./docs/release-binary-measured.md) · upstream:
190
+ [docs/release-binary-upstream.md](./docs/release-binary-upstream.md).
191
+
167
192
  ## Caddy — `@homeflare/alchemy/caddy`
168
193
 
169
194
  `CaddyConfig` declares a running Caddy's config as Caddyfile text, applied through Caddy's own admin
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The Caddyfile half of "protect this route with Cloudflare Access": a `forward_auth` block
3
+ * pointing at a verifier that speaks the contract in `@homeflare/cloudflare/access-auth`.
4
+ *
5
+ * ★ A BUILDER, NOT A RESOURCE. Nothing here is created, adopted or destroyed — it is Caddyfile
6
+ * TEXT that a caller drops into the `caddyfile` prop of a `CaddyConfig` / `caddyWithFile()`.
7
+ * A resource would own a fragment of a config that `CaddyConfig` already owns whole, and two
8
+ * owners of one file is the drift this package exists to prevent.
9
+ *
10
+ * ⛔ WHY IT IS GENERATED RATHER THAN COPY-PASTED. Every field below is load-bearing, and the
11
+ * failure mode of getting one wrong is a route that looks protected and is not:
12
+ * · a missing `copy_headers` — the upstream gets no identity and either breaks or, worse,
13
+ * falls back to trusting a header the CLIENT set;
14
+ * · the verifier on anything but loopback — the JWT check becomes advisory, because the
15
+ * verifier itself can be reached and answered around;
16
+ * · `uri` pointing at a path the verifier does not route — every request 404s, which is
17
+ * non-2xx, which Caddy correctly treats as a denial, so the symptom is "Access is broken".
18
+ */
19
+ export interface AccessForwardAuthProps {
20
+ /**
21
+ * Where the verifier listens, e.g. `127.0.0.1:9101`.
22
+ * ⛔ LOOPBACK OR A UNIX SOCKET. See `verifierProblems` — a routable verifier is not a check.
23
+ */
24
+ readonly verifier: string;
25
+ /** The path the verifier answers on. @default '/access/verify' */
26
+ readonly uri?: string;
27
+ /**
28
+ * Response headers copied from the verifier onto the upstream request.
29
+ * @default ['X-Access-Email', 'X-Access-Sub']
30
+ */
31
+ readonly copyHeaders?: readonly string[];
32
+ /** Lines placed inside the route BEFORE `forward_auth` — typically the upstream. */
33
+ readonly body?: readonly string[];
34
+ /** Indent applied to every emitted line. @default '\t' */
35
+ readonly indent?: string;
36
+ }
37
+ export declare const verifierProblems: (verifier: string) => string[];
38
+ /**
39
+ * Render the `forward_auth` block for one protected route.
40
+ *
41
+ * ```ts
42
+ * const caddyfile = `vnc.example.com {
43
+ * ${accessForwardAuth({ verifier: '127.0.0.1:9101', body: ['reverse_proxy 127.0.0.1:6080'] })}
44
+ * }
45
+ * `;
46
+ * ```
47
+ *
48
+ * ⛔ ONE VERIFIER ENDPOINT PER ACCESS APPLICATION. The AUD is fixed in the verifier's own config,
49
+ * never carried in this request — so two applications mean two `uri`s (or two verifiers), not
50
+ * one endpoint told which app to check. An AUD the request can influence is the vulnerability.
51
+ *
52
+ * ⚠️ `forward_auth` COPIES THE VERIFIER'S RESPONSE BACK TO THE CLIENT on any non-2xx, which is why
53
+ * the verifier answers a bare 401 with no body: whatever it returns is what the browser sees.
54
+ */
55
+ export declare const accessForwardAuth: (props: AccessForwardAuthProps) => string;
56
+ //# sourceMappingURL=access-forward-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-forward-auth.d.ts","sourceRoot":"","sources":["../../src/caddy/access-forward-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AA6BD,eAAO,MAAM,gBAAgB,aAAc,MAAM,KAAG,MAAM,EAWzD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,UAAW,sBAAsB,KAAG,MAqBjE,CAAC"}
@@ -9,6 +9,12 @@
9
9
  * ⛔ NOTHING IS ADOPTED SILENTLY: a running config that is not the declared one needs `--adopt`.
10
10
  * Guide, order of file and load, adoption, `--resume` and secrets: docs/caddy.md.
11
11
  */
12
+ /**
13
+ * ★ `accessForwardAuth` is Caddyfile TEXT, not a resource: it renders the `forward_auth` block
14
+ * that puts a Cloudflare Access check in front of a route, paired with the verifier in
15
+ * `@homeflare/cloudflare/access-auth`. Guide: docs/access-auth.md.
16
+ */
17
+ export { accessForwardAuth, verifierProblems, type AccessForwardAuthProps, } from './access-forward-auth.ts';
12
18
  export type { CaddyAdmin, CaddyAdminListener, CaddyAdminRequest, CaddyAdminResponse, } from './admin.ts';
13
19
  export { CaddyAdminService, CaddyUnreachableError, caddyAdminLayer } from './admin.ts';
14
20
  export { CaddyAdminError } from './admin-calls.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/caddy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/caddy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,sBAAsB,GAC5B,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1,3 +1,44 @@
1
+ // src/caddy/access-forward-auth.ts
2
+ var DEFAULT_COPY_HEADERS = ["X-Access-Email", "X-Access-Sub"];
3
+ var LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
4
+ var hostOf = (verifier) => {
5
+ const address = verifier.replace(/^https?:\/\//, "");
6
+ if (address.startsWith("unix/"))
7
+ return "unix";
8
+ if (address.startsWith("["))
9
+ return address.slice(0, address.indexOf("]") + 1);
10
+ const colon = address.indexOf(":");
11
+ return colon === -1 ? address : address.slice(0, colon);
12
+ };
13
+ var verifierProblems = (verifier) => {
14
+ const found = [];
15
+ const host = hostOf(verifier.trim());
16
+ if (verifier.trim() === "")
17
+ found.push("verifier is empty");
18
+ else if (host !== "unix" && !LOOPBACK.has(host)) {
19
+ found.push(`verifier must be loopback or a unix socket, got ${verifier} — a routable verifier can be bypassed`);
20
+ }
21
+ return found;
22
+ };
23
+ var accessForwardAuth = (props) => {
24
+ const problems = verifierProblems(props.verifier);
25
+ if (problems.length > 0)
26
+ throw new Error(`accessForwardAuth: ${problems.join("; ")}`);
27
+ const indent = props.indent ?? "\t";
28
+ const uri = props.uri ?? "/access/verify";
29
+ const headers = props.copyHeaders ?? DEFAULT_COPY_HEADERS;
30
+ const lines = [
31
+ ...props.body ?? [],
32
+ `forward_auth ${props.verifier} {`,
33
+ `${indent}uri ${uri}`,
34
+ `${indent}copy_headers {`,
35
+ ...headers.map((header) => `${indent}${indent}${header}`),
36
+ `${indent}}`,
37
+ "}"
38
+ ];
39
+ return lines.map((line) => `${indent}${line}`).join(`
40
+ `);
41
+ };
1
42
  // src/caddy/admin.ts
2
43
  import * as Context from "effect/Context";
3
44
  import * as Layer from "effect/Layer";
@@ -938,36 +979,29 @@ import { isResolved as isResolved2 } from "alchemy/Diff";
938
979
  import * as Provider2 from "alchemy/Provider";
939
980
  import * as Effect7 from "effect/Effect";
940
981
 
941
- // src/launchd/host-file-form.ts
942
- var DEFAULT_MODE = 420;
943
- var identityProblems = (name, value) => {
944
- if (value === undefined)
945
- return [];
946
- if (typeof value === "number") {
947
- return Number.isSafeInteger(value) && value >= 0 ? [] : [`${name} must be a non-negative integer id`];
982
+ // src/launchd/file-identity.ts
983
+ var sameEntry = (a, b) => {
984
+ if (a === undefined || b === undefined)
985
+ return false;
986
+ if (a.dev === undefined || a.ino === undefined || b.dev === undefined || b.ino === undefined) {
987
+ return;
948
988
  }
949
- return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value) ? [] : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
989
+ return a.dev === b.dev && a.ino === b.ino;
950
990
  };
951
- var pathProblems = (path) => {
952
- const found = [];
953
- if (!path.startsWith("/"))
954
- found.push("path must be absolute");
955
- if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
956
- found.push('path must be normalised (no trailing slash, no empty, "." or ".." segments)');
957
- }
958
- if (path.includes("\x00"))
959
- found.push("path contains NUL");
960
- return found;
991
+ var oneFile = async (runner, a, b) => {
992
+ if (a === b)
993
+ return true;
994
+ const [left, right] = await Promise.all([runner.stat(a), runner.stat(b)]);
995
+ return sameEntry(left, right);
961
996
  };
962
- var fileProblems = (props) => {
963
- const found = pathProblems(props.path);
964
- const mode = props.mode ?? DEFAULT_MODE;
965
- if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
966
- found.push("mode must be 0–0o7777");
967
- found.push(...identityProblems("owner", props.owner));
968
- found.push(...identityProblems("group", props.group));
969
- found.push(...fileSecretProblems(props.content));
970
- return found;
997
+ var leaveOldPath = async (runner, oldPath, path, remove, refuse3) => {
998
+ const same = await oneFile(runner, oldPath, path);
999
+ if (same === true)
1000
+ return;
1001
+ await remove();
1002
+ if (same === undefined && await runner.stat(path) === undefined) {
1003
+ throw refuse3(path, `removing the old path ${oldPath} removed this file too: they are one file under two ` + "spellings (a symlinked parent, or case on a case-insensitive volume). Deploy again to " + "reinstall it, and keep one spelling.");
1004
+ }
971
1005
  };
972
1006
 
973
1007
  // src/launchd/runner.ts
@@ -979,33 +1013,22 @@ class HostRunnerService extends Context2.Service()("homeflare/launchd/HostRunner
979
1013
  var hostRunnerLayer = (runner) => Layer3.succeed(HostRunnerService, runner);
980
1014
  var canActAsRoot = (runner) => runner.privileged || runner.effectiveUid() === 0;
981
1015
 
982
- // src/launchd/host-file-lifecycle.ts
983
- var refuse3 = (path, message) => new Error(`Host.File ${path}: ${message}`);
1016
+ // src/launchd/file-converge.ts
984
1017
  var numericId = (value) => typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : undefined;
985
- var desiredFile = async (runner, props) => {
986
- const found = fileProblems(props);
987
- if (found.length > 0)
988
- throw refuse3(props.path, found.join("; "));
989
- const bytes = new TextEncoder().encode(props.content);
1018
+ var resolveIds = async (runner, declared, path, refuse3) => {
990
1019
  let uid;
991
1020
  let gid;
992
- if (props.owner !== undefined) {
993
- uid = numericId(props.owner) ?? (await runner.lookupUser(String(props.owner)))?.uid;
1021
+ if (declared.owner !== undefined) {
1022
+ uid = numericId(declared.owner) ?? (await runner.lookupUser(String(declared.owner)))?.uid;
994
1023
  if (uid === undefined)
995
- throw refuse3(props.path, `no user ${String(props.owner)} on this host`);
1024
+ throw refuse3(path, `no user ${String(declared.owner)} on this host`);
996
1025
  }
997
- if (props.group !== undefined) {
998
- gid = numericId(props.group) ?? await runner.lookupGroup(String(props.group));
1026
+ if (declared.group !== undefined) {
1027
+ gid = numericId(declared.group) ?? await runner.lookupGroup(String(declared.group));
999
1028
  if (gid === undefined)
1000
- throw refuse3(props.path, `no group ${String(props.group)} on this host`);
1029
+ throw refuse3(path, `no group ${String(declared.group)} on this host`);
1001
1030
  }
1002
- return {
1003
- bytes,
1004
- mode: props.mode ?? DEFAULT_MODE,
1005
- sha256: sha256Hex(bytes),
1006
- ...uid === undefined ? {} : { uid },
1007
- ...gid === undefined ? {} : { gid }
1008
- };
1031
+ return { ...uid === undefined ? {} : { uid }, ...gid === undefined ? {} : { gid } };
1009
1032
  };
1010
1033
  var readFileAttributes = async (runner, path) => {
1011
1034
  const stat = await runner.stat(path);
@@ -1026,52 +1049,64 @@ var writeOptionsOf = (want) => ({
1026
1049
  ...want.uid === undefined ? {} : { uid: want.uid },
1027
1050
  ...want.gid === undefined ? {} : { gid: want.gid }
1028
1051
  });
1029
- var matches = (live, want) => live.sha256 === want.sha256 && live.mode === want.mode && (want.uid === undefined || live.uid === want.uid) && (want.gid === undefined || live.gid === want.gid);
1030
- var diffFile = async (runner, news, output) => {
1031
- const want = await desiredFile(runner, news);
1052
+ var fileMatches = (live, want) => live.sha256 === want.sha256 && live.mode === want.mode && (want.uid === undefined || live.uid === want.uid) && (want.gid === undefined || live.gid === want.gid);
1053
+ var diffTarget = async (runner, want, output) => {
1032
1054
  const writes = async (diff) => {
1033
- await runner.checkWrite?.(news.path, writeOptionsOf(want));
1055
+ await runner.checkWrite?.(want.path, writeOptionsOf(want));
1034
1056
  return diff;
1035
1057
  };
1036
- if (news.path !== output.path)
1037
- return writes({ action: "replace" });
1058
+ if (want.path !== output.path) {
1059
+ const same = await oneFile(runner, want.path, output.path);
1060
+ return writes({ action: same === true ? "update" : "replace" });
1061
+ }
1038
1062
  if (want.sha256 !== output.sha256)
1039
1063
  return writes({ action: "update" });
1040
- const live = await readFileAttributes(runner, news.path);
1041
- return live !== undefined && matches(live, want) ? { action: "noop" } : writes({ action: "update" });
1064
+ const live = await readFileAttributes(runner, want.path);
1065
+ return live !== undefined && fileMatches(live, want) ? { action: "noop" } : writes({ action: "update" });
1042
1066
  };
1043
- var reconcileFile = async (runner, props, output, adopt = false) => {
1044
- const want = await desiredFile(runner, props);
1067
+ var convergeFile = async (runner, spec, output, adopt = false) => {
1068
+ const { want, refuse: refuse3 } = spec;
1069
+ const path = want.path;
1045
1070
  if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
1046
- throw refuse3(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (sudoRunner() with this path under a prefix). This " + "provider never calls sudo itself.");
1071
+ throw refuse3(path, `owner ${String(spec.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (sudoRunner() with this path under a prefix). This " + "provider never calls sudo itself.");
1047
1072
  }
1048
- const stat = await runner.stat(props.path);
1073
+ const stat = await runner.stat(path);
1049
1074
  if (stat !== undefined && stat.kind !== "file") {
1050
- throw refuse3(props.path, `is a ${stat.kind}; remove it from whatever owns it before declaring it here`);
1075
+ throw refuse3(path, `is a ${stat.kind}; remove it from whatever owns it before declaring it here`);
1051
1076
  }
1052
- const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
1053
- const moved = output !== undefined && output.path !== props.path;
1077
+ const before = stat === undefined ? undefined : await readFileAttributes(runner, path);
1078
+ const moved = output !== undefined && output.path !== path;
1054
1079
  const prior = moved ? undefined : output;
1055
1080
  const takeOver = adopt && output === undefined;
1056
- if (prior === undefined && before !== undefined && !matches(before, want) && !takeOver) {
1057
- throw refuse3(props.path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
1081
+ if (prior === undefined && before !== undefined && !fileMatches(before, want) && !takeOver) {
1082
+ throw refuse3(path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
1058
1083
  }
1059
- if (before === undefined || !matches(before, want)) {
1060
- await runner.writeFileAtomic(props.path, want.bytes, writeOptionsOf(want));
1084
+ if (before === undefined || !fileMatches(before, want)) {
1085
+ const bytes = await spec.bytesFor(before);
1086
+ if (sha256Hex(bytes) !== want.sha256) {
1087
+ throw refuse3(path, `the bytes to write do not hash to ${want.sha256}. Nothing was written.`);
1088
+ }
1089
+ await runner.writeFileAtomic(path, bytes, writeOptionsOf(want));
1061
1090
  }
1062
- const after = await readFileAttributes(runner, props.path);
1063
- if (after === undefined || !matches(after, want)) {
1091
+ const rollBack = async () => {
1064
1092
  if (before === undefined)
1065
- await runner.removeFile(props.path).catch(() => {
1093
+ await runner.removeFile(path).catch(() => {
1066
1094
  return;
1067
1095
  });
1068
- throw refuse3(props.path, "the write returned but the file on disk does not match the declaration");
1096
+ };
1097
+ const after = await readFileAttributes(runner, path).catch(async (cause) => {
1098
+ await rollBack();
1099
+ throw refuse3(path, `the write returned but reading it back failed: ${String(cause)}`);
1100
+ });
1101
+ if (after === undefined || !fileMatches(after, want)) {
1102
+ await rollBack();
1103
+ throw refuse3(path, "the write returned but the file on disk does not match the declaration");
1069
1104
  }
1070
1105
  if (moved)
1071
- await deleteFile(runner, output);
1106
+ await leaveOldPath(runner, output.path, path, () => removeWholeFile(runner, output, refuse3), refuse3);
1072
1107
  return after;
1073
1108
  };
1074
- var deleteFile = async (runner, output) => {
1109
+ var removeWholeFile = async (runner, output, refuse3) => {
1075
1110
  const stat = await runner.stat(output.path);
1076
1111
  if (stat === undefined)
1077
1112
  return;
@@ -1080,6 +1115,61 @@ var deleteFile = async (runner, output) => {
1080
1115
  await runner.removeFile(output.path);
1081
1116
  };
1082
1117
 
1118
+ // src/launchd/host-file-form.ts
1119
+ var DEFAULT_MODE = 420;
1120
+ var identityProblems = (name, value) => {
1121
+ if (value === undefined)
1122
+ return [];
1123
+ if (typeof value === "number") {
1124
+ return Number.isSafeInteger(value) && value >= 0 ? [] : [`${name} must be a non-negative integer id`];
1125
+ }
1126
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value) ? [] : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
1127
+ };
1128
+ var pathProblems = (path) => {
1129
+ const found = [];
1130
+ if (!path.startsWith("/"))
1131
+ found.push("path must be absolute");
1132
+ if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
1133
+ found.push('path must be normalised (no trailing slash, no empty, "." or ".." segments)');
1134
+ }
1135
+ if (path.includes("\x00"))
1136
+ found.push("path contains NUL");
1137
+ return found;
1138
+ };
1139
+ var fileProblems = (props) => {
1140
+ const found = pathProblems(props.path);
1141
+ const mode = props.mode ?? DEFAULT_MODE;
1142
+ if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
1143
+ found.push("mode must be 0–0o7777");
1144
+ found.push(...identityProblems("owner", props.owner));
1145
+ found.push(...identityProblems("group", props.group));
1146
+ found.push(...fileSecretProblems(props.content));
1147
+ return found;
1148
+ };
1149
+
1150
+ // src/launchd/host-file-lifecycle.ts
1151
+ var refuse3 = (path, message) => new Error(`Host.File ${path}: ${message}`);
1152
+ var desiredFile = async (runner, props) => {
1153
+ const found = fileProblems(props);
1154
+ if (found.length > 0)
1155
+ throw refuse3(props.path, found.join("; "));
1156
+ const bytes = new TextEncoder().encode(props.content);
1157
+ return {
1158
+ bytes,
1159
+ mode: props.mode ?? DEFAULT_MODE,
1160
+ path: props.path,
1161
+ sha256: sha256Hex(bytes),
1162
+ ...await resolveIds(runner, props, props.path, refuse3)
1163
+ };
1164
+ };
1165
+ var diffFile = async (runner, news, output) => diffTarget(runner, await desiredFile(runner, news), output);
1166
+ var reconcileFile = async (runner, props, output, adopt = false) => {
1167
+ const want = await desiredFile(runner, props);
1168
+ const spec = { bytesFor: async () => want.bytes, owner: props.owner, refuse: refuse3, want };
1169
+ return convergeFile(runner, spec, output, adopt);
1170
+ };
1171
+ var deleteFile = (runner, output) => removeWholeFile(runner, output, refuse3);
1172
+
1083
1173
  // src/launchd/host-file.ts
1084
1174
  var HostFile = Resource2("Host.File");
1085
1175
  var HostFileProvider = () => Provider2.effect(HostFile, Effect7.gen(function* () {
@@ -1098,7 +1188,9 @@ var HostFileProvider = () => Provider2.effect(HostFile, Effect7.gen(function* ()
1098
1188
  if (isResolved2(news))
1099
1189
  return lift(() => diffFile(runner, news, output));
1100
1190
  const path = resolvedString(news, "path");
1101
- return Effect7.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
1191
+ if (path === undefined || path === output.path)
1192
+ return Effect7.succeed(undefined);
1193
+ return lift(async () => await oneFile(runner, path, output.path) === true ? undefined : { action: "replace" });
1102
1194
  },
1103
1195
  reconcile: ({ fqn, instanceId, news, output }) => Effect7.flatMap(adoptsAtApply({ fqn, instanceId, output }), (adopt) => lift(() => reconcileFile(runner, news, output, adopt))),
1104
1196
  delete: ({ output }) => lift(() => deleteFile(runner, output))
@@ -1128,11 +1220,13 @@ export {
1128
1220
  CaddyConfigProvider,
1129
1221
  CaddyUnreachableError,
1130
1222
  DEFAULT_ADMIN_ADDRESS,
1223
+ accessForwardAuth,
1131
1224
  caddyAdminLayer,
1132
1225
  caddyProviders,
1133
1226
  caddyWithFile,
1134
- localCaddyAdmin
1227
+ localCaddyAdmin,
1228
+ verifierProblems
1135
1229
  };
1136
1230
 
1137
- //# debugId=13AB2C6EDF123C4E64756E2164756E21
1231
+ //# debugId=A91D75C3B7D7145864756E2164756E21
1138
1232
  //# sourceMappingURL=index.js.map