@studiometa/trafic-agent 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +129 -20
  2. package/dist/cli.js +49 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/{server-BAjHxgqh.js → server-EPU5dtPx.js} +355 -21
  6. package/dist/server-EPU5dtPx.js.map +1 -0
  7. package/dist/server.d.ts.map +1 -1
  8. package/dist/server.js +1 -1
  9. package/dist/setup/agent.d.ts +2 -0
  10. package/dist/setup/agent.d.ts.map +1 -1
  11. package/dist/setup/audit.d.ts +39 -1
  12. package/dist/setup/audit.d.ts.map +1 -1
  13. package/dist/setup/ddev.d.ts +102 -3
  14. package/dist/setup/ddev.d.ts.map +1 -1
  15. package/dist/setup/docker.d.ts +23 -0
  16. package/dist/setup/docker.d.ts.map +1 -1
  17. package/dist/setup/index.d.ts.map +1 -1
  18. package/dist/setup/migrations/0015__catchall_priority.d.ts +40 -0
  19. package/dist/setup/migrations/0015__catchall_priority.d.ts.map +1 -0
  20. package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts +63 -0
  21. package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts.map +1 -0
  22. package/dist/setup/migrations/0018__docker_address_pools.d.ts +29 -0
  23. package/dist/setup/migrations/0018__docker_address_pools.d.ts.map +1 -0
  24. package/dist/setup/migrations/index.d.ts.map +1 -1
  25. package/dist/setup/types.d.ts +11 -0
  26. package/dist/setup/types.d.ts.map +1 -1
  27. package/dist/{setup-BrMbM8w4.js → setup-4zlNJqDe.js} +373 -173
  28. package/dist/setup-4zlNJqDe.js.map +1 -0
  29. package/dist/types.d.ts +21 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/utils/config.d.ts.map +1 -1
  32. package/dist/utils/tls.d.ts +10 -0
  33. package/dist/utils/tls.d.ts.map +1 -0
  34. package/package.json +1 -1
  35. package/templates/wait.html +1 -0
  36. package/dist/server-BAjHxgqh.js.map +0 -1
  37. package/dist/setup-BrMbM8w4.js.map +0 -1
@@ -1,9 +1,147 @@
1
1
  import { existsSync, readFileSync, watch, writeFileSync } from "node:fs";
2
+ import { parse } from "smol-toml";
2
3
  import { execFile, execSync } from "node:child_process";
3
4
  import { DatabaseSync } from "node:sqlite";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import { createServer } from "node:http";
6
7
  import { fileURLToPath } from "node:url";
