@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.
- package/README.md +68 -55
- 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts +36 -0
- package/dist/launchd/fake-runner.d.ts.map +1 -0
- package/dist/launchd/host-effect.d.ts +19 -0
- package/dist/launchd/host-effect.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +24 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -0
- package/dist/launchd/host-file-lifecycle.d.ts +29 -0
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
- package/dist/launchd/host-file.d.ts +25 -0
- package/dist/launchd/host-file.d.ts.map +1 -0
- package/dist/launchd/host-lookup.d.ts +27 -0
- package/dist/launchd/host-lookup.d.ts.map +1 -0
- package/dist/launchd/index.d.ts +21 -0
- package/dist/launchd/index.d.ts.map +1 -0
- package/dist/launchd/index.js +969 -0
- package/dist/launchd/index.js.map +25 -0
- package/dist/launchd/job-form.d.ts +90 -0
- package/dist/launchd/job-form.d.ts.map +1 -0
- package/dist/launchd/job-lifecycle.d.ts +34 -0
- package/dist/launchd/job-lifecycle.d.ts.map +1 -0
- package/dist/launchd/job-preflight.d.ts +50 -0
- package/dist/launchd/job-preflight.d.ts.map +1 -0
- package/dist/launchd/job-validate.d.ts +25 -0
- package/dist/launchd/job-validate.d.ts.map +1 -0
- package/dist/launchd/job.d.ts +34 -0
- package/dist/launchd/job.d.ts.map +1 -0
- package/dist/launchd/launchctl.d.ts +62 -0
- package/dist/launchd/launchctl.d.ts.map +1 -0
- package/dist/launchd/local-runner.d.ts +7 -0
- package/dist/launchd/local-runner.d.ts.map +1 -0
- package/dist/launchd/plist.d.ts +41 -0
- package/dist/launchd/plist.d.ts.map +1 -0
- package/dist/launchd/providers.d.ts +15 -0
- package/dist/launchd/providers.d.ts.map +1 -0
- package/dist/launchd/runner.d.ts +91 -0
- package/dist/launchd/runner.d.ts.map +1 -0
- package/dist/launchd/secret-tripwire.d.ts +24 -0
- package/dist/launchd/secret-tripwire.d.ts.map +1 -0
- package/dist/openbao/auth-method-form.d.ts +7 -0
- package/dist/openbao/auth-method-form.d.ts.map +1 -1
- package/dist/openbao/auth-method-reconcile.d.ts +15 -0
- package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
- package/dist/openbao/auth-method.d.ts +6 -0
- package/dist/openbao/auth-method.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role.d.ts.map +1 -1
- package/dist/openbao/auth-token-form.d.ts +48 -0
- package/dist/openbao/auth-token-form.d.ts.map +1 -0
- package/dist/openbao/bao-identity.d.ts +81 -0
- package/dist/openbao/bao-identity.d.ts.map +1 -0
- package/dist/openbao/bao-namespace.d.ts +20 -0
- package/dist/openbao/bao-namespace.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role.d.ts.map +1 -1
- package/dist/openbao/cloudflare-roles-expand.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/host-approles.d.ts +62 -0
- package/dist/openbao/host-approles.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +19 -0
- package/dist/openbao/index.d.ts.map +1 -1
- package/dist/openbao/index.js +1518 -310
- package/dist/openbao/index.js.map +40 -17
- package/dist/openbao/jwt-config-form.d.ts +51 -0
- package/dist/openbao/jwt-config-form.d.ts.map +1 -0
- package/dist/openbao/jwt-config.d.ts +25 -0
- package/dist/openbao/jwt-config.d.ts.map +1 -0
- package/dist/openbao/jwt-role-form.d.ts +91 -0
- package/dist/openbao/jwt-role-form.d.ts.map +1 -0
- package/dist/openbao/jwt-role.d.ts +31 -0
- package/dist/openbao/jwt-role.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role-form.d.ts +68 -0
- package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role.d.ts +25 -0
- package/dist/openbao/kubernetes-role.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
- package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement.d.ts +41 -0
- package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
- package/dist/openbao/mfa-totp-form.d.ts +52 -0
- package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
- package/dist/openbao/mfa-totp.d.ts +33 -0
- package/dist/openbao/mfa-totp.d.ts.map +1 -0
- package/dist/openbao/mfa-wire.d.ts +63 -0
- package/dist/openbao/mfa-wire.d.ts.map +1 -0
- package/dist/openbao/mount-form.d.ts +7 -0
- package/dist/openbao/mount-form.d.ts.map +1 -1
- package/dist/openbao/mount-move.d.ts +49 -0
- package/dist/openbao/mount-move.d.ts.map +1 -0
- package/dist/openbao/mount-path.d.ts +8 -4
- package/dist/openbao/mount-path.d.ts.map +1 -1
- package/dist/openbao/mount-reconcile.d.ts +16 -0
- package/dist/openbao/mount-reconcile.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +9 -0
- package/dist/openbao/mount.d.ts.map +1 -1
- package/dist/openbao/pki-role-canonical.d.ts +46 -0
- package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +38 -30
- package/dist/openbao/pki-role-form.d.ts.map +1 -1
- package/dist/openbao/pki-role.d.ts.map +1 -1
- 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/remount-wire.d.ts +44 -0
- package/dist/openbao/remount-wire.d.ts.map +1 -0
- package/dist/openbao/rename.d.ts +106 -0
- package/dist/openbao/rename.d.ts.map +1 -0
- package/dist/openbao/role-reconcile.d.ts +40 -0
- package/dist/openbao/role-reconcile.d.ts.map +1 -0
- package/docs/caddy.md +180 -0
- package/docs/launchd.md +167 -0
- package/docs/mesh-node.md +188 -0
- package/package.json +12 -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 +2 -0
- package/src/launchd/fake-runner.ts +176 -0
- package/src/launchd/host-effect.ts +28 -0
- package/src/launchd/host-file-form.ts +71 -0
- package/src/launchd/host-file-lifecycle.test.ts +198 -0
- package/src/launchd/host-file-lifecycle.ts +164 -0
- package/src/launchd/host-file.ts +64 -0
- package/src/launchd/host-lookup.test.ts +92 -0
- package/src/launchd/host-lookup.ts +94 -0
- package/src/launchd/index.ts +26 -0
- package/src/launchd/job-form.test.ts +171 -0
- package/src/launchd/job-form.ts +180 -0
- package/src/launchd/job-identity.test.ts +147 -0
- package/src/launchd/job-lifecycle.test.ts +220 -0
- package/src/launchd/job-lifecycle.ts +194 -0
- package/src/launchd/job-preflight.ts +152 -0
- package/src/launchd/job-validate.ts +134 -0
- package/src/launchd/job.ts +96 -0
- package/src/launchd/launchctl.test.ts +181 -0
- package/src/launchd/launchctl.ts +188 -0
- package/src/launchd/local-runner.test.ts +125 -0
- package/src/launchd/local-runner.ts +143 -0
- package/src/launchd/plist.test.ts +186 -0
- package/src/launchd/plist.ts +156 -0
- package/src/launchd/providers.test.ts +222 -0
- package/src/launchd/providers.ts +21 -0
- package/src/launchd/runner.ts +95 -0
- package/src/launchd/secret-tripwire.test.ts +110 -0
- package/src/launchd/secret-tripwire.ts +81 -0
- package/src/openbao/README.md +181 -0
- package/src/openbao/REPLACE.md +163 -0
- package/src/openbao/auth-method-form.ts +7 -0
- package/src/openbao/auth-method-reconcile.ts +74 -0
- package/src/openbao/auth-method.ts +20 -41
- package/src/openbao/auth-role-form.test.ts +16 -1
- package/src/openbao/auth-role-form.ts +11 -0
- package/src/openbao/auth-role.ts +12 -0
- package/src/openbao/auth-token-form.ts +161 -0
- package/src/openbao/bao-identity.test.ts +131 -0
- package/src/openbao/bao-identity.ts +188 -0
- package/src/openbao/bao-namespace.ts +32 -0
- package/src/openbao/cloudflare-role-wire.test.ts +8 -34
- package/src/openbao/cloudflare-role.ts +30 -8
- package/src/openbao/cloudflare-roles-expand.ts +2 -5
- package/src/openbao/fake-engines.ts +119 -0
- package/src/openbao/fake-stack.ts +112 -0
- package/src/openbao/host-approles.test.ts +121 -0
- package/src/openbao/host-approles.ts +126 -0
- package/src/openbao/index.ts +19 -0
- package/src/openbao/jwt-config-form.ts +161 -0
- package/src/openbao/jwt-config.ts +85 -0
- package/src/openbao/jwt-role-form.test.ts +130 -0
- package/src/openbao/jwt-role-form.ts +231 -0
- package/src/openbao/jwt-role.ts +102 -0
- package/src/openbao/kubernetes-role-form.test.ts +82 -0
- package/src/openbao/kubernetes-role-form.ts +149 -0
- package/src/openbao/kubernetes-role.ts +94 -0
- package/src/openbao/mfa-enforcement-form.ts +147 -0
- package/src/openbao/mfa-enforcement.ts +143 -0
- package/src/openbao/mfa-totp-form.ts +155 -0
- package/src/openbao/mfa-totp.ts +118 -0
- package/src/openbao/mfa-wire.ts +117 -0
- package/src/openbao/mfa.test.ts +200 -0
- package/src/openbao/mount-form.ts +9 -1
- package/src/openbao/mount-move.test.ts +164 -0
- package/src/openbao/mount-move.ts +113 -0
- package/src/openbao/mount-path.test.ts +11 -1
- package/src/openbao/mount-path.ts +17 -11
- package/src/openbao/mount-reconcile.ts +75 -0
- package/src/openbao/mount.ts +26 -43
- package/src/openbao/pki-role-canonical.ts +163 -0
- package/src/openbao/pki-role-form.test.ts +112 -0
- package/src/openbao/pki-role-form.ts +76 -110
- package/src/openbao/pki-role.ts +12 -27
- 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/remount-wire.test.ts +87 -0
- package/src/openbao/remount-wire.ts +112 -0
- package/src/openbao/rename-occupied.test.ts +166 -0
- package/src/openbao/rename.ts +162 -0
- package/src/openbao/role-reconcile.test.ts +104 -0
- package/src/openbao/role-reconcile.ts +84 -0
- package/src/openbao/role-rename.test.ts +205 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
// src/caddy/admin.ts
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
|
|
5
|
+
class CaddyUnreachableError extends Error {
|
|
6
|
+
name = "CaddyUnreachableError";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class CaddyAdminService extends Context.Service()("homeflare/caddy/CaddyAdmin") {
|
|
10
|
+
}
|
|
11
|
+
var caddyAdminLayer = (admin) => Layer.succeed(CaddyAdminService, admin);
|
|
12
|
+
// src/launchd/job-form.ts
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
// src/launchd/plist.ts
|
|
16
|
+
class PlistError extends Error {
|
|
17
|
+
path;
|
|
18
|
+
constructor(path, detail) {
|
|
19
|
+
super(`plist ${path === "" ? "(root)" : path}: ${detail}`);
|
|
20
|
+
this.path = path;
|
|
21
|
+
this.name = "PlistError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
var HEADER = `<?xml version="1.0" encoding="UTF-8"?>
|
|
25
|
+
` + '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' + `"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
26
|
+
` + `<plist version="1.0">
|
|
27
|
+
`;
|
|
28
|
+
var MAX_DEPTH = 32;
|
|
29
|
+
var FORBIDDEN = /[\x00-\x08\x0B\x0C\x0E-\x1F\uD800-\uDFFF\uFFFE\uFFFF]/u;
|
|
30
|
+
var escapeText = (text, path) => {
|
|
31
|
+
const bad = FORBIDDEN.exec(text);
|
|
32
|
+
if (bad !== null) {
|
|
33
|
+
const code = bad[0].codePointAt(0) ?? 0;
|
|
34
|
+
throw new PlistError(path, `U+${code.toString(16).toUpperCase().padStart(4, "0")} cannot appear in XML 1.0`);
|
|
35
|
+
}
|
|
36
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\r", " ");
|
|
37
|
+
};
|
|
38
|
+
var isPlainObject = (value) => {
|
|
39
|
+
const proto = Object.getPrototypeOf(value);
|
|
40
|
+
return proto === Object.prototype || proto === null;
|
|
41
|
+
};
|
|
42
|
+
var renderValue = (value, path, depth, out) => {
|
|
43
|
+
const pad = "\t".repeat(depth);
|
|
44
|
+
if (depth > MAX_DEPTH)
|
|
45
|
+
throw new PlistError(path, `nested deeper than ${String(MAX_DEPTH)}`);
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
out.push(`${pad}<string>${escapeText(value, path)}</string>`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "boolean") {
|
|
51
|
+
out.push(`${pad}<${value ? "true" : "false"}/>`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
if (!Number.isSafeInteger(value)) {
|
|
56
|
+
throw new PlistError(path, `${String(value)} is not a safe integer (no <real> support)`);
|
|
57
|
+
}
|
|
58
|
+
out.push(`${pad}<integer>${String(value === 0 ? 0 : value)}</integer>`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
if (value.length === 0) {
|
|
63
|
+
out.push(`${pad}<array/>`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
out.push(`${pad}<array>`);
|
|
67
|
+
for (const [index, item] of value.entries()) {
|
|
68
|
+
renderValue(item, `${path}[${String(index)}]`, depth + 1, out);
|
|
69
|
+
}
|
|
70
|
+
out.push(`${pad}</array>`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "object" && value !== null && isPlainObject(value)) {
|
|
74
|
+
renderDict(value, path, depth, out);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const kind = value === null ? "null" : typeof value;
|
|
78
|
+
throw new PlistError(path, `${kind} has no plist form — omit the key instead`);
|
|
79
|
+
};
|
|
80
|
+
var renderDict = (dict, path, depth, out) => {
|
|
81
|
+
const pad = "\t".repeat(depth);
|
|
82
|
+
const keys = Object.keys(dict).sort();
|
|
83
|
+
if (keys.length === 0) {
|
|
84
|
+
out.push(`${pad}<dict/>`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
out.push(`${pad}<dict>`);
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const child = path === "" ? key : `${path}.${key}`;
|
|
90
|
+
out.push(`${pad} <key>${escapeText(key, child)}</key>`);
|
|
91
|
+
renderValue(dict[key], child, depth + 1, out);
|
|
92
|
+
}
|
|
93
|
+
out.push(`${pad}</dict>`);
|
|
94
|
+
};
|
|
95
|
+
var renderPlist = (dict) => {
|
|
96
|
+
if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !isPlainObject(dict)) {
|
|
97
|
+
throw new PlistError("", "the top level of a launchd plist must be a dict");
|
|
98
|
+
}
|
|
99
|
+
const out = [];
|
|
100
|
+
renderDict(dict, "", 0, out);
|
|
101
|
+
return `${HEADER}${out.join(`
|
|
102
|
+
`)}
|
|
103
|
+
</plist>
|
|
104
|
+
`;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/launchd/job-form.ts
|
|
108
|
+
var parseDomain = (domain) => {
|
|
109
|
+
if (domain === "system")
|
|
110
|
+
return { kind: "system" };
|
|
111
|
+
const match = /^gui\/(\d+)$/.exec(domain);
|
|
112
|
+
return match?.[1] === undefined ? undefined : { kind: "gui", uid: Number(match[1]) };
|
|
113
|
+
};
|
|
114
|
+
var serviceTarget = (label, domain) => `${domain}/${label}`;
|
|
115
|
+
var plistPathFor = (label, domain, home) => {
|
|
116
|
+
if (domain.kind === "system")
|
|
117
|
+
return `/Library/LaunchDaemons/${label}.plist`;
|
|
118
|
+
if (home === undefined || !home.startsWith("/")) {
|
|
119
|
+
throw new Error(`gui/${String(domain.uid)}: no home directory for that uid on this host`);
|
|
120
|
+
}
|
|
121
|
+
return `${home}/Library/LaunchAgents/${label}.plist`;
|
|
122
|
+
};
|
|
123
|
+
var OWNED_KEYS = [
|
|
124
|
+
"EnvironmentVariables",
|
|
125
|
+
"GroupName",
|
|
126
|
+
"KeepAlive",
|
|
127
|
+
"Label",
|
|
128
|
+
"Program",
|
|
129
|
+
"ProgramArguments",
|
|
130
|
+
"RunAtLoad",
|
|
131
|
+
"StandardErrorPath",
|
|
132
|
+
"StandardOutPath",
|
|
133
|
+
"StartCalendarInterval",
|
|
134
|
+
"StartInterval",
|
|
135
|
+
"ThrottleInterval",
|
|
136
|
+
"UserName",
|
|
137
|
+
"WorkingDirectory"
|
|
138
|
+
];
|
|
139
|
+
var calendarDict = (entry) => {
|
|
140
|
+
const dict = {};
|
|
141
|
+
if (entry.minute !== undefined)
|
|
142
|
+
dict["Minute"] = entry.minute;
|
|
143
|
+
if (entry.hour !== undefined)
|
|
144
|
+
dict["Hour"] = entry.hour;
|
|
145
|
+
if (entry.day !== undefined)
|
|
146
|
+
dict["Day"] = entry.day;
|
|
147
|
+
if (entry.weekday !== undefined)
|
|
148
|
+
dict["Weekday"] = entry.weekday;
|
|
149
|
+
if (entry.month !== undefined)
|
|
150
|
+
dict["Month"] = entry.month;
|
|
151
|
+
return dict;
|
|
152
|
+
};
|
|
153
|
+
var keepAliveValue = (keepAlive) => {
|
|
154
|
+
if (typeof keepAlive === "boolean")
|
|
155
|
+
return keepAlive;
|
|
156
|
+
const dict = {};
|
|
157
|
+
if (keepAlive.successfulExit !== undefined)
|
|
158
|
+
dict["SuccessfulExit"] = keepAlive.successfulExit;
|
|
159
|
+
if (keepAlive.crashed !== undefined)
|
|
160
|
+
dict["Crashed"] = keepAlive.crashed;
|
|
161
|
+
return dict;
|
|
162
|
+
};
|
|
163
|
+
var jobPlistDict = (props) => {
|
|
164
|
+
const dict = { ...props.extraKeys };
|
|
165
|
+
dict["Label"] = props.label;
|
|
166
|
+
dict["ProgramArguments"] = [...props.programArguments];
|
|
167
|
+
const env = props.environment ?? {};
|
|
168
|
+
if (Object.keys(env).length > 0)
|
|
169
|
+
dict["EnvironmentVariables"] = { ...env };
|
|
170
|
+
if (props.runAtLoad !== undefined)
|
|
171
|
+
dict["RunAtLoad"] = props.runAtLoad;
|
|
172
|
+
if (props.keepAlive !== undefined)
|
|
173
|
+
dict["KeepAlive"] = keepAliveValue(props.keepAlive);
|
|
174
|
+
if (props.startInterval !== undefined)
|
|
175
|
+
dict["StartInterval"] = props.startInterval;
|
|
176
|
+
const calendar = props.startCalendarInterval;
|
|
177
|
+
if (calendar !== undefined) {
|
|
178
|
+
dict["StartCalendarInterval"] = Array.isArray(calendar) ? calendar.map(calendarDict) : calendarDict(calendar);
|
|
179
|
+
}
|
|
180
|
+
if (props.throttleInterval !== undefined)
|
|
181
|
+
dict["ThrottleInterval"] = props.throttleInterval;
|
|
182
|
+
if (props.standardOutPath !== undefined)
|
|
183
|
+
dict["StandardOutPath"] = props.standardOutPath;
|
|
184
|
+
if (props.standardErrorPath !== undefined)
|
|
185
|
+
dict["StandardErrorPath"] = props.standardErrorPath;
|
|
186
|
+
if (props.workingDirectory !== undefined)
|
|
187
|
+
dict["WorkingDirectory"] = props.workingDirectory;
|
|
188
|
+
if (props.userName !== undefined)
|
|
189
|
+
dict["UserName"] = props.userName;
|
|
190
|
+
if (props.groupName !== undefined)
|
|
191
|
+
dict["GroupName"] = props.groupName;
|
|
192
|
+
return dict;
|
|
193
|
+
};
|
|
194
|
+
var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
195
|
+
var renderJob = (props) => {
|
|
196
|
+
const text = renderPlist(jobPlistDict(props));
|
|
197
|
+
const bytes = new TextEncoder().encode(text);
|
|
198
|
+
return { bytes, sha256: sha256Hex(bytes), text };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/caddy/digest.ts
|
|
202
|
+
var sorted = (value) => {
|
|
203
|
+
if (Array.isArray(value))
|
|
204
|
+
return value.map(sorted);
|
|
205
|
+
if (typeof value === "object" && value !== null) {
|
|
206
|
+
const object = value;
|
|
207
|
+
const out = {};
|
|
208
|
+
for (const key of Object.keys(object).sort())
|
|
209
|
+
out[key] = sorted(object[key] ?? null);
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
};
|
|
214
|
+
var parseConfig = (text) => JSON.parse(text);
|
|
215
|
+
var canonicalConfig = (config) => JSON.stringify(sorted(config ?? null));
|
|
216
|
+
var configDigest = (config) => sha256Hex(canonicalConfig(config));
|
|
217
|
+
|
|
218
|
+
// src/caddy/admin-calls.ts
|
|
219
|
+
class CaddyAdminError extends Error {
|
|
220
|
+
status;
|
|
221
|
+
method;
|
|
222
|
+
path;
|
|
223
|
+
constructor(status, method, path, reason) {
|
|
224
|
+
super(`Caddy ${method} ${path} → ${String(status)}: ${reason}`);
|
|
225
|
+
this.status = status;
|
|
226
|
+
this.method = method;
|
|
227
|
+
this.path = path;
|
|
228
|
+
this.name = "CaddyAdminError";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
var CADDYFILE = "text/caddyfile";
|
|
232
|
+
var ERROR_OPEN = '{"error":';
|
|
233
|
+
var describe = (warning) => `${warning.file ?? "Caddyfile"}:${String(warning.line ?? 0)}: ${warning.message ?? "(no message)"}`;
|
|
234
|
+
var errorIn = (body) => {
|
|
235
|
+
const at = body.lastIndexOf(ERROR_OPEN);
|
|
236
|
+
if (at === -1)
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(body.slice(at));
|
|
240
|
+
return typeof parsed.error === "string" ? parsed.error : undefined;
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var warningsIn = (text) => {
|
|
246
|
+
if (text.trim() === "")
|
|
247
|
+
return [];
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(text);
|
|
250
|
+
return Array.isArray(parsed) ? parsed.map(describe) : [];
|
|
251
|
+
} catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var fail = (response, method, path) => {
|
|
256
|
+
const reason = errorIn(response.body) ?? (response.body.trim().slice(0, 500) || "(empty body)");
|
|
257
|
+
const shown = response.status === 200 ? `${reason} (error in a 200 body)` : reason;
|
|
258
|
+
throw new CaddyAdminError(response.status, method, path, shown);
|
|
259
|
+
};
|
|
260
|
+
var adaptCaddyfile = async (admin, caddyfile) => {
|
|
261
|
+
const response = await admin.request({
|
|
262
|
+
body: caddyfile,
|
|
263
|
+
headers: { "Content-Type": CADDYFILE },
|
|
264
|
+
method: "POST",
|
|
265
|
+
path: "/adapt"
|
|
266
|
+
});
|
|
267
|
+
if (response.status !== 200)
|
|
268
|
+
fail(response, "POST", "/adapt");
|
|
269
|
+
const parsed = JSON.parse(response.body);
|
|
270
|
+
return { config: parsed.result ?? null, warnings: (parsed.warnings ?? []).map(describe) };
|
|
271
|
+
};
|
|
272
|
+
var readRunningConfig = async (admin) => {
|
|
273
|
+
const response = await admin.request({ method: "GET", path: "/config/" });
|
|
274
|
+
if (response.status !== 200)
|
|
275
|
+
fail(response, "GET", "/config/");
|
|
276
|
+
return parseConfig(response.body);
|
|
277
|
+
};
|
|
278
|
+
var loadCaddyfile = async (admin, caddyfile, sourceFile) => {
|
|
279
|
+
const response = await admin.request({
|
|
280
|
+
body: caddyfile,
|
|
281
|
+
headers: {
|
|
282
|
+
"Content-Type": CADDYFILE,
|
|
283
|
+
...sourceFile === undefined ? {} : { "Caddy-Config-Source-Adapter": "caddyfile", "Caddy-Config-Source-File": sourceFile }
|
|
284
|
+
},
|
|
285
|
+
method: "POST",
|
|
286
|
+
path: "/load"
|
|
287
|
+
});
|
|
288
|
+
const refused = errorIn(response.body);
|
|
289
|
+
if (response.status !== 200 || refused !== undefined)
|
|
290
|
+
fail(response, "POST", "/load");
|
|
291
|
+
return warningsIn(response.body);
|
|
292
|
+
};
|
|
293
|
+
// src/caddy/config.ts
|
|
294
|
+
import { Resource } from "alchemy";
|
|
295
|
+
import { isResolved } from "alchemy/Diff";
|
|
296
|
+
import * as Provider from "alchemy/Provider";
|
|
297
|
+
import * as Effect2 from "effect/Effect";
|
|
298
|
+
|
|
299
|
+
// src/launchd/host-effect.ts
|
|
300
|
+
import * as Effect from "effect/Effect";
|
|
301
|
+
var lift = (step) => Effect.tryPromise({
|
|
302
|
+
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
|
|
303
|
+
try: step
|
|
304
|
+
});
|
|
305
|
+
var resolvedString = (news, key) => {
|
|
306
|
+
if (typeof news !== "object" || news === null)
|
|
307
|
+
return;
|
|
308
|
+
const value = news[key];
|
|
309
|
+
return typeof value === "string" ? value : undefined;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/caddy/admin-guard.ts
|
|
313
|
+
var DEFAULT_ADMIN_PORT = 2019;
|
|
314
|
+
var LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|\[?::1\]?)$/;
|
|
315
|
+
var parseListen = (listen) => {
|
|
316
|
+
if (listen.includes("{"))
|
|
317
|
+
return "a placeholder, which cannot be checked before it runs";
|
|
318
|
+
const unix = /^unix(?:gram|packet)?\/(.+?)(?:\|[0-7]{3,4})?$/.exec(listen);
|
|
319
|
+
if (unix !== null)
|
|
320
|
+
return { kind: "unix", path: unix[1] ?? "" };
|
|
321
|
+
if (/^[a-z0-9]+\//.test(listen) && !/^tcp[46]?\//.test(listen)) {
|
|
322
|
+
return `network ${JSON.stringify(listen.split("/")[0])}, not tcp or unix`;
|
|
323
|
+
}
|
|
324
|
+
const address = listen.replace(/^tcp[46]?\//, "");
|
|
325
|
+
const colon = address.lastIndexOf(":");
|
|
326
|
+
const port = Number(address.slice(colon + 1));
|
|
327
|
+
if (colon === -1 || !Number.isInteger(port) || port <= 0)
|
|
328
|
+
return "no single port";
|
|
329
|
+
return { host: address.slice(0, colon), kind: "tcp", port };
|
|
330
|
+
};
|
|
331
|
+
var unbracket = (host) => host.replace(/^\[(.*)\]$/, "$1");
|
|
332
|
+
var reaches = (hostHeader, listenHost) => {
|
|
333
|
+
const dialled = unbracket(hostHeader.slice(0, hostHeader.lastIndexOf(":")));
|
|
334
|
+
const listening = unbracket(listenHost);
|
|
335
|
+
return dialled === "localhost" || dialled === (listening === "localhost" ? "127.0.0.1" : listening);
|
|
336
|
+
};
|
|
337
|
+
var sameListener = (listen, transport) => listen.kind === "tcp" ? transport.kind === "tcp" && listen.port === transport.port && reaches(transport.hostHeader, listen.host) : transport.kind === "unix" && listen.path === transport.path;
|
|
338
|
+
var defaultOrigins = (port) => ["localhost", "[::1]", "127.0.0.1"].map((host) => `${host}:${String(port)}`);
|
|
339
|
+
var originHost = (origin) => {
|
|
340
|
+
if (!origin.includes("://"))
|
|
341
|
+
return origin;
|
|
342
|
+
try {
|
|
343
|
+
const url = new URL(origin);
|
|
344
|
+
return url.protocol === "http:" ? url.host : undefined;
|
|
345
|
+
} catch {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var listenProblem = (admin, transport) => {
|
|
350
|
+
if (admin.listen === undefined) {
|
|
351
|
+
const atDefault = transport.kind === "tcp" && transport.port === DEFAULT_ADMIN_PORT && reaches(transport.hostHeader, "localhost");
|
|
352
|
+
return atDefault ? undefined : "no `admin` address: Caddy would fall back to its default listener (localhost:2019, or " + "$CADDY_ADMIN), away from where this provider reaches it — declare `admin <address>`";
|
|
353
|
+
}
|
|
354
|
+
const listen = parseListen(admin.listen);
|
|
355
|
+
const shown = JSON.stringify(admin.listen);
|
|
356
|
+
if (typeof listen === "string") {
|
|
357
|
+
return `admin listen ${shown} is ${listen}; declare a literal loopback address`;
|
|
358
|
+
}
|
|
359
|
+
if (listen.kind === "tcp" && !LOOPBACK_HOST.test(listen.host)) {
|
|
360
|
+
return `admin listen ${shown} is not loopback — it would put the admin API on the network`;
|
|
361
|
+
}
|
|
362
|
+
return sameListener(listen, transport) ? undefined : `admin listen ${shown} moves the admin API away from where this provider reaches it`;
|
|
363
|
+
};
|
|
364
|
+
var adminProblems = (adapted, transport) => {
|
|
365
|
+
const admin = typeof adapted === "object" && adapted !== null ? adapted.admin ?? {} : {};
|
|
366
|
+
const found = [];
|
|
367
|
+
if (admin.disabled === true) {
|
|
368
|
+
return ["`admin off` would disable the admin API this resource manages Caddy through"];
|
|
369
|
+
}
|
|
370
|
+
if (admin.remote !== undefined)
|
|
371
|
+
found.push("`admin.remote` opens a network-facing admin API");
|
|
372
|
+
if (admin.config?.load !== undefined) {
|
|
373
|
+
found.push("`admin.config.load` makes Caddy pull its config from elsewhere, replacing this one");
|
|
374
|
+
}
|
|
375
|
+
const moved = listenProblem(admin, transport);
|
|
376
|
+
if (moved !== undefined)
|
|
377
|
+
found.push(moved);
|
|
378
|
+
if (transport.kind === "tcp") {
|
|
379
|
+
const allowed = admin.origins?.map(originHost) ?? defaultOrigins(transport.port);
|
|
380
|
+
if (!allowed.includes(transport.hostHeader)) {
|
|
381
|
+
const which = admin.origins === undefined ? `Caddy's default origins (no \`origins\` declared) ${JSON.stringify(allowed)}` : `admin origins ${JSON.stringify(admin.origins)}`;
|
|
382
|
+
found.push(`${which} do not allow Host ${transport.hostHeader}, which this provider sends — ` + "Caddy would refuse it every call after this one");
|
|
383
|
+
}
|
|
384
|
+
} else if (admin.enforce_origin === true) {
|
|
385
|
+
found.push("`enforce_origin` over a unix socket: this transport (like the Caddy CLI) sends no Origin " + "there, so Caddy would answer 403 to every call");
|
|
386
|
+
}
|
|
387
|
+
return found;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/launchd/secret-tripwire.ts
|
|
391
|
+
var ALTERNATIVE = "props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) " + "write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).";
|
|
392
|
+
var SECRET_NAME = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_KEY|PRIVATE_KEY|SECRET_KEY|CREDENTIALS?)$/i;
|
|
393
|
+
var INLINE_FLAG = /^--?[a-z0-9-]*(?:token|password|passwd|passphrase|secret|api-?key)=./i;
|
|
394
|
+
var BARE_FLAG = /^--?(?:[a-z0-9]+-)*(?:token|password|passwd|passphrase|secret|api-?key)$/i;
|
|
395
|
+
var PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
|
|
396
|
+
var keyLeaves = (value, path) => {
|
|
397
|
+
if (typeof value === "string")
|
|
398
|
+
return PRIVATE_KEY.test(value) ? [path] : [];
|
|
399
|
+
if (Array.isArray(value)) {
|
|
400
|
+
return value.flatMap((item, index) => keyLeaves(item, `${path}[${String(index)}]`));
|
|
401
|
+
}
|
|
402
|
+
if (typeof value === "object" && value !== null) {
|
|
403
|
+
return Object.entries(value).flatMap(([key, item]) => keyLeaves(item, `${path}.${key}`));
|
|
404
|
+
}
|
|
405
|
+
return [];
|
|
406
|
+
};
|
|
407
|
+
var jobSecretProblems = (environment, argv, extraKeys) => {
|
|
408
|
+
const found = [];
|
|
409
|
+
for (const [name, value] of Object.entries(environment ?? {})) {
|
|
410
|
+
if (SECRET_NAME.test(name))
|
|
411
|
+
found.push(`environment ${name} looks like a secret: ${ALTERNATIVE}`);
|
|
412
|
+
else if (PRIVATE_KEY.test(value))
|
|
413
|
+
found.push(`environment ${name} holds a private key: ${ALTERNATIVE}`);
|
|
414
|
+
}
|
|
415
|
+
for (const [index, arg] of argv.entries()) {
|
|
416
|
+
if (INLINE_FLAG.test(arg)) {
|
|
417
|
+
found.push(`programArguments[${String(index)}] passes a secret inline: ${ALTERNATIVE}`);
|
|
418
|
+
} else if (BARE_FLAG.test(arg) && index + 1 < argv.length) {
|
|
419
|
+
found.push(`programArguments[${String(index + 1)}] follows a secret flag: ${ALTERNATIVE}`);
|
|
420
|
+
} else if (PRIVATE_KEY.test(arg)) {
|
|
421
|
+
found.push(`programArguments[${String(index)}] holds a private key: ${ALTERNATIVE}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const path of keyLeaves(extraKeys ?? {}, "extraKeys")) {
|
|
425
|
+
found.push(`${path} holds a private key: ${ALTERNATIVE}`);
|
|
426
|
+
}
|
|
427
|
+
return found;
|
|
428
|
+
};
|
|
429
|
+
var fileSecretProblems = (content) => PRIVATE_KEY.test(content) ? [`content holds a private key: ${ALTERNATIVE}`] : [];
|
|
430
|
+
|
|
431
|
+
// src/caddy/caddyfile-tripwire.ts
|
|
432
|
+
var ALTERNATIVE2 = "props are stored unencrypted in Alchemy state. Use a runtime placeholder instead: {env.NAME} " + "(Caddy started with --envfile <file a secret renderer writes>) or {file./path/to/secret}.";
|
|
433
|
+
var SECRET_WORD = /^(?:token|api_token|auth_token|access_token|secret|client_secret|shared_secret|secret_key|password|passwd|passphrase|api_key|apikey|private_key|mac_key|hmac_key)$/i;
|
|
434
|
+
var DNS_DIRECTIVE = /^(?:dns|acme_dns)$/;
|
|
435
|
+
var HEADER_DIRECTIVE = /^(?:header|header_up|header_down|request_header)$/;
|
|
436
|
+
var CREDENTIAL_HEADER = /^[+-]?(?:authorization|proxy-authorization|cookie|x-api-key|api-key|x-auth-token|x-vault-token)$/i;
|
|
437
|
+
var SHAPES = [
|
|
438
|
+
[/^(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,})$/, "a GitHub token"],
|
|
439
|
+
[/^(?:AKIA|ASIA)[0-9A-Z]{16}$/, "an AWS access key id"],
|
|
440
|
+
[/^xox[abposr]-[A-Za-z0-9-]{10,}$/, "a Slack token"],
|
|
441
|
+
[/^sk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}$/, "an API secret key"],
|
|
442
|
+
[/^eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}$/, "a JWT"],
|
|
443
|
+
[/^\$2[abxy]?\$\d{2}\$[./A-Za-z0-9]{53}$/, "a bcrypt password hash"],
|
|
444
|
+
[/^\$argon2(?:id|i|d)\$/, "an argon2 password hash"]
|
|
445
|
+
];
|
|
446
|
+
var PLACEHOLDER = /\{[^{}\s]+\}/g;
|
|
447
|
+
var AUTH_SCHEME = /^(?:bearer|basic|token|digest)$/i;
|
|
448
|
+
var ENV_DEFAULT = /\{\$[^{}\s:]*:([^}]*)\}/g;
|
|
449
|
+
var withDefaults = (line) => line.replace(ENV_DEFAULT, "$1");
|
|
450
|
+
var tokensOf = (line) => {
|
|
451
|
+
const tokens = [];
|
|
452
|
+
let current = "";
|
|
453
|
+
let quote;
|
|
454
|
+
for (let index = 0;index < line.length; index += 1) {
|
|
455
|
+
const char = line[index] ?? "";
|
|
456
|
+
if (quote !== undefined) {
|
|
457
|
+
if (char === "\\" && quote === '"' && index + 1 < line.length) {
|
|
458
|
+
index += 1;
|
|
459
|
+
current += line[index] ?? "";
|
|
460
|
+
} else if (char === quote)
|
|
461
|
+
quote = undefined;
|
|
462
|
+
else
|
|
463
|
+
current += char;
|
|
464
|
+
} else if (char === '"' || char === "`") {
|
|
465
|
+
quote = char;
|
|
466
|
+
} else if (/\s/.test(char)) {
|
|
467
|
+
if (current !== "")
|
|
468
|
+
tokens.push(current);
|
|
469
|
+
current = "";
|
|
470
|
+
} else if (char === "#" && current === "") {
|
|
471
|
+
break;
|
|
472
|
+
} else
|
|
473
|
+
current += char;
|
|
474
|
+
}
|
|
475
|
+
if (current !== "")
|
|
476
|
+
tokens.push(current);
|
|
477
|
+
return tokens;
|
|
478
|
+
};
|
|
479
|
+
var placeholderOnly = (tokens) => tokens.flatMap((token) => token.replace(PLACEHOLDER, " ").split(/\s+/)).filter((part) => part !== "" && part !== "{" && part !== "}" && !AUTH_SCHEME.test(part)).length === 0;
|
|
480
|
+
var lineProblems = (tokens, where) => {
|
|
481
|
+
const [first = "", second] = tokens;
|
|
482
|
+
const found = [];
|
|
483
|
+
if (SECRET_WORD.test(first) && tokens.length > 1 && !placeholderOnly(tokens.slice(1))) {
|
|
484
|
+
found.push(`${where}: \`${first}\` has a literal value`);
|
|
485
|
+
}
|
|
486
|
+
if (DNS_DIRECTIVE.test(first) && tokens.length > 2 && !placeholderOnly(tokens.slice(2))) {
|
|
487
|
+
found.push(`${where}: \`${first} ${second ?? ""}\` has a literal credential`);
|
|
488
|
+
}
|
|
489
|
+
if (HEADER_DIRECTIVE.test(first)) {
|
|
490
|
+
const at = tokens.findIndex((token, index) => index > 0 && CREDENTIAL_HEADER.test(token));
|
|
491
|
+
if (at !== -1 && tokens.length > at + 1 && !placeholderOnly(tokens.slice(at + 1))) {
|
|
492
|
+
found.push(`${where}: \`${first} ${tokens[at] ?? ""}\` sets a literal credential`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const token of tokens) {
|
|
496
|
+
const shape = SHAPES.find(([pattern]) => pattern.test(token));
|
|
497
|
+
if (shape !== undefined)
|
|
498
|
+
found.push(`${where}: holds what looks like ${shape[1]}`);
|
|
499
|
+
}
|
|
500
|
+
return found;
|
|
501
|
+
};
|
|
502
|
+
var caddyfileSecretProblems = (caddyfile) => {
|
|
503
|
+
const found = [];
|
|
504
|
+
if (PRIVATE_KEY.test(caddyfile))
|
|
505
|
+
found.push("the Caddyfile holds a PEM private key");
|
|
506
|
+
for (const [index, line] of caddyfile.split(`
|
|
507
|
+
`).entries()) {
|
|
508
|
+
found.push(...lineProblems(tokensOf(withDefaults(line)), `line ${String(index + 1)}`));
|
|
509
|
+
}
|
|
510
|
+
return found.map((problem) => `${problem}: ${ALTERNATIVE2}`);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/caddy/config-form.ts
|
|
514
|
+
var absoluteNormalised = (path) => path.startsWith("/") && !path.endsWith("/") && !path.includes("\x00") && !path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..");
|
|
515
|
+
var configProblems = (props) => {
|
|
516
|
+
const found = [];
|
|
517
|
+
if (props.caddyfile.trim() === "") {
|
|
518
|
+
found.push("caddyfile is empty — loading it would stop every site Caddy serves");
|
|
519
|
+
}
|
|
520
|
+
if (props.sourceFile !== undefined && !absoluteNormalised(props.sourceFile)) {
|
|
521
|
+
found.push("sourceFile must be an absolute, normalised path");
|
|
522
|
+
}
|
|
523
|
+
found.push(...caddyfileSecretProblems(props.caddyfile));
|
|
524
|
+
return found;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/caddy/config-lifecycle.ts
|
|
528
|
+
var refuse = (admin, message) => new Error(`Caddy.Config at ${admin.endpoint}: ${message}`);
|
|
529
|
+
var short = (digest) => digest.slice(0, 12);
|
|
530
|
+
var servesNothing = (config) => {
|
|
531
|
+
const apps = typeof config === "object" && config !== null ? config.apps : undefined;
|
|
532
|
+
return typeof apps !== "object" || apps === null || Object.keys(apps).length === 0;
|
|
533
|
+
};
|
|
534
|
+
var desiredConfig = async (admin, props) => {
|
|
535
|
+
const found = configProblems(props);
|
|
536
|
+
if (found.length > 0)
|
|
537
|
+
throw refuse(admin, found.join("; "));
|
|
538
|
+
const adapted = await adaptCaddyfile(admin, props.caddyfile);
|
|
539
|
+
if (servesNothing(adapted.config)) {
|
|
540
|
+
throw refuse(admin, "the Caddyfile adapts to no apps — loading it would stop every site");
|
|
541
|
+
}
|
|
542
|
+
const guard = adminProblems(adapted.config, admin.listener);
|
|
543
|
+
if (guard.length > 0)
|
|
544
|
+
throw refuse(admin, guard.join("; "));
|
|
545
|
+
return { digest: configDigest(adapted.config), warnings: adapted.warnings };
|
|
546
|
+
};
|
|
547
|
+
var readLive = async (admin, sourceFile) => ({
|
|
548
|
+
configSha256: configDigest(await readRunningConfig(admin)),
|
|
549
|
+
endpoint: admin.endpoint,
|
|
550
|
+
...sourceFile === undefined ? {} : { sourceFile }
|
|
551
|
+
});
|
|
552
|
+
var diffConfig = async (admin, news, output) => {
|
|
553
|
+
const want = await desiredConfig(admin, news);
|
|
554
|
+
const live = await readLive(admin);
|
|
555
|
+
const converged = want.digest === live.configSha256 && live.configSha256 === output.configSha256 && news.sourceFile === output.sourceFile && admin.endpoint === output.endpoint;
|
|
556
|
+
return converged ? { action: "noop" } : { action: "update" };
|
|
557
|
+
};
|
|
558
|
+
var reconcileConfig = async (admin, props) => {
|
|
559
|
+
const want = await desiredConfig(admin, props);
|
|
560
|
+
const before = configDigest(await readRunningConfig(admin));
|
|
561
|
+
let warnings = want.warnings;
|
|
562
|
+
const loaded = before !== want.digest;
|
|
563
|
+
if (loaded) {
|
|
564
|
+
try {
|
|
565
|
+
warnings = await loadCaddyfile(admin, props.caddyfile, props.sourceFile);
|
|
566
|
+
} catch (cause) {
|
|
567
|
+
if (!(cause instanceof CaddyAdminError))
|
|
568
|
+
throw cause;
|
|
569
|
+
const after = await readLive(admin).catch(() => {
|
|
570
|
+
return;
|
|
571
|
+
});
|
|
572
|
+
const state = after === undefined ? "and the running config could not be read back" : after.configSha256 === before ? "Caddy kept the previous config (still serving)" : `and the running config is now ${short(after.configSha256)}, not the previous ${short(before)}`;
|
|
573
|
+
throw refuse(admin, `Caddy refused the Caddyfile — ${cause.message}; ${state}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const attributes = await readLive(admin, props.sourceFile);
|
|
577
|
+
if (attributes.configSha256 !== want.digest) {
|
|
578
|
+
throw refuse(admin, `after the load Caddy runs config ${short(attributes.configSha256)}, not the ` + `${short(want.digest)} this Caddyfile adapts to — another writer changed it, or it was not applied`);
|
|
579
|
+
}
|
|
580
|
+
return { attributes, loaded, warnings };
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/caddy/config.ts
|
|
584
|
+
var CaddyConfig = Resource("Caddy.Config", {
|
|
585
|
+
defaultRemovalPolicy: "retain"
|
|
586
|
+
});
|
|
587
|
+
var unreachable = (error) => error instanceof CaddyUnreachableError;
|
|
588
|
+
var planWithoutCaddy = (admin, fallback) => (error) => Effect2.as(Effect2.logWarning(`Caddy.Config: no Caddy at ${admin.endpoint} (${error.message}); planning without it — ` + "the Caddyfile is validated when the deploy reaches Caddy"), fallback);
|
|
589
|
+
var CaddyConfigProvider = () => Provider.effect(CaddyConfig, Effect2.gen(function* () {
|
|
590
|
+
const admin = yield* CaddyAdminService;
|
|
591
|
+
return CaddyConfig.Provider.of({
|
|
592
|
+
list: () => Effect2.succeed([]),
|
|
593
|
+
read: ({ olds }) => lift(() => readLive(admin, resolvedString(olds, "sourceFile"))).pipe(Effect2.catchIf(unreachable, planWithoutCaddy(admin, undefined))),
|
|
594
|
+
diff: ({ news, output }) => {
|
|
595
|
+
if (output === undefined)
|
|
596
|
+
return Effect2.succeed(undefined);
|
|
597
|
+
const update = planWithoutCaddy(admin, { action: "update" });
|
|
598
|
+
if (isResolved(news)) {
|
|
599
|
+
return lift(() => diffConfig(admin, news, output)).pipe(Effect2.catchIf(unreachable, update));
|
|
600
|
+
}
|
|
601
|
+
const caddyfile = resolvedString(news, "caddyfile");
|
|
602
|
+
if (caddyfile === undefined)
|
|
603
|
+
return Effect2.succeed(undefined);
|
|
604
|
+
return lift(async () => {
|
|
605
|
+
await diffConfig(admin, { caddyfile }, output);
|
|
606
|
+
return { action: "update" };
|
|
607
|
+
}).pipe(Effect2.catchIf(unreachable, update));
|
|
608
|
+
},
|
|
609
|
+
reconcile: ({ news }) => Effect2.gen(function* () {
|
|
610
|
+
const applied = yield* lift(() => reconcileConfig(admin, news));
|
|
611
|
+
for (const warning of applied.warnings) {
|
|
612
|
+
yield* Effect2.logWarning(`Caddy.Config at ${admin.endpoint}: ${warning}`);
|
|
613
|
+
}
|
|
614
|
+
return applied.attributes;
|
|
615
|
+
}),
|
|
616
|
+
delete: () => Effect2.void
|
|
617
|
+
});
|
|
618
|
+
}));
|
|
619
|
+
// src/caddy/local-admin.ts
|
|
620
|
+
import { request as httpRequest } from "node:http";
|
|
621
|
+
var DEFAULT_ADMIN_ADDRESS = "http://127.0.0.1:2019";
|
|
622
|
+
var UNIX = "unix://";
|
|
623
|
+
var HOST_PORT = /^(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\]):\d{1,5}$/;
|
|
624
|
+
var refuse2 = (address, why) => new Error(`Caddy admin address ${JSON.stringify(address)}: ${why}`);
|
|
625
|
+
var isLoopback = (hostname) => hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
626
|
+
var parseAdminAddress = (address, hostHeader) => {
|
|
627
|
+
if (hostHeader !== undefined && !HOST_PORT.test(hostHeader)) {
|
|
628
|
+
throw refuse2(address, `hostHeader ${JSON.stringify(hostHeader)} must be host:port`);
|
|
629
|
+
}
|
|
630
|
+
if (address.startsWith(UNIX)) {
|
|
631
|
+
const socketPath = address.slice(UNIX.length);
|
|
632
|
+
if (!socketPath.startsWith("/"))
|
|
633
|
+
throw refuse2(address, "the socket path must be absolute");
|
|
634
|
+
return { listener: { kind: "unix", path: socketPath }, target: { kind: "unix", socketPath } };
|
|
635
|
+
}
|
|
636
|
+
let url;
|
|
637
|
+
try {
|
|
638
|
+
url = new URL(address);
|
|
639
|
+
} catch {
|
|
640
|
+
throw refuse2(address, "not a URL (http://127.0.0.1:2019 or unix:///path)");
|
|
641
|
+
}
|
|
642
|
+
if (url.protocol !== "http:")
|
|
643
|
+
throw refuse2(address, "only http:// (loopback) or unix://");
|
|
644
|
+
if (!isLoopback(url.hostname)) {
|
|
645
|
+
throw refuse2(address, "not loopback — the admin API is unauthenticated; forward it to here");
|
|
646
|
+
}
|
|
647
|
+
if (url.pathname !== "/" || url.search !== "" || url.username !== "") {
|
|
648
|
+
throw refuse2(address, "no path, query or credentials — the API lives at the root");
|
|
649
|
+
}
|
|
650
|
+
const port = url.port === "" ? 80 : Number(url.port);
|
|
651
|
+
const host = hostHeader ?? `${url.hostname}:${String(port)}`;
|
|
652
|
+
return {
|
|
653
|
+
listener: {
|
|
654
|
+
hostHeader: host,
|
|
655
|
+
kind: "tcp",
|
|
656
|
+
port: Number(host.slice(host.lastIndexOf(":") + 1))
|
|
657
|
+
},
|
|
658
|
+
target: { host: url.hostname.replace(/^\[(.*)\]$/, "$1"), kind: "tcp", port }
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
var code = (cause) => typeof cause === "object" && cause !== null && ("code" in cause) ? String(cause.code) : undefined;
|
|
662
|
+
var neverConnected = (cause) => code(cause) === "ECONNREFUSED" || code(cause) === "ENOENT";
|
|
663
|
+
var exchange = (target, method, path, headers, body, timeoutMs) => new Promise((resolve, reject) => {
|
|
664
|
+
const bytes = body === undefined ? undefined : Buffer.from(body, "utf8");
|
|
665
|
+
const request = httpRequest({
|
|
666
|
+
...target.kind === "tcp" ? { host: target.host, port: target.port } : { socketPath: target.socketPath },
|
|
667
|
+
headers: bytes === undefined ? headers : { ...headers, "Content-Length": bytes.length },
|
|
668
|
+
method,
|
|
669
|
+
path,
|
|
670
|
+
timeout: timeoutMs
|
|
671
|
+
}, (response) => {
|
|
672
|
+
const chunks = [];
|
|
673
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
674
|
+
response.on("error", reject);
|
|
675
|
+
response.on("end", () => resolve({
|
|
676
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
677
|
+
status: response.statusCode ?? 0
|
|
678
|
+
}));
|
|
679
|
+
});
|
|
680
|
+
request.on("timeout", () => request.destroy(new Error(`no answer in ${String(timeoutMs)} ms`)));
|
|
681
|
+
request.on("error", reject);
|
|
682
|
+
request.end(bytes);
|
|
683
|
+
});
|
|
684
|
+
var localCaddyAdmin = (options = {}) => {
|
|
685
|
+
const address = options.address ?? DEFAULT_ADMIN_ADDRESS;
|
|
686
|
+
const { listener, target } = parseAdminAddress(address, options.hostHeader);
|
|
687
|
+
const timeoutMs = options.timeoutMs ?? 60000;
|
|
688
|
+
const retries = options.retries ?? 2;
|
|
689
|
+
const delay = options.retryDelayMs ?? 500;
|
|
690
|
+
const fixed = listener.kind === "tcp" ? { Host: listener.hostHeader, Origin: `http://${listener.hostHeader}` } : { Host: "127.0.0.1" };
|
|
691
|
+
return {
|
|
692
|
+
endpoint: address,
|
|
693
|
+
listener,
|
|
694
|
+
request: async ({ body, headers, method, path }) => {
|
|
695
|
+
for (let attempt = 0;; attempt += 1) {
|
|
696
|
+
try {
|
|
697
|
+
return await exchange(target, method, path, { ...headers, ...fixed }, body, timeoutMs);
|
|
698
|
+
} catch (cause) {
|
|
699
|
+
if (!neverConnected(cause) || attempt >= retries) {
|
|
700
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
701
|
+
const message = `Caddy admin ${method} ${path} at ${address}: ${reason}`;
|
|
702
|
+
throw neverConnected(cause) ? new CaddyUnreachableError(message) : new Error(message);
|
|
703
|
+
}
|
|
704
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
};
|
|
710
|
+
// src/caddy/providers.ts
|
|
711
|
+
import * as Layer2 from "effect/Layer";
|
|
712
|
+
var caddyProviders = (admin = localCaddyAdmin()) => CaddyConfigProvider().pipe(Layer2.provide(caddyAdminLayer(admin)));
|
|
713
|
+
// src/caddy/with-file.ts
|
|
714
|
+
import * as Effect4 from "effect/Effect";
|
|
715
|
+
import * as RemovalPolicy from "alchemy/RemovalPolicy";
|
|
716
|
+
|
|
717
|
+
// src/launchd/host-file.ts
|
|
718
|
+
import { Resource as Resource2 } from "alchemy";
|
|
719
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
720
|
+
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
721
|
+
import * as Provider2 from "alchemy/Provider";
|
|
722
|
+
import * as Effect3 from "effect/Effect";
|
|
723
|
+
|
|
724
|
+
// src/launchd/host-file-form.ts
|
|
725
|
+
var DEFAULT_MODE = 420;
|
|
726
|
+
var identityProblems = (name, value) => {
|
|
727
|
+
if (value === undefined)
|
|
728
|
+
return [];
|
|
729
|
+
if (typeof value === "number") {
|
|
730
|
+
return Number.isSafeInteger(value) && value >= 0 ? [] : [`${name} must be a non-negative integer id`];
|
|
731
|
+
}
|
|
732
|
+
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`];
|
|
733
|
+
};
|
|
734
|
+
var fileProblems = (props) => {
|
|
735
|
+
const found = [];
|
|
736
|
+
const { path } = props;
|
|
737
|
+
if (!path.startsWith("/"))
|
|
738
|
+
found.push("path must be absolute");
|
|
739
|
+
if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
|
|
740
|
+
found.push('path must be normalised (no trailing slash, no empty, "." or ".." segments)');
|
|
741
|
+
}
|
|
742
|
+
if (path.includes("\x00"))
|
|
743
|
+
found.push("path contains NUL");
|
|
744
|
+
const mode = props.mode ?? DEFAULT_MODE;
|
|
745
|
+
if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
|
|
746
|
+
found.push("mode must be 0–0o7777");
|
|
747
|
+
found.push(...identityProblems("owner", props.owner));
|
|
748
|
+
found.push(...identityProblems("group", props.group));
|
|
749
|
+
found.push(...fileSecretProblems(props.content));
|
|
750
|
+
return found;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/launchd/runner.ts
|
|
754
|
+
import * as Context2 from "effect/Context";
|
|
755
|
+
import * as Layer3 from "effect/Layer";
|
|
756
|
+
|
|
757
|
+
class HostRunnerService extends Context2.Service()("homeflare/launchd/HostRunner") {
|
|
758
|
+
}
|
|
759
|
+
var hostRunnerLayer = (runner) => Layer3.succeed(HostRunnerService, runner);
|
|
760
|
+
var canActAsRoot = (runner) => runner.privileged || runner.effectiveUid() === 0;
|
|
761
|
+
|
|
762
|
+
// src/launchd/host-file-lifecycle.ts
|
|
763
|
+
var refuse3 = (path, message) => new Error(`Host.File ${path}: ${message}`);
|
|
764
|
+
var numericId = (value) => typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : undefined;
|
|
765
|
+
var desiredFile = async (runner, props) => {
|
|
766
|
+
const found = fileProblems(props);
|
|
767
|
+
if (found.length > 0)
|
|
768
|
+
throw refuse3(props.path, found.join("; "));
|
|
769
|
+
const bytes = new TextEncoder().encode(props.content);
|
|
770
|
+
let uid;
|
|
771
|
+
let gid;
|
|
772
|
+
if (props.owner !== undefined) {
|
|
773
|
+
uid = numericId(props.owner) ?? (await runner.lookupUser(String(props.owner)))?.uid;
|
|
774
|
+
if (uid === undefined)
|
|
775
|
+
throw refuse3(props.path, `no user ${String(props.owner)} on this host`);
|
|
776
|
+
}
|
|
777
|
+
if (props.group !== undefined) {
|
|
778
|
+
gid = numericId(props.group) ?? await runner.lookupGroup(String(props.group));
|
|
779
|
+
if (gid === undefined)
|
|
780
|
+
throw refuse3(props.path, `no group ${String(props.group)} on this host`);
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
bytes,
|
|
784
|
+
mode: props.mode ?? DEFAULT_MODE,
|
|
785
|
+
sha256: sha256Hex(bytes),
|
|
786
|
+
...uid === undefined ? {} : { uid },
|
|
787
|
+
...gid === undefined ? {} : { gid }
|
|
788
|
+
};
|
|
789
|
+
};
|
|
790
|
+
var readFileAttributes = async (runner, path) => {
|
|
791
|
+
const stat = await runner.stat(path);
|
|
792
|
+
if (stat === undefined)
|
|
793
|
+
return;
|
|
794
|
+
const bytes = stat.kind === "file" ? await runner.readFile(path) : undefined;
|
|
795
|
+
return {
|
|
796
|
+
gid: stat.gid,
|
|
797
|
+
mode: stat.mode,
|
|
798
|
+
path,
|
|
799
|
+
sha256: bytes === undefined ? "" : sha256Hex(bytes),
|
|
800
|
+
size: stat.size,
|
|
801
|
+
uid: stat.uid
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
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);
|
|
805
|
+
var diffFile = async (runner, news, output) => {
|
|
806
|
+
if (news.path !== output.path)
|
|
807
|
+
return { action: "replace" };
|
|
808
|
+
const want = await desiredFile(runner, news);
|
|
809
|
+
if (want.sha256 !== output.sha256)
|
|
810
|
+
return { action: "update" };
|
|
811
|
+
const live = await readFileAttributes(runner, news.path);
|
|
812
|
+
return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
|
|
813
|
+
};
|
|
814
|
+
var reconcileFile = async (runner, props, output) => {
|
|
815
|
+
const want = await desiredFile(runner, props);
|
|
816
|
+
if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
|
|
817
|
+
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 (privileged: true). This provider never calls sudo.");
|
|
818
|
+
}
|
|
819
|
+
const stat = await runner.stat(props.path);
|
|
820
|
+
if (stat !== undefined && stat.kind !== "file") {
|
|
821
|
+
throw refuse3(props.path, `is a ${stat.kind}; remove it from whatever owns it before declaring it here`);
|
|
822
|
+
}
|
|
823
|
+
const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
|
|
824
|
+
const moved = output !== undefined && output.path !== props.path;
|
|
825
|
+
const prior = moved ? undefined : output;
|
|
826
|
+
if (prior === undefined && before !== undefined && !matches(before, want)) {
|
|
827
|
+
throw refuse3(props.path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
|
|
828
|
+
}
|
|
829
|
+
if (before === undefined || !matches(before, want)) {
|
|
830
|
+
await runner.writeFileAtomic(props.path, want.bytes, {
|
|
831
|
+
mode: want.mode,
|
|
832
|
+
...want.uid === undefined ? {} : { uid: want.uid },
|
|
833
|
+
...want.gid === undefined ? {} : { gid: want.gid }
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
const after = await readFileAttributes(runner, props.path);
|
|
837
|
+
if (after === undefined || !matches(after, want)) {
|
|
838
|
+
if (before === undefined)
|
|
839
|
+
await runner.removeFile(props.path).catch(() => {
|
|
840
|
+
return;
|
|
841
|
+
});
|
|
842
|
+
throw refuse3(props.path, "the write returned but the file on disk does not match the declaration");
|
|
843
|
+
}
|
|
844
|
+
if (moved)
|
|
845
|
+
await deleteFile(runner, output);
|
|
846
|
+
return after;
|
|
847
|
+
};
|
|
848
|
+
var deleteFile = async (runner, output) => {
|
|
849
|
+
const stat = await runner.stat(output.path);
|
|
850
|
+
if (stat === undefined)
|
|
851
|
+
return;
|
|
852
|
+
if (stat.kind !== "file")
|
|
853
|
+
throw refuse3(output.path, `is now a ${stat.kind}; not removing it`);
|
|
854
|
+
await runner.removeFile(output.path);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// src/launchd/host-file.ts
|
|
858
|
+
var HostFile = Resource2("Host.File");
|
|
859
|
+
var HostFileProvider = () => Provider2.effect(HostFile, Effect3.gen(function* () {
|
|
860
|
+
const runner = yield* HostRunnerService;
|
|
861
|
+
return HostFile.Provider.of({
|
|
862
|
+
list: () => Effect3.succeed([]),
|
|
863
|
+
read: ({ olds, output }) => lift(async () => {
|
|
864
|
+
const found = await readFileAttributes(runner, olds.path);
|
|
865
|
+
if (found === undefined)
|
|
866
|
+
return;
|
|
867
|
+
return output === undefined ? Unowned(found) : found;
|
|
868
|
+
}),
|
|
869
|
+
diff: ({ news, output }) => {
|
|
870
|
+
if (output === undefined)
|
|
871
|
+
return Effect3.succeed(undefined);
|
|
872
|
+
if (isResolved2(news))
|
|
873
|
+
return lift(() => diffFile(runner, news, output));
|
|
874
|
+
const path = resolvedString(news, "path");
|
|
875
|
+
return Effect3.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
|
|
876
|
+
},
|
|
877
|
+
reconcile: ({ news, output }) => lift(() => reconcileFile(runner, news, output)),
|
|
878
|
+
delete: ({ output }) => lift(() => deleteFile(runner, output))
|
|
879
|
+
});
|
|
880
|
+
}));
|
|
881
|
+
|
|
882
|
+
// src/caddy/with-file.ts
|
|
883
|
+
var caddyWithFile = (id, props) => Effect4.gen(function* () {
|
|
884
|
+
const found = configProblems({ caddyfile: props.caddyfile, sourceFile: props.path });
|
|
885
|
+
if (found.length > 0) {
|
|
886
|
+
return yield* Effect4.fail(new Error(`caddyWithFile ${id}: ${found.join("; ")}`));
|
|
887
|
+
}
|
|
888
|
+
const file = yield* HostFile(`${id}-file`, {
|
|
889
|
+
content: props.caddyfile,
|
|
890
|
+
mode: props.mode ?? 420,
|
|
891
|
+
path: props.path,
|
|
892
|
+
...props.owner === undefined ? {} : { owner: props.owner },
|
|
893
|
+
...props.group === undefined ? {} : { group: props.group }
|
|
894
|
+
}).pipe(RemovalPolicy.retain());
|
|
895
|
+
const config = yield* CaddyConfig(id, { caddyfile: props.caddyfile, sourceFile: file.path });
|
|
896
|
+
return { config, file };
|
|
897
|
+
});
|
|
898
|
+
export {
|
|
899
|
+
CaddyAdminError,
|
|
900
|
+
CaddyAdminService,
|
|
901
|
+
CaddyConfig,
|
|
902
|
+
CaddyConfigProvider,
|
|
903
|
+
CaddyUnreachableError,
|
|
904
|
+
DEFAULT_ADMIN_ADDRESS,
|
|
905
|
+
caddyAdminLayer,
|
|
906
|
+
caddyProviders,
|
|
907
|
+
caddyWithFile,
|
|
908
|
+
localCaddyAdmin
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
//# debugId=205F5FF536DDDFA164756E2164756E21
|
|
912
|
+
//# sourceMappingURL=index.js.map
|