vite-plugin-caddy-multiple-tls 1.6.1 → 1.7.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 +12 -0
- package/dist/index.js +433 -39
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -112,6 +112,10 @@ export default config;
|
|
|
112
112
|
|
|
113
113
|
This derives a host like `<repo>.<branch>.web-1.localhost`.
|
|
114
114
|
|
|
115
|
+
The plugin now treats hostname ownership as explicit. If another live Vite server already owns the resolved domain, it will refuse takeover instead of deleting the other server's route. Use `instanceLabel`, `domain`, or stop the other server first.
|
|
116
|
+
|
|
117
|
+
If a previous Vite process crashed and left stale ownership behind, the plugin will reclaim it automatically and clean up the stale Caddy route before continuing.
|
|
118
|
+
|
|
115
119
|
For a zero-config experience, use `baseDomain: 'localhost'` (the default) so the derived domain works without editing `/etc/hosts`.
|
|
116
120
|
|
|
117
121
|
`internalTls` defaults to `true` when you pass `baseDomain` or `domain`. You can override it if needed.
|
|
@@ -157,6 +161,14 @@ export default config;
|
|
|
157
161
|
|
|
158
162
|
## Troubleshooting
|
|
159
163
|
|
|
164
|
+
### `Cannot claim ... another Vite server already owns this domain`
|
|
165
|
+
|
|
166
|
+
This means another live dev server is already using the resolved hostname.
|
|
167
|
+
|
|
168
|
+
- Stop the other server if you want this one to use the same host.
|
|
169
|
+
- Add `instanceLabel` if both servers should run at the same time.
|
|
170
|
+
- Pass an explicit `domain` if you want total control over the hostname.
|
|
171
|
+
|
|
160
172
|
### `client is not allowed to access from origin ''`
|
|
161
173
|
|
|
162
174
|
This error comes from Caddy Admin API origin enforcement, not from Caddy being down.
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { execSync as execSync2 } from "child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
4
|
import path2 from "path";
|
|
5
5
|
|
|
6
6
|
// src/utils.ts
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
8
|
import { createHash } from "crypto";
|
|
9
|
-
import { open, unlink } from "fs/promises";
|
|
9
|
+
import { mkdir, open, readFile, readdir, rename, unlink, writeFile } from "fs/promises";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import path from "path";
|
|
12
12
|
var DEFAULT_SERVER_NAME = "srv0";
|
|
13
13
|
var DEFAULT_CADDY_API_URL = "http://localhost:2019";
|
|
14
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 ROUTE_ID_PREFIX = "vite-proxy-";
|
|
16
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
17
|
+
var LOCK_RETRY_MS = 50;
|
|
18
|
+
var ROUTE_OWNERSHIP_VERSION = 1;
|
|
19
|
+
var ROUTE_OWNERSHIP_STALE_AFTER_MS = 3e4;
|
|
20
|
+
var ROUTE_OWNERSHIP_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
15
21
|
var CONNECTIVITY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
16
22
|
"ECONNREFUSED",
|
|
17
23
|
"ECONNRESET",
|
|
@@ -26,6 +32,57 @@ var CONNECTIVITY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
26
32
|
function isRecord(value) {
|
|
27
33
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
34
|
}
|
|
35
|
+
function normalizeRouteOwnershipDomains(domains) {
|
|
36
|
+
return Array.from(new Set(domains)).sort();
|
|
37
|
+
}
|
|
38
|
+
function getRouteOwnershipDirectory() {
|
|
39
|
+
return path.join(os.tmpdir(), "vite-plugin-caddy-multiple-tls", "owners");
|
|
40
|
+
}
|
|
41
|
+
function getRouteOwnershipPaths(scope) {
|
|
42
|
+
const key = createHash("sha1").update(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
domains: normalizeRouteOwnershipDomains(scope.domains),
|
|
45
|
+
serverName: scope.serverName,
|
|
46
|
+
caddyApiUrl: scope.caddyApiUrl
|
|
47
|
+
})
|
|
48
|
+
).digest("hex").slice(0, 20);
|
|
49
|
+
const scopeLockKey = createHash("sha1").update(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
serverName: scope.serverName,
|
|
52
|
+
caddyApiUrl: scope.caddyApiUrl
|
|
53
|
+
})
|
|
54
|
+
).digest("hex").slice(0, 20);
|
|
55
|
+
const directory = getRouteOwnershipDirectory();
|
|
56
|
+
return {
|
|
57
|
+
directory,
|
|
58
|
+
recordPath: path.join(directory, `${key}.json`),
|
|
59
|
+
lockPath: path.join(directory, `scope-${scopeLockKey}.lock`)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isRouteOwnershipRecord(value) {
|
|
63
|
+
if (!isRecord(value)) return false;
|
|
64
|
+
if (value.version !== ROUTE_OWNERSHIP_VERSION) return false;
|
|
65
|
+
if (typeof value.ownerId !== "string" || !value.ownerId) return false;
|
|
66
|
+
if (typeof value.pid !== "number" || !Number.isFinite(value.pid)) return false;
|
|
67
|
+
if (typeof value.cwd !== "string") return false;
|
|
68
|
+
if (value.configRoot !== null && typeof value.configRoot !== "string") return false;
|
|
69
|
+
if (!Array.isArray(value.domains) || value.domains.some((domain) => typeof domain !== "string")) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (typeof value.routeId !== "string" || !value.routeId) return false;
|
|
73
|
+
if (value.tlsPolicyId !== null && typeof value.tlsPolicyId !== "string") return false;
|
|
74
|
+
if (typeof value.serverName !== "string" || !value.serverName) return false;
|
|
75
|
+
if (typeof value.caddyApiUrl !== "string" || !value.caddyApiUrl) return false;
|
|
76
|
+
if (typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt)) return false;
|
|
77
|
+
if (typeof value.lastSeenAt !== "number" || !Number.isFinite(value.lastSeenAt)) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function normalizeRouteOwnershipRecord(record) {
|
|
81
|
+
return {
|
|
82
|
+
...record,
|
|
83
|
+
domains: normalizeRouteOwnershipDomains(record.domains)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
29
86
|
function parseConfig(text) {
|
|
30
87
|
if (!text.trim()) return {};
|
|
31
88
|
try {
|
|
@@ -59,6 +116,9 @@ function buildCaddyRequestError(message, status, text) {
|
|
|
59
116
|
}
|
|
60
117
|
return new Error(`${message}: ${normalizedText}`);
|
|
61
118
|
}
|
|
119
|
+
function isNodeError(error) {
|
|
120
|
+
return Boolean(error) && typeof error === "object" && "code" in error;
|
|
121
|
+
}
|
|
62
122
|
function getErrorCode(error) {
|
|
63
123
|
if (!error || typeof error !== "object") return void 0;
|
|
64
124
|
if ("code" in error && typeof error.code === "string") {
|
|
@@ -131,10 +191,9 @@ function getLockPath(apiUrl) {
|
|
|
131
191
|
function sleep(ms) {
|
|
132
192
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
193
|
}
|
|
134
|
-
async function
|
|
135
|
-
|
|
194
|
+
async function withFileLock(lockPath, fn) {
|
|
195
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
136
196
|
const startedAt = Date.now();
|
|
137
|
-
const timeoutMs = 5e3;
|
|
138
197
|
while (true) {
|
|
139
198
|
try {
|
|
140
199
|
const handle = await open(lockPath, "wx");
|
|
@@ -146,16 +205,159 @@ async function withApiLock(apiUrl, fn) {
|
|
|
146
205
|
}
|
|
147
206
|
return;
|
|
148
207
|
} catch (e) {
|
|
149
|
-
if (e.code !== "EEXIST") {
|
|
208
|
+
if (!isNodeError(e) || e.code !== "EEXIST") {
|
|
150
209
|
throw e;
|
|
151
210
|
}
|
|
152
|
-
if (Date.now() - startedAt >=
|
|
211
|
+
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
153
212
|
await fn();
|
|
154
213
|
return;
|
|
155
214
|
}
|
|
156
|
-
await sleep(
|
|
215
|
+
await sleep(LOCK_RETRY_MS);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function withApiLock(apiUrl, fn) {
|
|
220
|
+
await withFileLock(getLockPath(apiUrl), fn);
|
|
221
|
+
}
|
|
222
|
+
async function readRouteOwnershipByPath(recordPath) {
|
|
223
|
+
try {
|
|
224
|
+
const text = await readFile(recordPath, "utf8");
|
|
225
|
+
const parsed = parseConfig(text);
|
|
226
|
+
if (!isRouteOwnershipRecord(parsed)) return null;
|
|
227
|
+
return normalizeRouteOwnershipRecord(parsed);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (isNodeError(e) && e.code === "ENOENT") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function writeRouteOwnership(record) {
|
|
236
|
+
const normalizedRecord = normalizeRouteOwnershipRecord(record);
|
|
237
|
+
const { directory, recordPath } = getRouteOwnershipPaths(normalizedRecord);
|
|
238
|
+
const tempPath = path.join(
|
|
239
|
+
directory,
|
|
240
|
+
`${path.basename(recordPath)}.${process.pid}.${Date.now()}.tmp`
|
|
241
|
+
);
|
|
242
|
+
await mkdir(directory, { recursive: true });
|
|
243
|
+
await writeFile(tempPath, JSON.stringify(normalizedRecord), "utf8");
|
|
244
|
+
await rename(tempPath, recordPath);
|
|
245
|
+
}
|
|
246
|
+
async function listRouteOwnershipRecords(scope) {
|
|
247
|
+
const directory = getRouteOwnershipDirectory();
|
|
248
|
+
let entries;
|
|
249
|
+
try {
|
|
250
|
+
entries = await readdir(directory);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
if (isNodeError(e) && e.code === "ENOENT") {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
const records = await Promise.all(
|
|
258
|
+
entries.filter((entry) => entry.endsWith(".json")).map((entry) => readRouteOwnershipByPath(path.join(directory, entry)))
|
|
259
|
+
);
|
|
260
|
+
return records.filter((record) => {
|
|
261
|
+
return Boolean(
|
|
262
|
+
record && record.serverName === scope.serverName && record.caddyApiUrl === scope.caddyApiUrl
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function isProcessAlive(pid) {
|
|
267
|
+
try {
|
|
268
|
+
process.kill(pid, 0);
|
|
269
|
+
return true;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
return isNodeError(e) && e.code === "EPERM";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function isRouteOwnershipActive(record, now = Date.now()) {
|
|
275
|
+
if (isProcessAlive(record.pid)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return record.pid <= 0 && now - record.lastSeenAt <= ROUTE_OWNERSHIP_STALE_AFTER_MS;
|
|
279
|
+
}
|
|
280
|
+
async function claimRouteOwnership(record) {
|
|
281
|
+
const normalizedRecord = normalizeRouteOwnershipRecord(record);
|
|
282
|
+
const { lockPath, recordPath } = getRouteOwnershipPaths(normalizedRecord);
|
|
283
|
+
let claimResult = null;
|
|
284
|
+
await withFileLock(lockPath, async () => {
|
|
285
|
+
const existingRecord = await readRouteOwnershipByPath(recordPath);
|
|
286
|
+
if (existingRecord?.ownerId === normalizedRecord.ownerId) {
|
|
287
|
+
await writeRouteOwnership(normalizedRecord);
|
|
288
|
+
claimResult = {
|
|
289
|
+
status: "claimed",
|
|
290
|
+
currentRecord: normalizedRecord
|
|
291
|
+
};
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const overlappingRecords = (await listRouteOwnershipRecords(normalizedRecord)).filter(
|
|
295
|
+
(candidate) => {
|
|
296
|
+
return candidate.ownerId !== normalizedRecord.ownerId && intersectsDomains(candidate.domains, normalizedRecord.domains);
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
const activeConflict = overlappingRecords.find((candidate) => {
|
|
300
|
+
return isRouteOwnershipActive(candidate);
|
|
301
|
+
});
|
|
302
|
+
if (activeConflict) {
|
|
303
|
+
claimResult = {
|
|
304
|
+
status: "active-conflict",
|
|
305
|
+
currentRecord: normalizedRecord,
|
|
306
|
+
existingRecord: activeConflict
|
|
307
|
+
};
|
|
308
|
+
return;
|
|
157
309
|
}
|
|
310
|
+
await writeRouteOwnership(normalizedRecord);
|
|
311
|
+
if (overlappingRecords.length > 0) {
|
|
312
|
+
claimResult = {
|
|
313
|
+
status: "reclaimed",
|
|
314
|
+
currentRecord: normalizedRecord,
|
|
315
|
+
previousRecords: overlappingRecords
|
|
316
|
+
};
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
claimResult = {
|
|
320
|
+
status: "claimed",
|
|
321
|
+
currentRecord: normalizedRecord
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
if (!claimResult) {
|
|
325
|
+
throw new Error("Failed to claim route ownership.");
|
|
158
326
|
}
|
|
327
|
+
return claimResult;
|
|
328
|
+
}
|
|
329
|
+
async function touchRouteOwnership(reference) {
|
|
330
|
+
const { lockPath, recordPath } = getRouteOwnershipPaths(reference);
|
|
331
|
+
let touched = false;
|
|
332
|
+
await withFileLock(lockPath, async () => {
|
|
333
|
+
const existingRecord = await readRouteOwnershipByPath(recordPath);
|
|
334
|
+
if (!existingRecord || existingRecord.ownerId !== reference.ownerId) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
await writeRouteOwnership({
|
|
338
|
+
...existingRecord,
|
|
339
|
+
lastSeenAt: Date.now()
|
|
340
|
+
});
|
|
341
|
+
touched = true;
|
|
342
|
+
});
|
|
343
|
+
return touched;
|
|
344
|
+
}
|
|
345
|
+
async function releaseRouteOwnership(reference) {
|
|
346
|
+
const { lockPath, recordPath } = getRouteOwnershipPaths(reference);
|
|
347
|
+
let released = false;
|
|
348
|
+
await withFileLock(lockPath, async () => {
|
|
349
|
+
const existingRecord = await readRouteOwnershipByPath(recordPath);
|
|
350
|
+
if (!existingRecord || existingRecord.ownerId !== reference.ownerId) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
await unlink(recordPath).catch((error) => {
|
|
354
|
+
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
released = true;
|
|
359
|
+
});
|
|
360
|
+
return released;
|
|
159
361
|
}
|
|
160
362
|
function validateCaddyIsInstalled() {
|
|
161
363
|
try {
|
|
@@ -403,33 +605,68 @@ function extractMatchedHosts(route) {
|
|
|
403
605
|
}
|
|
404
606
|
return hosts;
|
|
405
607
|
}
|
|
608
|
+
function extractMatchedSubjects(policy) {
|
|
609
|
+
if (!isRecord(policy) || !Array.isArray(policy.subjects)) return [];
|
|
610
|
+
const subjects = [];
|
|
611
|
+
for (const subject of policy.subjects) {
|
|
612
|
+
if (typeof subject === "string") {
|
|
613
|
+
subjects.push(subject);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return subjects;
|
|
617
|
+
}
|
|
406
618
|
function intersectsDomains(targetDomains, routeDomains) {
|
|
407
619
|
if (targetDomains.length === 0 || routeDomains.length === 0) return false;
|
|
408
620
|
const targetSet = new Set(targetDomains);
|
|
409
621
|
return routeDomains.some((domain) => targetSet.has(domain));
|
|
410
622
|
}
|
|
411
|
-
async function
|
|
412
|
-
if (domains.length === 0) return;
|
|
623
|
+
async function findManagedRoutesForDomains(domains, serverName = DEFAULT_SERVER_NAME, apiUrl, adminOrigin) {
|
|
624
|
+
if (domains.length === 0) return [];
|
|
413
625
|
const res = await caddyFetch(
|
|
414
626
|
`${getApiUrl(apiUrl)}/config/apps/http/servers/${serverName}/routes`,
|
|
415
627
|
void 0,
|
|
416
628
|
apiUrl,
|
|
417
629
|
adminOrigin
|
|
418
630
|
);
|
|
419
|
-
if (!res.ok) return;
|
|
631
|
+
if (!res.ok) return [];
|
|
420
632
|
const text = await res.text();
|
|
421
633
|
const parsed = parseConfig(text);
|
|
422
|
-
if (!Array.isArray(parsed)) return;
|
|
634
|
+
if (!Array.isArray(parsed)) return [];
|
|
635
|
+
const routeIds = [];
|
|
423
636
|
for (const route of parsed) {
|
|
424
637
|
if (!isRecord(route)) continue;
|
|
425
638
|
const id = route["@id"];
|
|
426
639
|
if (typeof id !== "string") continue;
|
|
427
|
-
if (!id.startsWith(
|
|
428
|
-
if (id === currentRouteId) continue;
|
|
640
|
+
if (!id.startsWith(ROUTE_ID_PREFIX)) continue;
|
|
429
641
|
const routeDomains = extractMatchedHosts(route);
|
|
430
642
|
if (!intersectsDomains(domains, routeDomains)) continue;
|
|
431
|
-
|
|
643
|
+
routeIds.push(id);
|
|
432
644
|
}
|
|
645
|
+
return routeIds;
|
|
646
|
+
}
|
|
647
|
+
async function findManagedTlsPoliciesForDomains(domains, apiUrl, adminOrigin) {
|
|
648
|
+
if (domains.length === 0) return [];
|
|
649
|
+
const res = await caddyFetch(
|
|
650
|
+
`${getApiUrl(apiUrl)}/config/apps/tls/automation/policies`,
|
|
651
|
+
void 0,
|
|
652
|
+
apiUrl,
|
|
653
|
+
adminOrigin
|
|
654
|
+
);
|
|
655
|
+
if (!res.ok) return [];
|
|
656
|
+
const text = await res.text();
|
|
657
|
+
const parsed = parseConfig(text);
|
|
658
|
+
if (!Array.isArray(parsed)) return [];
|
|
659
|
+
const policyIds = [];
|
|
660
|
+
for (const policy of parsed) {
|
|
661
|
+
if (!isRecord(policy)) continue;
|
|
662
|
+
const id = policy["@id"];
|
|
663
|
+
if (typeof id !== "string") continue;
|
|
664
|
+
if (!id.startsWith(ROUTE_ID_PREFIX)) continue;
|
|
665
|
+
const policyDomains = extractMatchedSubjects(policy);
|
|
666
|
+
if (!intersectsDomains(domains, policyDomains)) continue;
|
|
667
|
+
policyIds.push(id);
|
|
668
|
+
}
|
|
669
|
+
return policyIds;
|
|
433
670
|
}
|
|
434
671
|
async function addRoute(id, domains, port, cors, serverName = DEFAULT_SERVER_NAME, upstreamHost = "127.0.0.1", upstreamHostHeader, apiUrl, adminOrigin) {
|
|
435
672
|
const handlers = [];
|
|
@@ -718,13 +955,82 @@ function viteCaddyTlsPlugin({
|
|
|
718
955
|
`caddyAdminOrigin is invalid. Falling back to ${pluginCaddyApiUrl}.`
|
|
719
956
|
);
|
|
720
957
|
}
|
|
721
|
-
function
|
|
722
|
-
|
|
723
|
-
|
|
958
|
+
function createOwnerId() {
|
|
959
|
+
return `${process.pid}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
960
|
+
}
|
|
961
|
+
function createRouteOwnershipRecord(ownerId, domains, configRoot) {
|
|
962
|
+
const routeId = `vite-proxy-${ownerId}`;
|
|
963
|
+
const shouldUseInternalTls = internalTls ?? (baseDomain !== void 0 || loopbackDomain !== void 0 || domain !== void 0);
|
|
964
|
+
const now = Date.now();
|
|
965
|
+
return {
|
|
966
|
+
version: 1,
|
|
967
|
+
ownerId,
|
|
968
|
+
pid: process.pid,
|
|
724
969
|
cwd: process.cwd(),
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
970
|
+
configRoot: configRoot ?? null,
|
|
971
|
+
domains: [...domains],
|
|
972
|
+
routeId,
|
|
973
|
+
tlsPolicyId: shouldUseInternalTls ? `${routeId}-tls` : null,
|
|
974
|
+
serverName: serverName ?? "srv0",
|
|
975
|
+
caddyApiUrl: pluginCaddyApiUrl,
|
|
976
|
+
startedAt: now,
|
|
977
|
+
lastSeenAt: now
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function buildOwnershipConflictMessage(domains, existingRecord) {
|
|
981
|
+
const ownerLocation = existingRecord.configRoot ?? existingRecord.cwd;
|
|
982
|
+
const domainLabel = domains.join(", ");
|
|
983
|
+
return [
|
|
984
|
+
`Cannot claim ${domainLabel}: another Vite server already owns ${domains.length > 1 ? "these domains" : "this domain"}.`,
|
|
985
|
+
`Existing owner pid ${existingRecord.pid} from ${ownerLocation}.`,
|
|
986
|
+
"Stop the other server first, or use `instanceLabel` or `domain` to make the hostname unique."
|
|
987
|
+
].join(" ");
|
|
988
|
+
}
|
|
989
|
+
async function releaseOwnershipRecord(record) {
|
|
990
|
+
if (!record) return;
|
|
991
|
+
await releaseRouteOwnership(record);
|
|
992
|
+
}
|
|
993
|
+
async function releaseOwnershipRecords(records) {
|
|
994
|
+
await Promise.all(records.map((record) => releaseOwnershipRecord(record)));
|
|
995
|
+
}
|
|
996
|
+
async function cleanupClaimedResources(record, removeWithRetry) {
|
|
997
|
+
let cleaned = true;
|
|
998
|
+
if (record.tlsPolicyId) {
|
|
999
|
+
const tlsPolicyId = record.tlsPolicyId;
|
|
1000
|
+
cleaned = await removeWithRetry(
|
|
1001
|
+
() => removeTlsPolicy(
|
|
1002
|
+
tlsPolicyId,
|
|
1003
|
+
pluginCaddyApiUrl,
|
|
1004
|
+
pluginCaddyAdminOrigin
|
|
1005
|
+
),
|
|
1006
|
+
"TLS policy"
|
|
1007
|
+
) && cleaned;
|
|
1008
|
+
}
|
|
1009
|
+
cleaned = await removeWithRetry(
|
|
1010
|
+
() => removeRoute(record.routeId, pluginCaddyApiUrl, pluginCaddyAdminOrigin),
|
|
1011
|
+
"route"
|
|
1012
|
+
) && cleaned;
|
|
1013
|
+
return cleaned;
|
|
1014
|
+
}
|
|
1015
|
+
async function cleanupManagedResources(routeIds, tlsPolicyIds, removeWithRetry) {
|
|
1016
|
+
let cleaned = true;
|
|
1017
|
+
for (const managedTlsPolicyId of tlsPolicyIds) {
|
|
1018
|
+
cleaned = await removeWithRetry(
|
|
1019
|
+
() => removeTlsPolicy(
|
|
1020
|
+
managedTlsPolicyId,
|
|
1021
|
+
pluginCaddyApiUrl,
|
|
1022
|
+
pluginCaddyAdminOrigin
|
|
1023
|
+
),
|
|
1024
|
+
`managed TLS policy ${managedTlsPolicyId}`
|
|
1025
|
+
) && cleaned;
|
|
1026
|
+
}
|
|
1027
|
+
for (const managedRouteId of routeIds) {
|
|
1028
|
+
cleaned = await removeWithRetry(
|
|
1029
|
+
() => removeRoute(managedRouteId, pluginCaddyApiUrl, pluginCaddyAdminOrigin),
|
|
1030
|
+
`managed route ${managedRouteId}`
|
|
1031
|
+
) && cleaned;
|
|
1032
|
+
}
|
|
1033
|
+
return cleaned;
|
|
728
1034
|
}
|
|
729
1035
|
function isPreviewServer(server) {
|
|
730
1036
|
return server.config.isProduction;
|
|
@@ -757,13 +1063,16 @@ function viteCaddyTlsPlugin({
|
|
|
757
1063
|
instanceLabel
|
|
758
1064
|
});
|
|
759
1065
|
const domainArray = resolvedDomains ?? [];
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
const
|
|
1066
|
+
const ownerId = createOwnerId();
|
|
1067
|
+
const ownershipRecord = createRouteOwnershipRecord(ownerId, domainArray, config.root);
|
|
1068
|
+
const routeId = ownershipRecord.routeId;
|
|
1069
|
+
const tlsPolicyId = ownershipRecord.tlsPolicyId;
|
|
763
1070
|
let cleanupStarted = false;
|
|
764
1071
|
let resolvedPort = null;
|
|
765
1072
|
let resolvedHost = null;
|
|
766
1073
|
let setupStarted = false;
|
|
1074
|
+
let activeOwnershipRecord = null;
|
|
1075
|
+
let ownershipHeartbeat = null;
|
|
767
1076
|
function buildDomainResolutionMessage() {
|
|
768
1077
|
const issues = [];
|
|
769
1078
|
if (domain !== void 0 && !normalizeDomains(domain)) {
|
|
@@ -869,25 +1178,30 @@ function viteCaddyTlsPlugin({
|
|
|
869
1178
|
async function cleanupRoute() {
|
|
870
1179
|
if (cleanupStarted) return;
|
|
871
1180
|
cleanupStarted = true;
|
|
872
|
-
if (
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1181
|
+
if (ownershipHeartbeat) {
|
|
1182
|
+
clearInterval(ownershipHeartbeat);
|
|
1183
|
+
ownershipHeartbeat = null;
|
|
1184
|
+
}
|
|
1185
|
+
if (!activeOwnershipRecord) return;
|
|
1186
|
+
const cleaned = await cleanupClaimedResources(activeOwnershipRecord, removeWithRetry);
|
|
1187
|
+
if (cleaned) {
|
|
1188
|
+
await releaseOwnershipRecord(activeOwnershipRecord);
|
|
1189
|
+
activeOwnershipRecord = null;
|
|
877
1190
|
}
|
|
878
|
-
await removeWithRetry(
|
|
879
|
-
() => removeRoute(routeId, pluginCaddyApiUrl, pluginCaddyAdminOrigin),
|
|
880
|
-
"route"
|
|
881
|
-
);
|
|
882
1191
|
}
|
|
883
1192
|
function onServerClose() {
|
|
884
1193
|
void cleanupRoute();
|
|
885
1194
|
}
|
|
1195
|
+
function getSignalExitCode(signal) {
|
|
1196
|
+
if (signal === "SIGINT") return 130;
|
|
1197
|
+
if (signal === "SIGTERM") return 143;
|
|
1198
|
+
return 1;
|
|
1199
|
+
}
|
|
886
1200
|
function handleSignal(signal) {
|
|
887
1201
|
process.off("SIGINT", onSigint);
|
|
888
1202
|
process.off("SIGTERM", onSigterm);
|
|
889
1203
|
void cleanupRoute().finally(() => {
|
|
890
|
-
process.
|
|
1204
|
+
process.exit(getSignalExitCode(signal));
|
|
891
1205
|
});
|
|
892
1206
|
}
|
|
893
1207
|
function onSigint() {
|
|
@@ -918,6 +1232,25 @@ function viteCaddyTlsPlugin({
|
|
|
918
1232
|
console.error(`Failed to remove ${label} after ${maxAttempts} attempts.`);
|
|
919
1233
|
return false;
|
|
920
1234
|
}
|
|
1235
|
+
function startOwnershipHeartbeat(record) {
|
|
1236
|
+
ownershipHeartbeat = setInterval(() => {
|
|
1237
|
+
void touchRouteOwnership(record).then((touched) => {
|
|
1238
|
+
if (touched || !ownershipHeartbeat) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
console.error(
|
|
1242
|
+
`Lost route ownership for ${domainArray.join(", ")}. Cleaning up managed Caddy resources.`
|
|
1243
|
+
);
|
|
1244
|
+
void cleanupRoute();
|
|
1245
|
+
}).catch((error) => {
|
|
1246
|
+
console.error(
|
|
1247
|
+
`Failed to refresh route ownership for ${domainArray.join(", ")}.`,
|
|
1248
|
+
error
|
|
1249
|
+
);
|
|
1250
|
+
});
|
|
1251
|
+
}, ROUTE_OWNERSHIP_HEARTBEAT_INTERVAL_MS);
|
|
1252
|
+
ownershipHeartbeat.unref?.();
|
|
1253
|
+
}
|
|
921
1254
|
async function setupRoute() {
|
|
922
1255
|
if (!validateCaddyIsInstalled()) {
|
|
923
1256
|
return;
|
|
@@ -933,16 +1266,70 @@ function viteCaddyTlsPlugin({
|
|
|
933
1266
|
}
|
|
934
1267
|
const port = getServerPort();
|
|
935
1268
|
const upstreamHost = getUpstreamHost();
|
|
936
|
-
|
|
1269
|
+
let claimResult;
|
|
1270
|
+
try {
|
|
1271
|
+
claimResult = await claimRouteOwnership(ownershipRecord);
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
console.error("Failed to claim route ownership for the resolved domains.", e);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (claimResult.status === "active-conflict") {
|
|
1277
|
+
console.error(
|
|
1278
|
+
buildOwnershipConflictMessage(domainArray, claimResult.existingRecord)
|
|
1279
|
+
);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
activeOwnershipRecord = claimResult.currentRecord;
|
|
1283
|
+
if (claimResult.status === "reclaimed") {
|
|
1284
|
+
let reclaimed = true;
|
|
1285
|
+
for (const previousRecord of claimResult.previousRecords) {
|
|
1286
|
+
reclaimed = await cleanupClaimedResources(previousRecord, removeWithRetry) && reclaimed;
|
|
1287
|
+
}
|
|
1288
|
+
if (!reclaimed) {
|
|
1289
|
+
console.error(
|
|
1290
|
+
`Failed to reclaim stale ownership for ${domainArray.join(", ")}. Try stopping the other server or removing stale Caddy state manually.`
|
|
1291
|
+
);
|
|
1292
|
+
await releaseOwnershipRecord(activeOwnershipRecord);
|
|
1293
|
+
activeOwnershipRecord = null;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
await releaseOwnershipRecords(claimResult.previousRecords);
|
|
1297
|
+
}
|
|
1298
|
+
const conflictingRouteIds = (await findManagedRoutesForDomains(
|
|
937
1299
|
domainArray,
|
|
938
|
-
routeId,
|
|
939
1300
|
serverName,
|
|
940
1301
|
pluginCaddyApiUrl,
|
|
941
1302
|
pluginCaddyAdminOrigin
|
|
942
|
-
)
|
|
943
|
-
|
|
1303
|
+
)).filter((existingRouteId) => {
|
|
1304
|
+
return existingRouteId !== routeId;
|
|
1305
|
+
});
|
|
1306
|
+
const conflictingTlsPolicyIds = (await findManagedTlsPoliciesForDomains(
|
|
1307
|
+
domainArray,
|
|
1308
|
+
pluginCaddyApiUrl,
|
|
1309
|
+
pluginCaddyAdminOrigin
|
|
1310
|
+
)).filter((existingTlsPolicyId) => {
|
|
1311
|
+
return existingTlsPolicyId !== tlsPolicyId;
|
|
1312
|
+
});
|
|
1313
|
+
if (conflictingRouteIds.length > 0 || conflictingTlsPolicyIds.length > 0) {
|
|
1314
|
+
const reclaimedOrphans = await cleanupManagedResources(
|
|
1315
|
+
conflictingRouteIds,
|
|
1316
|
+
conflictingTlsPolicyIds,
|
|
1317
|
+
removeWithRetry
|
|
1318
|
+
);
|
|
1319
|
+
if (reclaimedOrphans) {
|
|
1320
|
+
console.warn(
|
|
1321
|
+
`Reclaimed orphaned managed Caddy resources for ${domainArray.join(", ")}.`
|
|
1322
|
+
);
|
|
1323
|
+
} else {
|
|
1324
|
+
console.error(
|
|
1325
|
+
`Cannot claim ${domainArray.join(", ")} because Caddy still has orphaned managed resources. Remove the stale Caddy state or use \`instanceLabel\` or \`domain\` to make the hostname unique.`
|
|
1326
|
+
);
|
|
1327
|
+
await releaseOwnershipRecord(activeOwnershipRecord);
|
|
1328
|
+
activeOwnershipRecord = null;
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
944
1332
|
if (tlsPolicyId) {
|
|
945
|
-
await removeTlsPolicy(tlsPolicyId, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
|
|
946
1333
|
try {
|
|
947
1334
|
await addTlsPolicy(
|
|
948
1335
|
tlsPolicyId,
|
|
@@ -956,6 +1343,8 @@ function viteCaddyTlsPlugin({
|
|
|
956
1343
|
`Failed to add TLS policy to Caddy. Is the Caddy Admin API reachable at ${pluginCaddyApiUrl}?`,
|
|
957
1344
|
e
|
|
958
1345
|
);
|
|
1346
|
+
await releaseOwnershipRecord(activeOwnershipRecord);
|
|
1347
|
+
activeOwnershipRecord = null;
|
|
959
1348
|
return;
|
|
960
1349
|
}
|
|
961
1350
|
}
|
|
@@ -975,12 +1364,17 @@ function viteCaddyTlsPlugin({
|
|
|
975
1364
|
if (tlsPolicyAdded && tlsPolicyId) {
|
|
976
1365
|
await removeTlsPolicy(tlsPolicyId, pluginCaddyApiUrl, pluginCaddyAdminOrigin);
|
|
977
1366
|
}
|
|
1367
|
+
await releaseOwnershipRecord(activeOwnershipRecord);
|
|
1368
|
+
activeOwnershipRecord = null;
|
|
978
1369
|
console.error(
|
|
979
1370
|
`Failed to add route to Caddy. Is the Caddy Admin API reachable at ${pluginCaddyApiUrl}?`,
|
|
980
1371
|
e
|
|
981
1372
|
);
|
|
982
1373
|
return;
|
|
983
1374
|
}
|
|
1375
|
+
if (activeOwnershipRecord) {
|
|
1376
|
+
startOwnershipHeartbeat(activeOwnershipRecord);
|
|
1377
|
+
}
|
|
984
1378
|
console.log("\n\u{1F512} Caddy is proxying your traffic on https");
|
|
985
1379
|
console.log(`
|
|
986
1380
|
\u27A1\uFE0F Upstream target: http://${formatUpstreamTarget(upstreamHost, port)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-caddy-multiple-tls",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Vite plugin that uses Caddy to provide local HTTPS with derived domains.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vite",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/vampaz/vite-plugin-caddy-multiple-tls/#readme",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
50
|
+
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/fs-extra": "^11.0.4",
|
|
54
54
|
"@types/node": "^25.0.3",
|
|
55
55
|
"fs-extra": "^11.3.3",
|
|
56
56
|
"tsup": "^8.5.1",
|
|
57
|
-
"vite": "^
|
|
57
|
+
"vite": "^8.0.0",
|
|
58
58
|
"vitest": "^4.0.16"
|
|
59
59
|
}
|
|
60
60
|
}
|