8
+ //#region src/utils/config.ts
9
+ var DEFAULT_CONFIG_PATHS = [
10
+ "/etc/trafic/config.toml",
11
+ "./config.toml",
12
+ "./trafic.toml"
13
+ ];
14
+ var DEFAULT_CONFIG = {
15
+ tld: "",
16
+ port: 9876,
17
+ dbPath: "/var/lib/trafic/db.sqlite",
18
+ projectListPath: "/home/ddev/.ddev/project_list.yaml",
19
+ projectsDir: "/home/ddev/www",
20
+ idleTimeout: "30m",
21
+ idleCheckInterval: "5m",
22
+ auth: {
23
+ defaultPolicy: "basic",
24
+ allowedIps: [],
25
+ tokens: [],
26
+ basicAuth: [],
27
+ rules: [],
28
+ trustedProxyHops: 1
29
+ },
30
+ tls: { dnsEnv: {} }
31
+ };
32
+ /**
33
+ * Parse a duration string (e.g., "30m", "1h", "2h30m") to milliseconds
34
+ */
35
+ function parseDuration(duration) {
36
+ const match = duration.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
37
+ if (!match) return 0;
38
+ const hours = parseInt(match[1] ?? "0", 10);
39
+ const minutes = parseInt(match[2] ?? "0", 10);
40
+ const seconds = parseInt(match[3] ?? "0", 10);
41
+ return (hours * 3600 + minutes * 60 + seconds) * 1e3;
42
+ }
43
+ /**
44
+ * Load configuration from TOML file
45
+ */
46
+ function loadConfig(configPath) {
47
+ let path = configPath;
48
+ if (!path) path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));
49
+ if (!path || !existsSync(path)) {
50
+ console.warn("No config file found, using defaults");
51
+ return DEFAULT_CONFIG;
52
+ }
53
+ const content = readFileSync(path, "utf-8");
54
+ const parsed = parse(content);
55
+ const auth = mergeAuthConfig(parsed.auth);
56
+ const tls = mergeTlsConfig(parsed.tls);
57
+ return {
58
+ tld: parsed.tld ?? DEFAULT_CONFIG.tld,
59
+ port: parsed.port ?? DEFAULT_CONFIG.port,
60
+ dbPath: parsed.db_path ?? DEFAULT_CONFIG.dbPath,
61
+ projectListPath: parsed.project_list_path ?? DEFAULT_CONFIG.projectListPath,
62
+ projectsDir: parsed.projects_dir ?? DEFAULT_CONFIG.projectsDir,
63
+ idleTimeout: parsed.idle_timeout ?? DEFAULT_CONFIG.idleTimeout,
64
+ idleCheckInterval: parsed.idle_check_interval ?? DEFAULT_CONFIG.idleCheckInterval,
65
+ auth,
66
+ tls
67
+ };
68
+ }
69
+ /**
70
+ * Read the [tls] section.
71
+ *
72
+ * `dns_env` holds provider credentials, so its values are taken as-is and
73
+ * only checked for being strings — validateConfig reports the rest.
74
+ */
75
+ function mergeTlsConfig(raw) {
76
+ if (!raw) return DEFAULT_CONFIG.tls;
77
+ const env = raw.dns_env ?? {};
78
+ const dnsEnv = {};
79
+ for (const [key, value] of Object.entries(env)) dnsEnv[key] = value;
80
+ return {
81
+ dnsProvider: raw.dns_provider || void 0,
82
+ caServer: raw.ca_server || void 0,
83
+ dnsEnv
84
+ };
85
+ }
86
+ /**
87
+ * Parse basic_auth entries from TOML.
88
+ * Accepts either plain strings ("user:pass") or objects ({username, password}).
89
+ */
90
+ function parseBasicAuthEntries(raw) {
91
+ if (!Array.isArray(raw)) return null;
92
+ return raw.map((entry) => {
93
+ if (typeof entry === "string") return entry;
94
+ if (typeof entry === "object" && entry !== null) {
95
+ const { username, password } = entry;
96
+ return `${username}:${password}`;
97
+ }
98
+ return String(entry);
99
+ });
100
+ }
101
+ function mergeAuthConfig(raw) {
102
+ if (!raw) return DEFAULT_CONFIG.auth;
103
+ const rules = raw.rules ?? [];
104
+ return {
105
+ defaultPolicy: raw.default_policy ?? DEFAULT_CONFIG.auth.defaultPolicy,
106
+ allowedIps: raw.allowed_ips ?? DEFAULT_CONFIG.auth.allowedIps,
107
+ tokens: raw.tokens ?? DEFAULT_CONFIG.auth.tokens,
108
+ basicAuth: parseBasicAuthEntries(raw.basic_auth) ?? DEFAULT_CONFIG.auth.basicAuth,
109
+ rules: rules.map((r) => ({
110
+ match: r.match,
111
+ policy: r.policy,
112
+ tokens: r.tokens,
113
+ allowedIps: r.allowed_ips
114
+ })),
115
+ trustedProxyHops: parseTrustedProxyHops(raw.trusted_proxy_hops)
116
+ };
117
+ }
118
+ /**
119
+ * Parse trusted_proxy_hops, ignoring values that cannot be a hop count.
120
+ *
121
+ * A bad value falls back to the default rather than 0, because 0 would read
122
+ * the socket peer — always the proxy — and quietly stop the IP allowlist and
123
+ * per-hostname IP rules from ever matching.
124
+ */
125
+ function parseTrustedProxyHops(raw) {
126
+ const hops = typeof raw === "number" ? raw : Number(raw);
127
+ if (!Number.isInteger(hops) || hops < 1) return DEFAULT_CONFIG.auth.trustedProxyHops;
128
+ return hops;
129
+ }
130
+ /**
131
+ * Validate configuration
132
+ */
133
+ function validateConfig(config) {
134
+ const errors = [];
135
+ if (!config.tld) errors.push("tld is required");
136
+ if (config.port < 1 || config.port > 65535) errors.push("port must be between 1 and 65535");
137
+ if (config.auth.defaultPolicy !== "allow" && config.auth.defaultPolicy !== "deny" && config.auth.defaultPolicy !== "basic" && config.auth.defaultPolicy !== "token") errors.push("auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"");
138
+ for (const [key, value] of Object.entries(config.tls.dnsEnv)) {
139
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) errors.push(`tls.dns_env key "${key}" is not a valid environment variable name`);
140
+ if (typeof value !== "string") errors.push(`tls.dns_env.${key} must be a string`);
141
+ }
142
+ return errors;
143
+ }
144
+ //#endregion
7
145
  //#region src/utils/auth.ts
