vite-plugin-caddy-multiple-tls 1.5.1 → 1.6.1

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 CHANGED
@@ -92,6 +92,26 @@ export default config;
92
92
 
93
93
  You can override auto-detection with `repo` or `branch` if needed.
94
94
 
95
+ If you run different projects that derive the same `<repo>.<branch>` host, add `instanceLabel` to keep domains unique:
96
+
97
+ ```js
98
+ // vite.config.js
99
+ import { defineConfig } from 'vite';
100
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
101
+
102
+ const config = defineConfig({
103
+ plugins: [
104
+ caddyTls({
105
+ instanceLabel: 'web-1',
106
+ })
107
+ ]
108
+ });
109
+
110
+ export default config;
111
+ ```
112
+
113
+ This derives a host like `<repo>.<branch>.web-1.localhost`.
114
+
95
115
  For a zero-config experience, use `baseDomain: 'localhost'` (the default) so the derived domain works without editing `/etc/hosts`.
96
116
 
97
117
  `internalTls` defaults to `true` when you pass `baseDomain` or `domain`. You can override it if needed.
@@ -116,6 +136,40 @@ const config = defineConfig({
116
136
  export default config;
117
137
  ```
118
138
 
139
+ If your Caddy Admin API enforces a specific allowed origin that differs from `caddyApiUrl`, set `caddyAdminOrigin`.
140
+
141
+ ```js
142
+ // vite.config.js
143
+ import { defineConfig } from 'vite';
144
+ import caddyTls from 'vite-plugin-caddy-multiple-tls';
145
+
146
+ const config = defineConfig({
147
+ plugins: [
148
+ caddyTls({
149
+ caddyApiUrl: 'http://127.0.0.1:2019',
150
+ caddyAdminOrigin: 'http://localhost:2019',
151
+ })
152
+ ]
153
+ });
154
+
155
+ export default config;
156
+ ```
157
+
158
+ ## Troubleshooting
159
+
160
+ ### `client is not allowed to access from origin ''`
161
+
162
+ This error comes from Caddy Admin API origin enforcement, not from Caddy being down.
163
+
164
+ - Check `caddyApiUrl` points to the correct Admin API endpoint.
165
+ - If Admin API expects a different origin than the API URL host, set `caddyAdminOrigin`.
166
+ - Verify behavior directly:
167
+
168
+ ```bash
169
+ curl -i http://127.0.0.1:2019/config/
170
+ curl -i -H 'Origin: http://127.0.0.1:2019' http://127.0.0.1:2019/config/
171
+ ```
172
+
119
173
  > [!IMPORTANT]
120
174
  > **Hosts file limitation:** If you use a custom domain, you must **manually** add each generated subdomain to your `/etc/hosts` file (e.g., `127.0.0.1 repo.branch.local.example.test`). System hosts files **do not support wildcards** (e.g., `*.local.example.test`), so you lose the benefit of automatic domain resolution that `localhost` provides.
121
175
 
package/dist/index.d.ts CHANGED
@@ -11,11 +11,15 @@ interface ViteCaddyTlsPluginOptions {
11
11
  repo?: string;
12
12
  /** Override branch name used in derived domains */
13
13
  branch?: string;
14
+ /** Extra unique label appended after branch in derived domains */
15
+ instanceLabel?: string;
14
16
  cors?: string;
15
17
  /** Override the default Caddy server name (srv0) */
16
18
  serverName?: string;
17
19
  /** Override the Caddy Admin API base URL (default: http://localhost:2019) */
18
20
  caddyApiUrl?: string;
21
+ /** Override the Origin header used for Caddy Admin API requests (defaults to caddyApiUrl origin) */
22
+ caddyAdminOrigin?: string;
19
23
  /** Use Caddy's internal CA for the provided domains (defaults to true when baseDomain or domain is set) */
20
24
  internalTls?: boolean;
21
25
  /**
@@ -37,6 +41,6 @@ type LoopbackDomain = 'localtest.me' | 'lvh.me' | 'nip.io';
37
41
  * ```
38
42
  * @returns {Plugin} - a Vite plugin
39
43
  */
40
- declare function viteCaddyTlsPlugin({ domain, baseDomain, loopbackDomain, repo, branch, cors, serverName, caddyApiUrl, internalTls, upstreamHostHeader, }?: ViteCaddyTlsPluginOptions): PluginOption;
44
+ declare function viteCaddyTlsPlugin({ domain, baseDomain, loopbackDomain, repo, branch, instanceLabel, cors, serverName, caddyApiUrl, caddyAdminOrigin, internalTls, upstreamHostHeader, }?: ViteCaddyTlsPluginOptions): PluginOption;
41
45
 
42
46
  export { type ViteCaddyTlsPluginOptions, viteCaddyTlsPlugin as default };
package/dist/index.js CHANGED
@@ -1,18 +1,28 @@
1
1
  // src/index.ts
2
2
  import { execSync as execSync2 } from "child_process";
3
- import path from "path";
3
+ import { createHash as createHash2 } from "crypto";
4
+ import path2 from "path";
4
5
 
5
6
  // src/utils.ts
6
7
  import { execSync } from "child_process";
8
+ import { createHash } from "crypto";
9
+ import { open, unlink } from "fs/promises";
10
+ import os from "os";
11
+ import path from "path";
7
12
  var DEFAULT_SERVER_NAME = "srv0";
8
13
  var DEFAULT_CADDY_API_URL = "http://localhost:2019";
9
- var caddyApiUrl = DEFAULT_CADDY_API_URL;
10
- function setCaddyApiUrl(url) {
11
- caddyApiUrl = url;
12
- }
13
- function getCaddyApiUrl() {
14
- return caddyApiUrl;
15
- }
14
+ var CADDY_ADMIN_ORIGIN_POLICY_ERROR_MESSAGE = "Caddy Admin API rejected request due to origin policy. Check caddyApiUrl and admin origin settings.";
15
+ var CONNECTIVITY_ERROR_CODES = /* @__PURE__ */ new Set([
16
+ "ECONNREFUSED",
17
+ "ECONNRESET",
18
+ "EHOSTUNREACH",
19
+ "ENETUNREACH",
20
+ "ENOTFOUND",
21
+ "ETIMEDOUT",
22
+ "UND_ERR_CONNECT_TIMEOUT",
23
+ "UND_ERR_HEADERS_TIMEOUT",
24
+ "UND_ERR_SOCKET"
25
+ ]);
16
26
  function isRecord(value) {
17
27
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
28
  }
@@ -27,6 +37,126 @@ function parseConfig(text) {
27
37
  function isTlsPolicyOverlapError(text) {
28
38
  return text.includes("cannot apply more than one automation policy to host");
29
39
  }
40
+ function getApiUrl(apiUrl) {
41
+ return apiUrl ?? DEFAULT_CADDY_API_URL;
42
+ }
43
+ function toError(error) {
44
+ if (error instanceof Error) return error;
45
+ return new Error(String(error));
46
+ }
47
+ function isOriginPolicyError(status, text) {
48
+ if (status !== 403) return false;
49
+ const normalizedText = text.toLowerCase();
50
+ return normalizedText.includes("origin") && normalizedText.includes("not allowed");
51
+ }
52
+ function buildCaddyRequestError(message, status, text) {
53
+ if (isOriginPolicyError(status, text)) {
54
+ return new Error(CADDY_ADMIN_ORIGIN_POLICY_ERROR_MESSAGE);
55
+ }
56
+ const normalizedText = text.trim();
57
+ if (!normalizedText) {
58
+ return new Error(`${message}: HTTP ${status}`);
59
+ }
60
+ return new Error(`${message}: ${normalizedText}`);
61
+ }
62
+ function getErrorCode(error) {
63
+ if (!error || typeof error !== "object") return void 0;
64
+ if ("code" in error && typeof error.code === "string") {
65
+ return error.code;
66
+ }
67
+ if ("cause" in error) {
68
+ const cause = error.cause;
69
+ if (cause && typeof cause === "object") {
70
+ if ("code" in cause && typeof cause.code === "string") {
71
+ return cause.code;
72
+ }
73
+ }
74
+ }
75
+ return void 0;
76
+ }
77
+ function isConnectivityError(error) {
78
+ const code = getErrorCode(error);
79
+ return Boolean(code) && CONNECTIVITY_ERROR_CODES.has(code);
80
+ }
81
+ function getAdminOrigin(apiUrl, adminOrigin) {
82
+ const originSource = adminOrigin ?? getApiUrl(apiUrl);
83
+ try {
84
+ return new URL(originSource).origin;
85
+ } catch (e) {
86
+ return new URL(getApiUrl(apiUrl)).origin;
87
+ }
88
+ }
89
+ async function caddyFetch(input, init, apiUrl, adminOrigin) {
90
+ const headers = new Headers(init?.headers);
91
+ headers.set("Origin", getAdminOrigin(apiUrl, adminOrigin));
92
+ return fetch(input, {
93
+ ...init,
94
+ headers
95
+ });
96
+ }
97
+ async function checkCaddyAdminStatus(apiUrl, adminOrigin) {
98
+ try {
99
+ const res = await caddyFetch(`${getApiUrl(apiUrl)}/config/`, void 0, apiUrl, adminOrigin);
100
+ if (res.ok) {
101
+ return { status: "running" };
102
+ }
103
+ const text = await res.text();
104
+ return {
105
+ status: "api-error",
106
+ error: buildCaddyRequestError("Failed to read Caddy config", res.status, text)
107
+ };
108
+ } catch (e) {
109
+ const error = toError(e);
110
+ if (isConnectivityError(error)) {
111
+ return {
112
+ status: "connectivity-error",
113
+ error
114
+ };
115
+ }
116
+ return {
117
+ status: "api-error",
118
+ error
119
+ };
120
+ }
121
+ }
122
+ async function assertCaddyResponse(res, message) {
123
+ if (res.ok) return;
124
+ const text = await res.text();
125
+ throw buildCaddyRequestError(message, res.status, text);
126
+ }
127
+ function getLockPath(apiUrl) {
128
+ const key = createHash("sha1").update(getApiUrl(apiUrl)).digest("hex").slice(0, 12);
129
+ return path.join(os.tmpdir(), `vite-plugin-caddy-multiple-tls-${key}.lock`);
130
+ }
131
+ function sleep(ms) {
132
+ return new Promise((resolve) => setTimeout(resolve, ms));
133
+ }
134
+ async function withApiLock(apiUrl, fn) {
135
+ const lockPath = getLockPath(apiUrl);
136
+ const startedAt = Date.now();
137
+ const timeoutMs = 5e3;
138
+ while (true) {
139
+ try {
140
+ const handle = await open(lockPath, "wx");
141
+ try {
142
+ await fn();
143
+ } finally {
144
+ await handle.close();
145
+ await unlink(lockPath).catch(() => void 0);
146
+ }
147
+ return;
148
+ } catch (e) {
149
+ if (e.code !== "EEXIST") {
150
+ throw e;
151
+ }
152
+ if (Date.now() - startedAt >= timeoutMs) {
153
+ await fn();
154
+ return;
155
+ }
156
+ await sleep(50);
157
+ }
158
+ }
159
+ }
30
160
  function validateCaddyIsInstalled() {
31
161
  try {
32
162
  execSync("caddy version");
@@ -36,31 +166,36 @@ function validateCaddyIsInstalled() {
36
166
  return false;
37
167
  }
38
168
  }
39
- async function isCaddyRunning() {
169
+ async function startCaddy(apiUrl, adminOrigin) {
40
170
  try {
41
- const res = await fetch(`${caddyApiUrl}/config/`);
42
- return res.ok;
171
+ execSync("caddy start", { stdio: "ignore" });
43
172
  } catch (e) {
44
- return false;
45
173
  }
46
- }
47
- async function startCaddy() {
48
- try {
49
- execSync("caddy start", { stdio: "ignore" });
50
- for (let i = 0; i < 10; i++) {
51
- if (await isCaddyRunning()) return true;
52
- await new Promise((r) => setTimeout(r, 500));
174
+ for (let i = 0; i < 10; i++) {
175
+ const status = await checkCaddyAdminStatus(apiUrl, adminOrigin);
176
+ if (status.status === "running") return true;
177
+ if (status.status === "api-error") {
178
+ throw status.error;
53
179
  }
54
- return false;
55
- } catch (e) {
56
- console.error("Failed to start Caddy:", e);
57
- return false;
180
+ await sleep(500);
58
181
  }
182
+ return false;
59
183
  }
60
- async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
61
- const serverUrl = `${caddyApiUrl}/config/apps/http/servers/${serverName}`;
62
- const res = await fetch(serverUrl);
184
+ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME, apiUrl, adminOrigin) {
185
+ const resolvedApiUrl = getApiUrl(apiUrl);
186
+ const serverUrl = `${resolvedApiUrl}/config/apps/http/servers/${serverName}`;
187
+ const res = await caddyFetch(serverUrl, void 0, apiUrl, adminOrigin);
63
188
  if (res.ok) return;
189
+ if (res.status === 403) {
190
+ const text = await res.text();
191
+ if (isOriginPolicyError(res.status, text)) {
192
+ throw buildCaddyRequestError(
193
+ "Failed to initialize Caddy base configuration",
194
+ res.status,
195
+ text
196
+ );
197
+ }
198
+ }
64
199
  const baseConfig = {
65
200
  listen: [":443"],
66
201
  routes: []
@@ -70,11 +205,13 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
70
205
  [serverName]: baseConfig
71
206
  }
72
207
  };
73
- const configRes = await fetch(`${caddyApiUrl}/config/`);
74
- if (!configRes.ok) {
75
- const text = await configRes.text();
76
- throw new Error(`Failed to read Caddy config: ${text}`);
77
- }
208
+ const configRes = await caddyFetch(
209
+ `${resolvedApiUrl}/config/`,
210
+ void 0,
211
+ apiUrl,
212
+ adminOrigin
213
+ );
214
+ await assertCaddyResponse(configRes, "Failed to read Caddy config");
78
215
  const configText = await configRes.text();
79
216
  const config = parseConfig(configText);
80
217
  if (config === void 0) {
@@ -82,18 +219,27 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
82
219
  }
83
220
  const isEmptyConfig = configText.trim() === "" || config === null || isRecord(config) && Object.keys(config).length === 0;
84
221
  if (isEmptyConfig) {
85
- const loadRes = await fetch(`${caddyApiUrl}/load`, {
86
- method: "POST",
87
- headers: { "Content-Type": "application/json" },
88
- body: JSON.stringify({
89
- apps: {
90
- http: httpAppConfig
91
- }
92
- })
93
- });
222
+ const loadRes = await caddyFetch(
223
+ `${resolvedApiUrl}/load`,
224
+ {
225
+ method: "POST",
226
+ headers: { "Content-Type": "application/json" },
227
+ body: JSON.stringify({
228
+ apps: {
229
+ http: httpAppConfig
230
+ }
231
+ })
232
+ },
233
+ apiUrl,
234
+ adminOrigin
235
+ );
94
236
  if (!loadRes.ok) {
95
237
  const text = await loadRes.text();
96
- throw new Error(`Failed to initialize Caddy base configuration: ${text}`);
238
+ throw buildCaddyRequestError(
239
+ "Failed to initialize Caddy base configuration",
240
+ loadRes.status,
241
+ text
242
+ );
97
243
  }
98
244
  return;
99
245
  }
@@ -104,81 +250,136 @@ async function ensureBaseConfig(serverName = DEFAULT_SERVER_NAME) {
104
250
  let hasHttp = isRecord(http);
105
251
  let hasServers = isRecord(servers);
106
252
  if (!hasApps) {
107
- const createAppsRes = await fetch(`${caddyApiUrl}/config/apps`, {
108
- method: "PUT",
109
- headers: { "Content-Type": "application/json" },
110
- body: JSON.stringify({})
111
- });
253
+ const createAppsRes = await caddyFetch(
254
+ `${resolvedApiUrl}/config/apps`,
255
+ {
256
+ method: "PUT",
257
+ headers: { "Content-Type": "application/json" },
258
+ body: JSON.stringify({})
259
+ },
260
+ apiUrl,
261
+ adminOrigin
262
+ );
112
263
  if (!createAppsRes.ok && createAppsRes.status !== 409) {
113
264
  const text = await createAppsRes.text();
114
- throw new Error(`Failed to initialize Caddy base configuration: ${text}`);
265
+ throw buildCaddyRequestError(
266
+ "Failed to initialize Caddy base configuration",
267
+ createAppsRes.status,
268
+ text
269
+ );
115
270
  }
116
271
  hasApps = true;
117
272
  }
118
273
  if (!hasHttp) {
119
- const createHttpRes = await fetch(`${caddyApiUrl}/config/apps/http`, {
120
- method: "PUT",
121
- headers: { "Content-Type": "application/json" },
122
- body: JSON.stringify({ servers: {} })
123
- });
274
+ const createHttpRes = await caddyFetch(
275
+ `${resolvedApiUrl}/config/apps/http`,
276
+ {
277
+ method: "PUT",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ servers: {} })
280
+ },
281
+ apiUrl,
282
+ adminOrigin
283
+ );
124
284
  if (!createHttpRes.ok && createHttpRes.status !== 409) {
125
285
  const text = await createHttpRes.text();
126
- throw new Error(`Failed to initialize Caddy base configuration: ${text}`);
286
+ throw buildCaddyRequestError(
287
+ "Failed to initialize Caddy base configuration",
288
+ createHttpRes.status,
289
+ text
290
+ );
127
291
  }
128
292
  hasHttp = true;
129
293
  hasServers = true;
130
294
  }
131
295
  if (!hasServers) {
132
- const createServersRes = await fetch(`${caddyApiUrl}/config/apps/http/servers`, {
133
- method: "PUT",
134
- headers: { "Content-Type": "application/json" },
135
- body: JSON.stringify({})
136
- });
296
+ const createServersRes = await caddyFetch(
297
+ `${resolvedApiUrl}/config/apps/http/servers`,
298
+ {
299
+ method: "PUT",
300
+ headers: { "Content-Type": "application/json" },
301
+ body: JSON.stringify({})
302
+ },
303
+ apiUrl,
304
+ adminOrigin
305
+ );
137
306
  if (!createServersRes.ok && createServersRes.status !== 409) {
138
307
  const text = await createServersRes.text();
139
- throw new Error(`Failed to initialize Caddy base configuration: ${text}`);
308
+ throw buildCaddyRequestError(
309
+ "Failed to initialize Caddy base configuration",
310
+ createServersRes.status,
311
+ text
312
+ );
140
313
  }
141
314
  }
142
- const createServerRes = await fetch(serverUrl, {
143
- method: "PUT",
144
- headers: { "Content-Type": "application/json" },
145
- body: JSON.stringify(baseConfig)
146
- });
315
+ const createServerRes = await caddyFetch(
316
+ serverUrl,
317
+ {
318
+ method: "PUT",
319
+ headers: { "Content-Type": "application/json" },
320
+ body: JSON.stringify(baseConfig)
321
+ },
322
+ apiUrl,
323
+ adminOrigin
324
+ );
147
325
  if (!createServerRes.ok && createServerRes.status !== 409) {
148
326
  const text = await createServerRes.text();
149
- throw new Error(`Failed to initialize Caddy base configuration: ${text}`);
327
+ throw buildCaddyRequestError(
328
+ "Failed to initialize Caddy base configuration",
329
+ createServerRes.status,
330
+ text
331
+ );
150
332
  }
151
333
  }
152
- async function ensureTlsAutomation() {
153
- const policiesUrl = `${caddyApiUrl}/config/apps/tls/automation/policies`;
154
- const policiesRes = await fetch(policiesUrl);
334
+ async function ensureTlsAutomation(apiUrl, adminOrigin) {
335
+ const resolvedApiUrl = getApiUrl(apiUrl);
336
+ const policiesUrl = `${resolvedApiUrl}/config/apps/tls/automation/policies`;
337
+ const policiesRes = await caddyFetch(policiesUrl, void 0, apiUrl, adminOrigin);
155
338
  if (policiesRes.ok) return;
156
339
  const policiesText = await policiesRes.text();
157
340
  if (policiesRes.status !== 404 && !policiesText.includes("invalid traversal path")) {
158
- throw new Error(
159
- `Failed to initialize Caddy TLS automation: ${policiesText}`
341
+ throw buildCaddyRequestError(
342
+ "Failed to initialize Caddy TLS automation",
343
+ policiesRes.status,
344
+ policiesText
160
345
  );
161
346
  }
162
- const automationRes = await fetch(`${caddyApiUrl}/config/apps/tls/automation`, {
163
- method: "PUT",
164
- headers: { "Content-Type": "application/json" },
165
- body: JSON.stringify({ policies: [] })
166
- });
347
+ const automationRes = await caddyFetch(
348
+ `${resolvedApiUrl}/config/apps/tls/automation`,
349
+ {
350
+ method: "PUT",
351
+ headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify({ policies: [] })
353
+ },
354
+ apiUrl,
355
+ adminOrigin
356
+ );
167
357
  if (automationRes.ok || automationRes.status === 409) return;
168
358
  const automationText = await automationRes.text();
169
359
  if (!automationText.includes("invalid traversal path")) {
170
- throw new Error(
171
- `Failed to initialize Caddy TLS automation: ${automationText}`
360
+ throw buildCaddyRequestError(
361
+ "Failed to initialize Caddy TLS automation",
362
+ automationRes.status,
363
+ automationText
172
364
  );
173
365
  }
174
- const tlsRes = await fetch(`${caddyApiUrl}/config/apps/tls`, {
175
- method: "PUT",
176
- headers: { "Content-Type": "application/json" },
177
- body: JSON.stringify({ automation: { policies: [] } })
178
- });
366
+ const tlsRes = await caddyFetch(
367
+ `${resolvedApiUrl}/config/apps/tls`,
368
+ {
369
+ method: "PUT",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify({ automation: { policies: [] } })
372
+ },
373
+ apiUrl,
374
+ adminOrigin
375
+ );
179
376
  if (!tlsRes.ok && tlsRes.status !== 409) {
180
377
  const text = await tlsRes.text();
181
- throw new Error(`Failed to initialize Caddy TLS automation: ${text}`);
378
+ throw buildCaddyRequestError(
379
+ "Failed to initialize Caddy TLS automation",
380
+ tlsRes.status,
381
+ text
382
+ );
182
383
  }
183
384
  }
184
385
  function formatDialAddress(host, port) {
@@ -187,7 +388,50 @@ function formatDialAddress(host, port) {
187
388
  }
188
389
  return `${host}:${port}`;
189
390
  }
190
- async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAME, upstreamHost = "127.0.0.1", upstreamHostHeader) {
391
+ function extractMatchedHosts(route) {
392
+ if (!isRecord(route)) return [];
393
+ const match = route.match;
394
+ if (!Array.isArray(match)) return [];
395
+ const hosts = [];
396
+ for (const item of match) {
397
+ if (!isRecord(item) || !Array.isArray(item.host)) continue;
398
+ for (const host of item.host) {
399
+ if (typeof host === "string") {
400
+ hosts.push(host);
401
+ }
402
+ }
403
+ }
404
+ return hosts;
405
+ }
406
+ function intersectsDomains(targetDomains, routeDomains) {
407
+ if (targetDomains.length === 0 || routeDomains.length === 0) return false;
408
+ const targetSet = new Set(targetDomains);
409
+ return routeDomains.some((domain) => targetSet.has(domain));
410
+ }
411
+ async function cleanupStaleRoutesForDomains(domains, currentRouteId, serverName = DEFAULT_SERVER_NAME, apiUrl, adminOrigin) {
412
+ if (domains.length === 0) return;
413
+ const res = await caddyFetch(
414
+ `${getApiUrl(apiUrl)}/config/apps/http/servers/${serverName}/routes`,
415
+ void 0,
416
+ apiUrl,
417
+ adminOrigin
418
+ );
419
+ if (!res.ok) return;
420
+ const text = await res.text();
421
+ const parsed = parseConfig(text);
422
+ if (!Array.isArray(parsed)) return;
423
+ for (const route of parsed) {
424
+ if (!isRecord(route)) continue;
425
+ const id = route["@id"];
426
+ if (typeof id !== "string") continue;
427
+ if (!id.startsWith("vite-proxy-")) continue;
428
+ if (id === currentRouteId) continue;
429
+ const routeDomains = extractMatchedHosts(route);
430
+ if (!intersectsDomains(domains, routeDomains)) continue;
431
+ await removeRoute(id, apiUrl, adminOrigin);
432
+ }
433
+ }
434
+ async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAME, upstreamHost = "127.0.0.1", upstreamHostHeader, apiUrl, adminOrigin) {
191
435
  const handlers = [];
192
436
  if (cors) {
193
437
  handlers.push({
@@ -237,22 +481,24 @@ async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAM
237
481
  ],
238
482
  terminal: true
239
483
  };
240
- const res = await fetch(
241
- `${caddyApiUrl}/config/apps/http/servers/${serverName}/routes`,
484
+ const res = await caddyFetch(
485
+ `${getApiUrl(apiUrl)}/config/apps/http/servers/${serverName}/routes`,
242
486
  {
243
487
  method: "POST",
244
488
  // Append to routes list
245
489
  headers: { "Content-Type": "application/json" },
246
490
  body: JSON.stringify(route)
247
- }
491
+ },
492
+ apiUrl,
493
+ adminOrigin
248
494
  );
249
495
  if (!res.ok) {
250
496
  const text = await res.text();
251
- throw new Error(`Failed to add route: ${text}`);
497
+ throw buildCaddyRequestError("Failed to add route", res.status, text);
252
498
  }
253
499
  }
254
- async function addTlsPolicy(id, domains) {
255
- await ensureTlsAutomation();
500
+ async function addTlsPolicy(id, domains, apiUrl, adminOrigin) {
501
+ await ensureTlsAutomation(apiUrl, adminOrigin);
256
502
  const policy = {
257
503
  "@id": id,
258
504
  subjects: domains,
@@ -262,39 +508,78 @@ async function addTlsPolicy(id, domains) {
262
508
  }
263
509
  ]
264
510
  };
265
- const res = await fetch(`${caddyApiUrl}/config/apps/tls/automation/policies`, {
266
- method: "POST",
267
- headers: { "Content-Type": "application/json" },
268
- body: JSON.stringify(policy)
269
- });
511
+ const res = await caddyFetch(
512
+ `${getApiUrl(apiUrl)}/config/apps/tls/automation/policies`,
513
+ {
514
+ method: "POST",
515
+ headers: { "Content-Type": "application/json" },
516
+ body: JSON.stringify(policy)
517
+ },
518
+ apiUrl,
519
+ adminOrigin
520
+ );
270
521
  if (!res.ok) {
271
522
  const text = await res.text();
272
523
  if (isTlsPolicyOverlapError(text)) {
273
524
  return;
274
525
  }
275
- throw new Error(`Failed to add TLS policy: ${text}`);
526
+ throw buildCaddyRequestError("Failed to add TLS policy", res.status, text);
276
527
  }
277
528
  }
278
- async function removeRoute(id) {
279
- const res = await fetch(`${caddyApiUrl}/id/${id}`, {
280
- method: "DELETE"
281
- });
529
+ async function removeRoute(id, apiUrl, adminOrigin) {
530
+ const res = await caddyFetch(
531
+ `${getApiUrl(apiUrl)}/id/${id}`,
532
+ {
533
+ method: "DELETE"
534
+ },
535
+ apiUrl,
536
+ adminOrigin
537
+ );
282
538
  if (!res.ok && res.status !== 404) {
283
- console.error(`Failed to remove route ${id}`);
539
+ const text = await res.text();
540
+ const error = buildCaddyRequestError(`Failed to remove route ${id}`, res.status, text);
541
+ console.error(error.message);
284
542
  return false;
285
543
  }
286
544
  return true;
287
545
  }
288
- async function removeTlsPolicy(id) {
289
- const res = await fetch(`${caddyApiUrl}/id/${id}`, {
290
- method: "DELETE"
291
- });
546
+ async function removeTlsPolicy(id, apiUrl, adminOrigin) {
547
+ const res = await caddyFetch(
548
+ `${getApiUrl(apiUrl)}/id/${id}`,
549
+ {
550
+ method: "DELETE"
551
+ },
552
+ apiUrl,
553
+ adminOrigin
554
+ );
292
555
  if (!res.ok && res.status !== 404) {
293
- console.error(`Failed to remove TLS policy ${id}`);
556
+ const text = await res.text();
557
+ const error = buildCaddyRequestError(
558
+ `Failed to remove TLS policy ${id}`,
559
+ res.status,
560
+ text
561
+ );
562
+ console.error(error.message);
294
563
  return false;
295
564
  }
296
565
  return true;
297
566
  }
567
+ async function ensureCaddyReady(serverName = DEFAULT_SERVER_NAME, apiUrl, adminOrigin) {
568
+ await withApiLock(apiUrl, async () => {
569
+ const status = await checkCaddyAdminStatus(apiUrl, adminOrigin);
570
+ if (status.status === "api-error") {
571
+ throw status.error;
572
+ }
573
+ let running = status.status === "running";
574
+ if (status.status === "connectivity-error") {
575
+ running = await startCaddy(apiUrl, adminOrigin);
576
+ }
577
+ if (!running) {
578
+ throw new Error("Failed to start Caddy server.");
579
+ }
580
+ await ensureBaseConfig(serverName, apiUrl, adminOrigin);
581
+ });
582
+ }
298
583
 
299
584
  // src/index.ts
300
585
  var LOOPBACK_DOMAINS = {
@@ -310,7 +595,7 @@ function getGitRepoInfo() {
310
595
  try {
311
596
  const repoRoot = execGit("git rev-parse --show-toplevel");
312
597
  if (repoRoot) {
313
- info.repo = path.basename(repoRoot);
598
+ info.repo = path2.basename(repoRoot);
314
599
  }
315
600
  } catch (e) {
316
601
  }
@@ -364,7 +649,13 @@ function buildDerivedDomain(options) {
364
649
  const repoLabel = sanitizeDomainLabel(repo);
365
650
  const branchLabel = sanitizeDomainLabel(branch);
366
651
  if (!repoLabel || !branchLabel) return null;
367
- return `${repoLabel}.${branchLabel}.${baseDomain}`;
652
+ const labels = [repoLabel, branchLabel];
653
+ if (options.instanceLabel !== void 0) {
654
+ const instanceLabel = sanitizeDomainLabel(options.instanceLabel);
655
+ if (!instanceLabel) return null;
656
+ labels.push(instanceLabel);
657
+ }
658
+ return `${labels.join(".")}.${baseDomain}`;
368
659
  }
369
660
  function normalizeDomain(domain) {
370
661
  const trimmed = domain.trim().toLowerCase();
@@ -382,6 +673,15 @@ function normalizeCaddyApiUrl(url) {
382
673
  if (!trimmed) return null;
383
674
  return trimmed.replace(/\/+$/g, "");
384
675
  }
676
+ function normalizeCaddyAdminOrigin(origin) {
677
+ const trimmed = origin.trim();
678
+ if (!trimmed) return null;
679
+ try {
680
+ return new URL(trimmed).origin;
681
+ } catch (e) {
682
+ return null;
683
+ }
684
+ }
385
685
  function resolveDomains(options) {
386
686
  if (options.domain) {
387
687
  return normalizeDomains(options.domain);
@@ -396,22 +696,35 @@ function viteCaddyTlsPlugin({
396
696
  loopbackDomain,
397
697
  repo,
398
698
  branch,
699
+ instanceLabel,
399
700
  cors,
400
701
  serverName,
401
- caddyApiUrl: caddyApiUrl2,
702
+ caddyApiUrl,
703
+ caddyAdminOrigin,
402
704
  internalTls,
403
705
  upstreamHostHeader
404
706
  } = {}) {
405
- if (caddyApiUrl2 !== void 0) {
406
- const normalizedApiUrl = normalizeCaddyApiUrl(caddyApiUrl2);
407
- if (normalizedApiUrl) {
408
- setCaddyApiUrl(normalizedApiUrl);
409
- } else {
410
- setCaddyApiUrl(DEFAULT_CADDY_API_URL);
411
- console.warn(
412
- `caddyApiUrl is empty after trimming. Falling back to ${DEFAULT_CADDY_API_URL}.`
413
- );
414
- }
707
+ const normalizedApiUrl = caddyApiUrl ? normalizeCaddyApiUrl(caddyApiUrl) : null;
708
+ const pluginCaddyApiUrl = normalizedApiUrl ?? DEFAULT_CADDY_API_URL;
709
+ const normalizedAdminOrigin = caddyAdminOrigin ? normalizeCaddyAdminOrigin(caddyAdminOrigin) : null;
710
+ const pluginCaddyAdminOrigin = normalizedAdminOrigin ?? pluginCaddyApiUrl;
711
+ if (caddyApiUrl !== void 0 && !normalizedApiUrl) {
712
+ console.warn(
713
+ `caddyApiUrl is empty after trimming. Falling back to ${DEFAULT_CADDY_API_URL}.`
714
+ );
715
+ }
716
+ if (caddyAdminOrigin !== void 0 && !normalizedAdminOrigin) {
717
+ console.warn(
718
+ `caddyAdminOrigin is invalid. Falling back to ${pluginCaddyApiUrl}.`
719
+ );
720
+ }
721
+ function getInstanceKey(domains, configRoot) {
722
+ const keyMaterial = JSON.stringify({
723
+ domains: [...domains].sort(),
724
+ cwd: process.cwd(),
725
+ root: configRoot ?? null
726
+ });
727
+ return createHash2("sha1").update(keyMaterial).digest("hex").slice(0, 12);
415
728
  }
416
729
  function isPreviewServer(server) {
417
730
  return server.config.isProduction;
@@ -440,10 +753,11 @@ function viteCaddyTlsPlugin({
440
753
  baseDomain,
441
754
  loopbackDomain,
442
755
  repo,
443
- branch
756
+ branch,
757
+ instanceLabel
444
758
  });
445
759
  const domainArray = resolvedDomains ?? [];
446
- const routeId = `vite-proxy-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
760
+ const routeId = `vite-proxy-${getInstanceKey(domainArray, config.root)}`;
447
761
  const shouldUseInternalTls = internalTls ?? (baseDomain !== void 0 || loopbackDomain !== void 0 || domain !== void 0);
448
762
  const tlsPolicyId = shouldUseInternalTls ? `${routeId}-tls` : null;
449
763
  let cleanupStarted = false;
@@ -458,6 +772,9 @@ function viteCaddyTlsPlugin({
458
772
  if (baseDomain !== void 0 && !normalizeBaseDomain(baseDomain)) {
459
773
  issues.push("`baseDomain` is empty after trimming");
460
774
  }
775
+ if (instanceLabel !== void 0 && !sanitizeDomainLabel(instanceLabel)) {
776
+ issues.push("`instanceLabel` is empty after sanitization");
777
+ }
461
778
  const info = getGitRepoInfo();
462
779
  const resolvedRepo = repo ?? info.repo;
463
780
  const resolvedBranch = branch ?? info.branch;
@@ -553,9 +870,15 @@ function viteCaddyTlsPlugin({
553
870
  if (cleanupStarted) return;
554
871
  cleanupStarted = true;
555
872
  if (tlsPolicyId) {
556
- await removeWithRetry(() => removeTlsPolicy(tlsPolicyId), "TLS policy");
873
+ await removeWithRetry(
874
+ () => removeTlsPolicy(tlsPolicyId, pluginCaddyApiUrl, pluginCaddyAdminOrigin),
875
+ "TLS policy"
876
+ );
557
877
  }
558
- await removeWithRetry(() => removeRoute(routeId), "route");
878
+ await removeWithRetry(
879
+ () => removeRoute(routeId, pluginCaddyApiUrl, pluginCaddyAdminOrigin),
880
+ "route"
881
+ );
559
882
  }
560
883
  function onServerClose() {
561
884
  void cleanupRoute();
@@ -599,32 +922,38 @@ function viteCaddyTlsPlugin({
599
922
  if (!validateCaddyIsInstalled()) {
600
923
  return;
601
924
  }
602
- let running = await isCaddyRunning();
603
- if (!running) {
604
- running = await startCaddy();
605
- if (!running) {
606
- console.error("Failed to start Caddy server.");
607
- return;
608
- }
609
- }
610
925
  try {
611
- await ensureBaseConfig(serverName);
926
+ await ensureCaddyReady(serverName, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
612
927
  } catch (e) {
613
928
  console.error(
614
- `Failed to configure Caddy base settings. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
929
+ `Failed to configure Caddy base settings. Is the Caddy Admin API reachable at ${pluginCaddyApiUrl}?`,
615
930
  e
616
931
  );
617
932
  return;
618
933
  }
619
934
  const port = getServerPort();
620
935
  const upstreamHost = getUpstreamHost();
936
+ await cleanupStaleRoutesForDomains(
937
+ domainArray,
938
+ routeId,
939
+ serverName,
940
+ pluginCaddyApiUrl,
941
+ pluginCaddyAdminOrigin
942
+ );
943
+ await removeRoute(routeId, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
621
944
  if (tlsPolicyId) {
945
+ await removeTlsPolicy(tlsPolicyId, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
622
946
  try {
623
- await addTlsPolicy(tlsPolicyId, domainArray);
947
+ await addTlsPolicy(
948
+ tlsPolicyId,
949
+ domainArray,
950
+ pluginCaddyApiUrl,
951
+ pluginCaddyAdminOrigin
952
+ );
624
953
  tlsPolicyAdded = true;
625
954
  } catch (e) {
626
955
  console.error(
627
- `Failed to add TLS policy to Caddy. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
956
+ `Failed to add TLS policy to Caddy. Is the Caddy Admin API reachable at ${pluginCaddyApiUrl}?`,
628
957
  e
629
958
  );
630
959
  return;
@@ -638,14 +967,16 @@ function viteCaddyTlsPlugin({
638
967
  cors,
639
968
  serverName,
640
969
  upstreamHost,
641
- upstreamHostHeader
970
+ upstreamHostHeader,
971
+ pluginCaddyApiUrl,
972
+ pluginCaddyAdminOrigin
642
973
  );
643
974
  } catch (e) {
644
975
  if (tlsPolicyAdded && tlsPolicyId) {
645
- await removeTlsPolicy(tlsPolicyId);
976
+ await removeTlsPolicy(tlsPolicyId, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
646
977
  }
647
978
  console.error(
648
- `Failed to add route to Caddy. Is the Caddy Admin API reachable at ${getCaddyApiUrl()}?`,
979
+ `Failed to add route to Caddy. Is the Caddy Admin API reachable at ${pluginCaddyApiUrl}?`,
649
980
  e
650
981
  );
651
982
  return;
@@ -708,7 +1039,8 @@ function viteCaddyTlsPlugin({
708
1039
  baseDomain,
709
1040
  loopbackDomain,
710
1041
  repo,
711
- branch
1042
+ branch,
1043
+ instanceLabel
712
1044
  });
713
1045
  const defaultHmrDomain = resolvedDomains?.[0];
714
1046
  const hmrConfig = userConfig.server?.hmr === void 0 && defaultHmrDomain ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-caddy-multiple-tls",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Vite plugin that uses Caddy to provide local HTTPS with derived domains.",
5
5
  "keywords": [
6
6
  "vite",