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