8
146
  /**
9
147
  * Check if an IP matches a pattern (supports CIDR notation)
@@ -832,14 +970,42 @@ function readRouterPorts(io = nodeIo) {
832
970
  https: read("router-https-port", DEFAULT_ROUTER_PORTS.https)
833
971
  };
834
972
  }
973
+ /** Where the agent writes the Traefik files it owns. */
974
+ var TRAEFIK_DIR = "/home/ddev/.ddev/traefik";
975
+ /** Static config the agent owns, merged by DDEV into .static_config.yaml. */
976
+ var STATIC_CONFIG = `${TRAEFIK_DIR}/static_config.trafic.yaml`;
977
+ /** Wildcard TLS store, read before DDEV's own default_config.yaml. */
978
+ var TLS_STORE_CONFIG = `${TRAEFIK_DIR}/custom-global-config/0-trafic-tls.yaml`;
979
+ /** Router compose override carrying the DNS provider credentials. */
980
+ var ROUTER_COMPOSE_OVERRIDE = "/home/ddev/.ddev/router-compose.trafic.yaml";
981
+ /** Name of the DNS-01 resolver the agent adds to Traefik. */
982
+ var DNS_RESOLVER = "acme-dns";
835
983
  /**
836
984
  * Build the static config attaching forward auth to every entry point.
837
985
  *
838
986
  * The error page middleware stays on the web entry points only: a 502 from a
839
987
  * stopped project should show the waiting page, while a tool port has nothing
840
988
  * to wait for.
841
- */
842
- function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS) {
989
+ *
990
+ * With `tls`, a DNS-01 certificate resolver is added. It is only ever used by
991
+ * the default TLS store (see buildTlsStoreConfig): DDEV keeps writing
992
+ * `certResolver: acme-tlsChallenge` on every project router, and Traefik
993
+ * skips those requests once the default store holds a certificate matching
994
+ * the host with wildcard semantics.
995
+ *
996
+ * The propagation check is disabled on purpose. lego sets two TXT values at
997
+ * the same name, one for `<tld>` and one for `*.<tld>`, then asks public
998
+ * resolvers for each. A resolver that answered between the two writes caches
999
+ * the partial answer for the record TTL — 120s on Cloudflare, longer than
1000
+ * lego's check window — and the order fails with "NS 9.9.9.9:53 did not
1001
+ * return the expected TXT record" although both records were correct. That
1002
+ * was seen on a live server; the staging run had passed by timing luck.
1003
+ * Let's Encrypt resolves authoritatively itself, so the check adds a failure
1004
+ * mode without adding safety. `PropagationWait(delay, skipCheck)` sleeps the
1005
+ * delay and then skips the check, so the 30s covers propagation to the
1006
+ * provider's edges before the CA looks.
1007
+ */
1008
+ function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS, tls) {
843
1009
  const webEntryPoint = (name) => ` ${name}:
844
1010
  http:
845
1011
  middlewares:
@@ -853,8 +1019,66 @@ function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS) {
853
1019
  `;
854
1020
  const web = [routerPorts.http, routerPorts.https];
855
1021
  const tools = toolPorts.filter((port) => !web.includes(port));
856
- return `entryPoints:
1022
+ const entryPoints = `entryPoints:
857
1023
  ${web.map((port) => webEntryPoint(`http-${port}`)).join("")}${tools.map(toolEntryPoint).join("")}`;
1024
+ if (!tls) return entryPoints;
1025
+ return `${entryPoints}
1026
+ certificatesResolvers:
1027
+ ${DNS_RESOLVER}:
1028
+ acme:
1029
+ email: "${escapeYamlDouble(tls.email)}"
1030
+ storage: /mnt/ddev-global-cache/traefik/${DNS_RESOLVER}.json
1031
+ ${tls.caServer ? ` caServer: "${escapeYamlDouble(tls.caServer)}"\n` : ""} dnsChallenge:
1032
+ provider: "${escapeYamlDouble(tls.provider)}"
1033
+ propagation:
1034
+ delayBeforeChecks: 30s
1035
+ disableChecks: true
1036
+ `;
1037
+ }
1038
+ /**
1039
+ * Build the default TLS store holding the wildcard certificate.
1040
+ *
1041
+ * Traefik's file provider keeps the first `tls.stores.default` it reads in
1042
+ * directory order and logs "TLS store default already configured, skipping"
1043
+ * for every later file. DDEV writes an empty one in `default_config.yaml`,
1044
+ * so this has to be read first — hence the `0-` prefix on the file name.
1045
+ */
1046
+ function buildTlsStoreConfig(tld) {
1047
+ return `# Trafic: wildcard certificate served to every host without its own
1048
+ #
1049
+ # Read before DDEV's default_config.yaml — Traefik keeps the first default
1050
+ # store it sees, so the file name has to sort first.
1051
+ tls:
1052
+ stores:
1053
+ default:
1054
+ defaultGeneratedCert:
1055
+ resolver: ${DNS_RESOLVER}
1056
+ domain:
1057
+ main: "${escapeYamlDouble(tld)}"
1058
+ sans:
1059
+ - "*.${escapeYamlDouble(tld)}"
1060
+ `;
1061
+ }
1062
+ /**
1063
+ * Build the router compose override carrying the provider credentials.
1064
+ *
1065
+ * lego reads them from the router container's environment, so they cannot be
1066
+ * passed through a config file. DDEV merges `~/.ddev/router-compose.*.yaml`
1067
+ * into the router compose and recreates the container when it changes, on the
1068
+ * next `ddev start`.
1069
+ */
1070
+ function buildRouterComposeOverride(env) {
1071
+ return `# Trafic: DNS-01 provider credentials for the Traefik router
1072
+ # Contains secrets — this file is mode 600.
1073
+ services:
1074
+ ddev-router:
1075
+ environment:
1076
+ ${Object.entries(env).map(([key, value]) => ` - "${key}=${escapeYamlDouble(value)}"`).join("\n")}
1077
+ `;
1078
+ }
1079
+ /** Escape a value for a double-quoted YAML scalar. */
1080
+ function escapeYamlDouble(value) {
1081
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
858
1082
  }
859
1083
  /**
860
1084
  * Build the dynamic config: the middlewares, the agent service, and the
@@ -887,12 +1111,20 @@ http:
887
1111
  query: "/"
888
1112
 
889
1113
  routers:
890
- # Lowest priority, so a DDEV project router always wins. These only match
891
- # when none does — which is what a stopped project produces, because DDEV
892
- # removes that project's router and Traefik would otherwise answer 404.
893
- # Without them the waiting page never appears and scale-to-zero never
894
- # restarts anything. Auth is unaffected: it is attached at the entry
895
- # point, not here, so this cannot reintroduce the bypass that #27 fixed.
1114
+ # The lowest priority above DDEV's own fallback routers, which sit at
1115
+ # priority 1 since DDEV 1.25.4, and still far below any project router —
1116
+ # so a DDEV project router always wins. These only match when none does —
1117
+ # which is what a stopped project produces, because DDEV removes that
1118
+ # project's router and Traefik would otherwise answer 404. Without them
1119
+ # the waiting page never appears and scale-to-zero never restarts
1120
+ # anything. Auth is unaffected: it is attached at the entry point, not
1121
+ # here, so this cannot reintroduce the bypass that #27 fixed.
1122
+ #
1123
+ # Priority 1 is not enough any more: DDEV 1.25.4 ships
1124
+ # ddev-router-fallback-http and ddev-router-fallback-https with the same
1125
+ # PathPrefix(\`/\`) rule at priority 1, pointing at a 404 responder. With
1126
+ # equal priority and identical rules Traefik picked DDEV's, so a stopped
1127
+ # project answered DDEV's 404 page instead of the waiting page.
896
1128
  #
897
1129
  # Each names exactly one entry point. Leaving entryPoints out attaches a
898
1130
  # router to all of them, which breaks DDEV's router health check — it
@@ -901,7 +1133,7 @@ http:
901
1133
  rule: "PathPrefix(\`/\`)"
902
1134
  entryPoints:
903
1135
  - http-${routerPorts.http}
904
- priority: 1
1136
+ priority: 2
905
1137
  service: trafic-service
906
1138
 
907
1139
  # Carries tls, so it only matches the https entry point. The plain one
@@ -910,7 +1142,7 @@ http:
910
1142
  rule: "PathPrefix(\`/\`)"
911
1143
  entryPoints:
912
1144
  - http-${routerPorts.https}
913
- priority: 1
1145
+ priority: 2
914
1146
  service: trafic-service
915
1147
  tls: {}
916
1148
 
@@ -922,23 +1154,108 @@ http:
922
1154
  `;
923
1155
  }
924
1156
  /**
925
- * Configure Traefik for forward auth
1157
+ * Read DDEV's Let's Encrypt settings.
1158
+ *
1159
+ * DDEV owns them — `ddev config global --use-letsencrypt --letsencrypt-email`
1160
+ * — so they are the single source of truth for the ACME account, wildcard
1161
+ * mode included.
926
1162
  */
927
- function configureTraefik(io = nodeIo) {
1163
+ function readLetsEncryptSettings(io = nodeIo) {
1164
+ const output = io.execSilent("su - ddev -c 'ddev config global'");
1165
+ return {
1166
+ enabled: /^use-letsencrypt=true$/m.test(output),
1167
+ email: /^letsencrypt-email=(.+)$/m.exec(output)?.[1]?.trim() || void 0
1168
+ };
1169
+ }
1170
+ /** Read the project TLD from DDEV's global config. */
1171
+ function readProjectTld(io = nodeIo) {
1172
+ const output = io.execSilent("su - ddev -c 'ddev config global'");
1173
+ return /^project-tld=(.+)$/m.exec(output)?.[1]?.trim() ?? "";
1174
+ }
1175
+ /**
1176
+ * Configure Traefik for forward auth, and for the wildcard certificate when
1177
+ * a DNS provider is configured.
1178
+ *
1179
+ * Everything the agent owns in Traefik's configuration is written here, so
1180
+ * setup and the migrations share one code path and cannot drift apart.
1181
+ */
1182
+ function configureTraefik(options = {}, io = nodeIo) {
928
1183
  step("Configure Traefik for forward auth");
929
- io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
1184
+ io.exec(`mkdir -p ${TRAEFIK_DIR}/custom-global-config`, { silent: true });
930
1185
  io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
931
1186
  const gatewayIp = getDockerGatewayIp(io);
932
1187
  const routerPorts = readRouterPorts(io);
933
1188
  const dynamicConfig = buildDynamicConfig(gatewayIp, routerPorts);
934
- io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
935
- io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
936
- const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts);
937
- io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", staticConfig);
938
- io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
1189
+ io.writeFile(`${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, dynamicConfig);
1190
+ io.exec(`chown ddev:ddev ${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, { silent: true });
1191
+ const tls = options.tls ?? loadConfig().tls;
1192
+ const resolver = tls.dnsProvider ? buildTlsResolverOptions(tls, io) : void 0;
1193
+ const tld = resolver ? readWildcardTld(options.tld, io) : "";
1194
+ const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts, resolver);
1195
+ io.writeFile(STATIC_CONFIG, staticConfig);
1196
+ io.exec(`chown ddev:ddev ${STATIC_CONFIG}`, { silent: true });
939
1197
  info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);
1198
+ if (resolver) writeWildcardFiles(tls, tld, io);
1199
+ else removeWildcardFiles(io);
940
1200
  success("Traefik configured with Trafic middleware");
941
1201
  }
1202
+ /**
1203
+ * Name of one running DDEV project, or null when none runs.
1204
+ *
1205
+ * `ddev list -j` is the only source that knows: `project_list.yaml`, which the
1206
+ * agent watches for discovery, records every project DDEV has ever seen but
1207
+ * carries no live status — a project stopped an hour ago still sits there.
1208
+ * `ddev list -j` asks Docker, and wraps the entries in a `raw` array.
1209
+ *
1210
+ * Used to push a changed global Traefik config into the running router: DDEV
1211
+ * copies `custom-global-config/*.yaml` into it on any `ddev start`, so
1212
+ * starting a project that is already running is enough, and nothing that is
1213
+ * serving traffic has to be stopped.
1214
+ */
1215
+ function findRunningProject(io = nodeIo) {
1216
+ const output = io.execSilent("su - ddev -c 'ddev list -j'");
1217
+ if (!output) return null;
1218
+ try {
1219
+ return (JSON.parse(output).raw ?? []).find((project) => project.status === "running")?.name ?? null;
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Turn the agent's TLS config into what the static config needs.
1226
+ *
1227
+ * The ACME account comes from DDEV: a resolver without an email is refused by
1228
+ * Let's Encrypt, and `use_letsencrypt=false` means DDEV is also stripping the
1229
+ * certificate resolver from the project routers.
1230
+ */
1231
+ function buildTlsResolverOptions(tls, io) {
1232
+ const letsEncrypt = readLetsEncryptSettings(io);
1233
+ if (!letsEncrypt.enabled || !letsEncrypt.email) throw new Error("tls.dns_provider needs Let's Encrypt enabled in DDEV with an account email.\n Run setup again with --email=<address>, or unset tls.dns_provider.");
1234
+ return {
1235
+ provider: tls.dnsProvider,
1236
+ email: letsEncrypt.email,
1237
+ caServer: tls.caServer
1238
+ };
1239
+ }
1240
+ /** Read the TLD the wildcard certificate covers, or fail before any write. */
1241
+ function readWildcardTld(configured, io) {
1242
+ const tld = configured || readProjectTld(io);
1243
+ if (!tld) throw new Error("Cannot configure the wildcard certificate: no project TLD is set in DDEV");
1244
+ return tld;
1245
+ }
1246
+ /** Write the wildcard TLS store and the router credentials. */
1247
+ function writeWildcardFiles(tls, tld, io) {
1248
+ io.writeFile(TLS_STORE_CONFIG, buildTlsStoreConfig(tld));
1249
+ io.exec(`chown ddev:ddev ${TLS_STORE_CONFIG}`, { silent: true });
1250
+ io.exec(`install -m 600 -o ddev -g ddev /dev/null ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });
1251
+ io.writeFile(ROUTER_COMPOSE_OVERRIDE, buildRouterComposeOverride(tls.dnsEnv));
1252
+ success(`Wildcard certificate: *.${tld} via the ${tls.dnsProvider} DNS-01 provider`);
1253
+ }
1254
+ /** Remove the wildcard files where no DNS provider is configured. */
1255
+ function removeWildcardFiles(io) {
1256
+ if (io.fileExists(TLS_STORE_CONFIG)) io.exec(`rm -f ${TLS_STORE_CONFIG}`, { silent: true });
1257
+ if (io.fileExists("/home/ddev/.ddev/router-compose.trafic.yaml")) io.exec(`rm -f ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });
1258
+ }
942
1259
  //#endregion
943
1260
  //#region src/utils/traefik.ts
944
1261
  /**
@@ -999,6 +1316,22 @@ function syncForwardAuthAddress(projectListPath, io = nodeIo) {
999
1316
  }
1000
1317
  }
1001
1318
  //#endregion
1319
+ //#region src/utils/tls.ts
1320
+ /**
1321
+ * Warn when the config asks for a wildcard certificate that Traefik never got.
1322
+ *
1323
+ * The files are written by setup and by migration 0016, not by the running
1324
+ * agent: writing them needs root, and applying them needs a project start.
1325
+ * So this only reports, and says which command fixes it.
1326
+ */
1327
+ function warnIfWildcardNotApplied(config, staticConfigPath = STATIC_CONFIG) {
1328
+ if (!config.tls.dnsProvider) return false;
1329
+ if (existsSync(staticConfigPath) && readFileSync(staticConfigPath, "utf-8").includes(`acme-dns:`)) return false;
1330
+ console.warn(`tls.dns_provider is set to "${config.tls.dnsProvider}" but Traefik has no ${DNS_RESOLVER} resolver.`);
1331
+ console.warn(" The wildcard certificate is not being requested. Run `sudo trafic-agent upgrade`, or re-run setup.");
1332
+ return true;
1333
+ }
1334
+ //#endregion
1002
1335
  //#region src/server.ts
1003
1336
  var __dirname = dirname(fileURLToPath(import.meta.url));
1004
1337
  var config;
@@ -1180,6 +1513,7 @@ function startServer(agentConfig) {
1180
1513
  config = agentConfig;
1181
1514
  initDb(config.dbPath);
1182
1515
  syncForwardAuthAddress(config.projectListPath);
1516
+ warnIfWildcardNotApplied(config);
1183
1517
  reloadProjects();
1184
1518
  watchProjectList(config.projectListPath, () => {
1185
1519
  console.log("Project list changed, reloading...");
@@ -1212,6 +1546,6 @@ function startServer(agentConfig) {
1212
1546
  });
1213
1547
  }
1214
1548
  //#endregion
1215
- export { error as A, buildHostnameIndex as B, getIdleProjects as C, setProjectStatus as D, logAccess as E, resetSteps as F, startProject as G, getProjectInfo as H, setDryRun as I, checkAuth as J, stopProject as K, step as L, execSilent as M, info as N, updateProjectAccess as O, isRoot as P, parseBearerToken as Q, success as R, getDb as S, initDb as T, listProjects as U, getProjectFromHostname as V, loadProjectList as W, matchIp as X, matchHostname as Y, parseBasicAuth as Z, loadProjectConfig as _, handleStatus as a, closeDb as b, addDdevAptRepo as c, getDockerGatewayIp as d, installDdev as f, getIdleTimeoutMs as g, readToolPorts as h, handleRequest as i, exec as j, nodeIo as k, configureDdev as l, purgeTraefikAcmeStorage as m, handleAuth as n, routePath as o, installSystemDeps as p, watchProjectList as q, handleErrors as r, startServer as s, getEffectiveAuthConfig as t, configureTraefik as u, shouldNeverStop as v, getProject as w, getAccessLogs as x, cleanOldLogs as y, warn as z };
1549
+ export { checkAuth as $, initDb as A, resetSteps as B, shouldNeverStop as C, getDb as D, getAccessLogs as E, error as F, buildHostnameIndex as G, step as H, exec as I, listProjects as J, getProjectFromHostname as K, execSilent as L, setProjectStatus as M, updateProjectAccess as N, getIdleProjects as O, nodeIo as P, watchProjectList as Q, info as R, loadProjectConfig as S, closeDb as T, success as U, setDryRun as V, warn as W, startProject as X, loadProjectList as Y, stopProject as Z, installDdev as _, handleStatus as a, parseDuration as at, readToolPorts as b, DNS_RESOLVER as c, TLS_STORE_CONFIG as d, matchHostname as et, addDdevAptRepo as f, getDockerGatewayIp as g, findRunningProject as h, handleRequest as i, loadConfig as it, logAccess as j, getProject as k, ROUTER_COMPOSE_OVERRIDE as l, configureTraefik as m, handleAuth as n, parseBasicAuth as nt, routePath as o, validateConfig as ot, configureDdev as p, getProjectInfo as q, handleErrors as r, parseBearerToken as rt, startServer as s, getEffectiveAuthConfig as t, matchIp as tt, STATIC_CONFIG as u, installSystemDeps as v, cleanOldLogs as w, getIdleTimeoutMs as x, purgeTraefikAcmeStorage as y, isRoot as z };
1216
1550
 
1217
- //# sourceMappingURL=server-BAjHxgqh.js.map
1551
+ //# sourceMappingURL=server-EPU5dtPx.js.map