unplugin-cloudflare-tunnel 0.0.0-alpha-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/LICENSE +21 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/astro.d.ts +25 -0
- package/dist/astro.js +27 -0
- package/dist/esbuild.d.ts +18 -0
- package/dist/esbuild.js +19 -0
- package/dist/farm.d.ts +20 -0
- package/dist/farm.js +21 -0
- package/dist/index-BjNI6nQt.d.ts +157 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/rolldown.d.ts +20 -0
- package/dist/rolldown.js +21 -0
- package/dist/rollup.d.ts +20 -0
- package/dist/rollup.js +21 -0
- package/dist/rspack.d.ts +20 -0
- package/dist/rspack.js +21 -0
- package/dist/src-BC4MyCER.js +729 -0
- package/dist/virtual.d.ts +41 -0
- package/dist/vite.d.ts +20 -0
- package/dist/vite.js +21 -0
- package/dist/webpack.d.ts +20 -0
- package/dist/webpack.js +21 -0
- package/package.json +133 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { bin, install } from "cloudflared";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { exec, spawn } from "node:child_process";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createUnplugin } from "unplugin";
|
|
6
|
+
|
|
7
|
+
//#region src/index.ts
|
|
8
|
+
const INFO_LOG_REGEX = /^.*Z INF .*/;
|
|
9
|
+
const CloudflareErrorSchema = z.object({
|
|
10
|
+
code: z.number(),
|
|
11
|
+
message: z.string()
|
|
12
|
+
});
|
|
13
|
+
const CloudflareApiResponseSchema = z.object({
|
|
14
|
+
success: z.boolean(),
|
|
15
|
+
errors: z.array(CloudflareErrorSchema).optional(),
|
|
16
|
+
messages: z.array(z.string()).optional(),
|
|
17
|
+
result: z.unknown()
|
|
18
|
+
});
|
|
19
|
+
const AccountSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
name: z.string()
|
|
22
|
+
});
|
|
23
|
+
const ZoneSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
name: z.string()
|
|
26
|
+
});
|
|
27
|
+
const TunnelSchema = z.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
name: z.string(),
|
|
30
|
+
account_tag: z.string(),
|
|
31
|
+
created_at: z.string(),
|
|
32
|
+
connections: z.array(z.unknown()).optional()
|
|
33
|
+
});
|
|
34
|
+
const DNSRecordSchema = z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
type: z.string(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
content: z.string(),
|
|
39
|
+
proxied: z.boolean(),
|
|
40
|
+
comment: z.string().nullish()
|
|
41
|
+
});
|
|
42
|
+
const unpluginFactory = (options = {}) => {
|
|
43
|
+
const { enabled = true } = options;
|
|
44
|
+
if (enabled === false) {
|
|
45
|
+
const VIRTUAL_MODULE_ID_STUB = "virtual:unplugin-cloudflare-tunnel";
|
|
46
|
+
return {
|
|
47
|
+
name: "unplugin-cloudflare-tunnel",
|
|
48
|
+
enforce: "pre",
|
|
49
|
+
resolveId(id) {
|
|
50
|
+
if (id === VIRTUAL_MODULE_ID_STUB) return "\0" + VIRTUAL_MODULE_ID_STUB;
|
|
51
|
+
},
|
|
52
|
+
load(id) {
|
|
53
|
+
if (id === "\0" + VIRTUAL_MODULE_ID_STUB) return "export function getTunnelUrl() { return \"\"; }";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const GLOBAL_STATE = Symbol.for("unplugin-cloudflare-tunnel.state");
|
|
58
|
+
const globalState = globalThis[GLOBAL_STATE] ?? {};
|
|
59
|
+
globalThis[GLOBAL_STATE] = globalState;
|
|
60
|
+
let child = globalState.child;
|
|
61
|
+
const VIRTUAL_MODULE_ID = "virtual:unplugin-cloudflare-tunnel";
|
|
62
|
+
const isQuickMode = !("hostname" in options);
|
|
63
|
+
if (isQuickMode) {
|
|
64
|
+
const invalidOptions = [
|
|
65
|
+
"apiToken",
|
|
66
|
+
"accountId",
|
|
67
|
+
"zoneId",
|
|
68
|
+
"tunnelName",
|
|
69
|
+
"dns",
|
|
70
|
+
"ssl",
|
|
71
|
+
"cleanup"
|
|
72
|
+
].filter((opt) => opt in options);
|
|
73
|
+
if (invalidOptions.length > 0) throw new Error(`[unplugin-cloudflare-tunnel] The following options are only supported in named tunnel mode (when hostname is provided): ${invalidOptions.join(", ")}. Either provide a hostname for named tunnel mode, or remove these options for quick tunnel mode.`);
|
|
74
|
+
}
|
|
75
|
+
let providedApiToken;
|
|
76
|
+
let hostname;
|
|
77
|
+
let tunnelName;
|
|
78
|
+
let forcedAccount;
|
|
79
|
+
let forcedZone;
|
|
80
|
+
let dnsOption;
|
|
81
|
+
let sslOption;
|
|
82
|
+
let cleanupConfig;
|
|
83
|
+
if (isQuickMode) {
|
|
84
|
+
tunnelName = "quick-tunnel";
|
|
85
|
+
cleanupConfig = {};
|
|
86
|
+
} else {
|
|
87
|
+
const namedOptions = options;
|
|
88
|
+
providedApiToken = namedOptions.apiToken;
|
|
89
|
+
hostname = namedOptions.hostname;
|
|
90
|
+
forcedAccount = namedOptions.accountId;
|
|
91
|
+
forcedZone = namedOptions.zoneId;
|
|
92
|
+
tunnelName = namedOptions.tunnelName || "dev-tunnel";
|
|
93
|
+
dnsOption = namedOptions.dns;
|
|
94
|
+
sslOption = namedOptions.ssl;
|
|
95
|
+
cleanupConfig = namedOptions.cleanup || {};
|
|
96
|
+
}
|
|
97
|
+
const { port: userProvidedPort, logFile, logLevel, debug = false } = options;
|
|
98
|
+
const debugLog = (...args) => {
|
|
99
|
+
if (debug) console.log("[cloudflare-tunnel:debug]", ...args);
|
|
100
|
+
};
|
|
101
|
+
if (!isQuickMode && (!hostname || typeof hostname !== "string")) throw new Error("[unplugin-cloudflare-tunnel] hostname is required and must be a valid string in named tunnel mode");
|
|
102
|
+
let tunnelUrl = hostname ? `https://${hostname}` : "";
|
|
103
|
+
if (tunnelName && !/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(tunnelName)) throw new Error("[unplugin-cloudflare-tunnel] tunnelName must contain only letters, numbers, and hyphens. It cannot start or end with a hyphen.");
|
|
104
|
+
if (userProvidedPort && (typeof userProvidedPort !== "number" || userProvidedPort < 1 || userProvidedPort > 65535)) throw new Error("[unplugin-cloudflare-tunnel] port must be a valid number between 1 and 65535");
|
|
105
|
+
if (logLevel && ![
|
|
106
|
+
"debug",
|
|
107
|
+
"info",
|
|
108
|
+
"warn",
|
|
109
|
+
"error",
|
|
110
|
+
"fatal"
|
|
111
|
+
].includes(logLevel)) throw new Error("[unplugin-cloudflare-tunnel] logLevel must be one of: debug, info, warn, error, fatal");
|
|
112
|
+
const effectiveLogLevel = logLevel ?? (debug ? "info" : "warn");
|
|
113
|
+
debugLog("Effective cloudflared log level:", effectiveLogLevel);
|
|
114
|
+
if (dnsOption) {
|
|
115
|
+
if (!dnsOption.startsWith("*.") && dnsOption !== hostname) throw new Error("[unplugin-cloudflare-tunnel] dns option must either be a wildcard (e.g., '*.example.com') or exactly match the hostname");
|
|
116
|
+
}
|
|
117
|
+
if (sslOption) {
|
|
118
|
+
if (!sslOption.startsWith("*.") && sslOption !== hostname) throw new Error("[unplugin-cloudflare-tunnel] ssl option must either be a wildcard (e.g., '*.example.com') or exactly match the hostname");
|
|
119
|
+
}
|
|
120
|
+
const trackSslCertificate = (certificateId, hosts, tunnelName$1, timestamp = (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
121
|
+
const trackingKey = `ssl-cert-${certificateId}`;
|
|
122
|
+
globalState[trackingKey] = {
|
|
123
|
+
id: certificateId,
|
|
124
|
+
hosts,
|
|
125
|
+
tunnelName: tunnelName$1,
|
|
126
|
+
timestamp,
|
|
127
|
+
pluginVersion: "1.0.0"
|
|
128
|
+
};
|
|
129
|
+
debugLog(`Tracking SSL certificate: ${certificateId} for hosts: ${hosts.join(", ")}`);
|
|
130
|
+
};
|
|
131
|
+
const findMismatchedSslCertificates = async (apiToken, zoneId, currentTunnelName, currentHostname) => {
|
|
132
|
+
try {
|
|
133
|
+
const certPacks = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, z.any());
|
|
134
|
+
const currentTunnelCerts = (Array.isArray(certPacks) ? certPacks : certPacks.result || []).filter((cert) => {
|
|
135
|
+
return (cert.hostnames || cert.hosts || []).some((host) => host.startsWith(`cf-tunnel-plugin-${currentTunnelName}--`));
|
|
136
|
+
});
|
|
137
|
+
debugLog(`Found ${currentTunnelCerts.length} SSL certificates for current tunnel: ${currentTunnelName}`);
|
|
138
|
+
const mismatchedCerts = currentTunnelCerts.filter((cert) => {
|
|
139
|
+
return !(cert.hostnames || cert.hosts || []).some((host) => {
|
|
140
|
+
if (host.startsWith("cf-tunnel-plugin-")) return false;
|
|
141
|
+
return host === currentHostname || host.startsWith("*.") && currentHostname.endsWith(host.slice(1));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
debugLog(`Found ${mismatchedCerts.length} mismatched SSL certificates`, mismatchedCerts.map((c) => ({
|
|
145
|
+
id: c.id,
|
|
146
|
+
hosts: c.hostnames || c.hosts,
|
|
147
|
+
currentHostname
|
|
148
|
+
})));
|
|
149
|
+
return mismatchedCerts;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ SSL certificate listing failed: ${error.message}`);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const cleanupMismatchedDnsRecords = async (apiToken, zoneId, dnsComment, currentHostname, tunnelId) => {
|
|
156
|
+
try {
|
|
157
|
+
const pluginDnsRecords = await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?comment=${dnsComment}&match=all`, void 0, z.array(DNSRecordSchema));
|
|
158
|
+
debugLog(`Found ${pluginDnsRecords.length} DNS records for current tunnel: ${dnsComment}`);
|
|
159
|
+
const expectedCnameContent = `${tunnelId}.cfargotunnel.com`;
|
|
160
|
+
const mismatchedRecords = pluginDnsRecords.filter((record) => {
|
|
161
|
+
if (record.name === currentHostname && record.content === expectedCnameContent) return false;
|
|
162
|
+
if (dnsOption && record.name === dnsOption && record.content === expectedCnameContent) return false;
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
165
|
+
debugLog(`Found ${mismatchedRecords.length} mismatched DNS records`, mismatchedRecords.map((r) => ({
|
|
166
|
+
name: r.name,
|
|
167
|
+
content: r.content,
|
|
168
|
+
expected: expectedCnameContent,
|
|
169
|
+
comment: r.comment
|
|
170
|
+
})));
|
|
171
|
+
const deletedRecords = [];
|
|
172
|
+
if (mismatchedRecords.length > 0) {
|
|
173
|
+
console.log(`[unplugin-cloudflare-tunnel] 🧹 Cleaning up ${mismatchedRecords.length} mismatched DNS records from tunnel '${dnsComment}'...`);
|
|
174
|
+
for (const record of mismatchedRecords) try {
|
|
175
|
+
await cf(apiToken, "DELETE", `/zones/${zoneId}/dns_records/${record.id}`);
|
|
176
|
+
deletedRecords.push(record);
|
|
177
|
+
console.log(`[unplugin-cloudflare-tunnel] ✅ Deleted mismatched DNS record: ${record.name} → ${record.content}`);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to delete DNS record ${record.name}: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
found: mismatchedRecords,
|
|
184
|
+
deleted: deletedRecords
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ DNS cleanup failed: ${error.message}`);
|
|
188
|
+
return {
|
|
189
|
+
found: [],
|
|
190
|
+
deleted: []
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const cf = async (apiToken, method, url, body, resultSchema) => {
|
|
195
|
+
try {
|
|
196
|
+
debugLog("→ CF API", method, url, body ? { body } : "");
|
|
197
|
+
const response = await fetch(`https://api.cloudflare.com/client/v4${url}`, {
|
|
198
|
+
method,
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${apiToken}`,
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
"User-Agent": "unplugin-cloudflare-tunnel/1.0.0"
|
|
203
|
+
},
|
|
204
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
208
|
+
throw new Error(`[unplugin-cloudflare-tunnel] API request failed: ${response.status} ${response.statusText}. Response: ${errorText}`);
|
|
209
|
+
}
|
|
210
|
+
const rawData = await response.json();
|
|
211
|
+
debugLog("← CF API response", rawData);
|
|
212
|
+
const apiResponse = CloudflareApiResponseSchema.parse(rawData);
|
|
213
|
+
if (!apiResponse.success) {
|
|
214
|
+
const errorMsg = apiResponse.errors?.map((e) => e.message || `Error ${e.code}`).join(", ") || "Unknown API error";
|
|
215
|
+
throw new Error(`[unplugin-cloudflare-tunnel] Cloudflare API error: ${errorMsg}`);
|
|
216
|
+
}
|
|
217
|
+
if (resultSchema) {
|
|
218
|
+
const parsed = resultSchema.parse(apiResponse.result);
|
|
219
|
+
debugLog("← Parsed result", parsed);
|
|
220
|
+
return parsed;
|
|
221
|
+
}
|
|
222
|
+
debugLog("← Result (untyped)", apiResponse.result);
|
|
223
|
+
return apiResponse.result;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof Error) {
|
|
226
|
+
if (error.message.includes("[unplugin-cloudflare-tunnel]")) throw error;
|
|
227
|
+
throw new Error(`[unplugin-cloudflare-tunnel] API request failed: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
throw new Error("[unplugin-cloudflare-tunnel] Unknown API error occurred");
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const retryWithBackoff = async (fn, maxRetries = 5, initialDelayMs = 1e3) => {
|
|
233
|
+
let attempt = 0;
|
|
234
|
+
while (true) try {
|
|
235
|
+
return await fn();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
attempt += 1;
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
if (attempt > maxRetries) {
|
|
240
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Edge certificate request failed after ${maxRetries} retries: ${message}`);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const delay = initialDelayMs * 2 ** (attempt - 1);
|
|
244
|
+
console.error(`[unplugin-cloudflare-tunnel] ⚠️ Edge certificate request failed (attempt ${attempt}/${maxRetries}): ${message}`);
|
|
245
|
+
console.error(`[unplugin-cloudflare-tunnel] ⏳ Retrying in ${delay}ms...`);
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const spawnQuickTunnel = async (localTarget) => {
|
|
250
|
+
const cloudflaredArgs = ["tunnel"];
|
|
251
|
+
cloudflaredArgs.push("--loglevel", "info");
|
|
252
|
+
if (logFile) cloudflaredArgs.push("--logfile", logFile);
|
|
253
|
+
cloudflaredArgs.push("--url", localTarget);
|
|
254
|
+
debugLog("Spawning quick tunnel:", bin, cloudflaredArgs);
|
|
255
|
+
const child$1 = spawn(bin, cloudflaredArgs, {
|
|
256
|
+
stdio: [
|
|
257
|
+
"ignore",
|
|
258
|
+
"pipe",
|
|
259
|
+
"pipe"
|
|
260
|
+
],
|
|
261
|
+
detached: false,
|
|
262
|
+
windowsHide: true,
|
|
263
|
+
shell: process.platform === "win32"
|
|
264
|
+
});
|
|
265
|
+
console.log(`[unplugin-cloudflare-tunnel] Quick tunnel process spawned with PID: ${child$1.pid}`);
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
let urlFound = false;
|
|
268
|
+
const timeout = setTimeout(() => {
|
|
269
|
+
if (!urlFound) reject(/* @__PURE__ */ new Error("Quick tunnel URL not found in output within 30 seconds"));
|
|
270
|
+
}, 3e4);
|
|
271
|
+
child$1.stdout?.on("data", (data) => {
|
|
272
|
+
const output = data.toString();
|
|
273
|
+
if (!globalState.shuttingDown || debug) {
|
|
274
|
+
if (effectiveLogLevel === "debug" || effectiveLogLevel === "info") console.log(`[cloudflared stdout] ${output.trim()}`);
|
|
275
|
+
else for (const line of output.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.log(`[cloudflared stdout] ${line.trim()}`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
child$1.stderr?.on("data", (data) => {
|
|
279
|
+
const error = data.toString().trim();
|
|
280
|
+
const urlMatch = error.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
281
|
+
if (urlMatch && !urlFound) {
|
|
282
|
+
urlFound = true;
|
|
283
|
+
clearTimeout(timeout);
|
|
284
|
+
resolve({
|
|
285
|
+
child: child$1,
|
|
286
|
+
url: urlMatch[0]
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (error.includes("Failed to parse ICMP reply") || error.includes("unknow ip version 0")) {
|
|
290
|
+
if (logLevel === "debug") console.log(`[cloudflared debug] ${error}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!globalState.shuttingDown || debug) {
|
|
294
|
+
if (effectiveLogLevel === "debug" || effectiveLogLevel === "info") console.error(`[cloudflared stderr] ${error}`);
|
|
295
|
+
else for (const line of error.split("\n")) if (!INFO_LOG_REGEX.test(line)) console.error(`[cloudflared stderr] ${line.trim()}`);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
child$1.on("error", (error) => {
|
|
299
|
+
clearTimeout(timeout);
|
|
300
|
+
reject(/* @__PURE__ */ new Error(`Failed to start quick tunnel process: ${error.message}`));
|
|
301
|
+
});
|
|
302
|
+
child$1.on("exit", (code, signal) => {
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
if (!urlFound) reject(/* @__PURE__ */ new Error(`Quick tunnel process exited before URL was found (code: ${code}, signal: ${signal})`));
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
const killCloudflared = (signal = "SIGTERM") => {
|
|
309
|
+
if (!child || child.killed) return;
|
|
310
|
+
globalState.shuttingDown = true;
|
|
311
|
+
globalState.tunnelUrl = void 0;
|
|
312
|
+
try {
|
|
313
|
+
console.log(`[unplugin-cloudflare-tunnel] 🛑 Terminating cloudflared process (PID: ${child.pid}) with ${signal}...`);
|
|
314
|
+
if (!child.kill(signal) && process.platform === "win32") exec(`taskkill /pid ${child.pid} /T /F`, () => {});
|
|
315
|
+
if (signal === "SIGTERM") setTimeout(() => {
|
|
316
|
+
if (child && !child.killed) {
|
|
317
|
+
console.log("[unplugin-cloudflare-tunnel] 🛑 Force killing cloudflared process...");
|
|
318
|
+
if (process.platform === "win32") exec(`taskkill /pid ${child.pid} /T /F`, () => {});
|
|
319
|
+
else child.kill("SIGKILL");
|
|
320
|
+
}
|
|
321
|
+
}, 2e3);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.log(`[unplugin-cloudflare-tunnel] Note: Error killing cloudflared: ${error}`);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
let exitHandlersRegistered = globalState.exitHandlersRegistered ?? false;
|
|
327
|
+
const registerExitHandler = () => {
|
|
328
|
+
if (exitHandlersRegistered) return;
|
|
329
|
+
exitHandlersRegistered = true;
|
|
330
|
+
globalState.exitHandlersRegistered = true;
|
|
331
|
+
const cleanup = () => killCloudflared("SIGTERM");
|
|
332
|
+
process.once("exit", cleanup);
|
|
333
|
+
process.once("beforeExit", cleanup);
|
|
334
|
+
[
|
|
335
|
+
"SIGINT",
|
|
336
|
+
"SIGTERM",
|
|
337
|
+
"SIGQUIT",
|
|
338
|
+
"SIGHUP"
|
|
339
|
+
].forEach((signal) => {
|
|
340
|
+
process.once(signal, () => {
|
|
341
|
+
killCloudflared(signal);
|
|
342
|
+
try {
|
|
343
|
+
process.kill(process.pid, signal);
|
|
344
|
+
} catch {
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
process.once("uncaughtException", (error) => {
|
|
350
|
+
console.error("[unplugin-cloudflare-tunnel] Uncaught exception, cleaning up cloudflared...", error);
|
|
351
|
+
killCloudflared("SIGTERM");
|
|
352
|
+
});
|
|
353
|
+
process.once("unhandledRejection", (reason) => {
|
|
354
|
+
console.error("[unplugin-cloudflare-tunnel] Unhandled rejection, cleaning up cloudflared...", reason);
|
|
355
|
+
killCloudflared("SIGTERM");
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
const configureServer = async (server) => {
|
|
359
|
+
const generateDnsComment = () => {
|
|
360
|
+
return `unplugin-cloudflare-tunnel:${tunnelName}`;
|
|
361
|
+
};
|
|
362
|
+
try {
|
|
363
|
+
const { host: serverHost, port: detectedPort } = normalizeAddress(server.httpServer?.address());
|
|
364
|
+
const port = userProvidedPort || detectedPort || server.config.server.port || 5173;
|
|
365
|
+
const newConfigHash = JSON.stringify({
|
|
366
|
+
isQuickMode,
|
|
367
|
+
hostname,
|
|
368
|
+
port,
|
|
369
|
+
tunnelName,
|
|
370
|
+
dnsOption,
|
|
371
|
+
sslOption
|
|
372
|
+
});
|
|
373
|
+
if (globalState.child && !globalState.child.killed && globalState.configHash === newConfigHash) {
|
|
374
|
+
tunnelUrl = await globalState.tunnelUrl ?? "";
|
|
375
|
+
console.log("[unplugin-cloudflare-tunnel] Config unchanged – re-using existing tunnel");
|
|
376
|
+
globalState.shuttingDown = false;
|
|
377
|
+
registerExitHandler();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (globalState.child && !globalState.child.killed) {
|
|
381
|
+
console.log("[unplugin-cloudflare-tunnel] Config changed – terminating previous tunnel...");
|
|
382
|
+
try {
|
|
383
|
+
globalState.child.kill("SIGTERM");
|
|
384
|
+
} catch (_) {}
|
|
385
|
+
}
|
|
386
|
+
delete globalState.child;
|
|
387
|
+
delete globalState.configHash;
|
|
388
|
+
globalState.shuttingDown = false;
|
|
389
|
+
if (isQuickMode) {
|
|
390
|
+
console.log("[unplugin-cloudflare-tunnel] Starting quick tunnel mode...");
|
|
391
|
+
debugLog("Quick tunnel mode - no API token or hostname required");
|
|
392
|
+
await ensureCloudflaredBinary(bin);
|
|
393
|
+
const localTarget$1 = getLocalTarget(serverHost, port);
|
|
394
|
+
debugLog("← Quick tunnel connecting to local target", localTarget$1);
|
|
395
|
+
try {
|
|
396
|
+
const { child: quickChild, url } = await spawnQuickTunnel(localTarget$1);
|
|
397
|
+
tunnelUrl = url;
|
|
398
|
+
child = quickChild;
|
|
399
|
+
globalState.child = child;
|
|
400
|
+
globalState.configHash = newConfigHash;
|
|
401
|
+
registerExitHandler();
|
|
402
|
+
console.log(`🌐 Quick tunnel ready at: ${url}`);
|
|
403
|
+
server.httpServer?.on("listening", async () => {
|
|
404
|
+
try {
|
|
405
|
+
const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
|
|
406
|
+
if (actualPort !== port) {
|
|
407
|
+
console.log(`[unplugin-cloudflare-tunnel] ⚠️ Port conflict detected - server is using port ${actualPort} instead of ${port}`);
|
|
408
|
+
console.log(`[unplugin-cloudflare-tunnel] 🔄 Quick tunnel needs to be restarted for new port...`);
|
|
409
|
+
killCloudflared("SIGTERM");
|
|
410
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
411
|
+
const { child: newChild, url: newUrl } = await spawnQuickTunnel(getLocalTarget(actualServerHost, actualPort ?? port));
|
|
412
|
+
tunnelUrl = newUrl;
|
|
413
|
+
child = newChild;
|
|
414
|
+
globalState.child = child;
|
|
415
|
+
console.log(`🌐 Quick tunnel updated for port ${actualPort}: ${newUrl}`);
|
|
416
|
+
globalState.configHash = JSON.stringify({
|
|
417
|
+
isQuickMode,
|
|
418
|
+
hostname,
|
|
419
|
+
port: actualPort,
|
|
420
|
+
tunnelName,
|
|
421
|
+
dnsOption,
|
|
422
|
+
sslOption
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to update quick tunnel for port change: ${error.message}`);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
server.httpServer?.once("close", () => {
|
|
430
|
+
killCloudflared("SIGTERM");
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Quick tunnel setup failed: ${error.message}`);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
console.log("[unplugin-cloudflare-tunnel] Starting named tunnel mode...");
|
|
439
|
+
const apiToken = providedApiToken || process.env.CLOUDFLARE_API_KEY;
|
|
440
|
+
if (!apiToken) throw new Error("[unplugin-cloudflare-tunnel] API token is required. Provide it via 'apiToken' option or set the CLOUDFLARE_API_KEY environment variable. Get your token at: https://dash.cloudflare.com/profile/api-tokens");
|
|
441
|
+
console.log(`[unplugin-cloudflare-tunnel] Using port ${port}${userProvidedPort === port ? " (user-provided)" : " (from bundler config)"}`);
|
|
442
|
+
await ensureCloudflaredBinary(bin);
|
|
443
|
+
const accounts = await cf(apiToken, "GET", "/accounts", void 0, z.array(AccountSchema));
|
|
444
|
+
const accountId = forcedAccount || accounts[0]?.id;
|
|
445
|
+
if (!accountId) throw new Error("Unable to determine Cloudflare account ID");
|
|
446
|
+
const apexDomain = hostname.split(".").slice(-2).join(".");
|
|
447
|
+
const parentDomain = hostname.split(".").slice(1).join(".");
|
|
448
|
+
debugLog("← Apex domain", apexDomain);
|
|
449
|
+
debugLog("← Parent domain", parentDomain);
|
|
450
|
+
let zoneId = forcedZone;
|
|
451
|
+
if (!zoneId) {
|
|
452
|
+
let zones = [];
|
|
453
|
+
try {
|
|
454
|
+
zones = await cf(apiToken, "GET", `/zones?name=${parentDomain}`, void 0, z.array(ZoneSchema));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
debugLog("← Error fetching zone for parent domain", error);
|
|
457
|
+
}
|
|
458
|
+
if (zones.length === 0) zones = await cf(apiToken, "GET", `/zones?name=${apexDomain}`, void 0, z.array(ZoneSchema));
|
|
459
|
+
zoneId = zones[0]?.id;
|
|
460
|
+
}
|
|
461
|
+
if (!zoneId) throw new Error(`Zone ${apexDomain} not found in account ${accountId}`);
|
|
462
|
+
const { autoCleanup = true } = cleanupConfig;
|
|
463
|
+
let tunnel = (await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel?name=${tunnelName}`, void 0, z.array(TunnelSchema)))[0];
|
|
464
|
+
if (!tunnel) {
|
|
465
|
+
console.log(`[unplugin-cloudflare-tunnel] Creating tunnel '${tunnelName}'...`);
|
|
466
|
+
tunnel = await cf(apiToken, "POST", `/accounts/${accountId}/cfd_tunnel`, {
|
|
467
|
+
name: tunnelName,
|
|
468
|
+
config_src: "cloudflare"
|
|
469
|
+
}, TunnelSchema);
|
|
470
|
+
}
|
|
471
|
+
const tunnelId = tunnel.id;
|
|
472
|
+
if (autoCleanup) {
|
|
473
|
+
console.log(`[unplugin-cloudflare-tunnel] 🧹 Running resource cleanup for tunnel '${tunnelName}'...`);
|
|
474
|
+
const dnsCleanup = await cleanupMismatchedDnsRecords(apiToken, zoneId, generateDnsComment(), hostname, tunnelId);
|
|
475
|
+
if (dnsCleanup.found.length > 0) console.log(`[unplugin-cloudflare-tunnel] 📊 DNS cleanup: ${dnsCleanup.found.length} mismatched, ${dnsCleanup.deleted.length} deleted`);
|
|
476
|
+
const mismatchedSslCerts = await findMismatchedSslCertificates(apiToken, zoneId, tunnelName, hostname);
|
|
477
|
+
if (mismatchedSslCerts.length > 0) {
|
|
478
|
+
for (const cert of mismatchedSslCerts) await cf(apiToken, "DELETE", `/zones/${zoneId}/ssl/certificate_packs/${cert.id}`);
|
|
479
|
+
console.log(`[unplugin-cloudflare-tunnel] 📊 SSL cleanup: ${mismatchedSslCerts.length} deleted`);
|
|
480
|
+
}
|
|
481
|
+
} else debugLog("← Cleanup skipped", cleanupConfig);
|
|
482
|
+
const localTarget = getLocalTarget(serverHost, port);
|
|
483
|
+
debugLog("← Connecting to local target", localTarget);
|
|
484
|
+
await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
|
|
485
|
+
hostname,
|
|
486
|
+
service: localTarget
|
|
487
|
+
}, { service: "http_status:404" }] } });
|
|
488
|
+
const generateSslTagHostname = () => {
|
|
489
|
+
return `cf-tunnel-plugin-${tunnelName}--${parentDomain}`;
|
|
490
|
+
};
|
|
491
|
+
if (dnsOption) {
|
|
492
|
+
const ensureDnsRecord = async (type, content) => {
|
|
493
|
+
if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(dnsOption)}`, void 0, z.array(DNSRecordSchema))).length === 0) {
|
|
494
|
+
console.log(`[unplugin-cloudflare-tunnel] Creating ${type} record for ${dnsOption}...`);
|
|
495
|
+
await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
|
|
496
|
+
type,
|
|
497
|
+
name: dnsOption,
|
|
498
|
+
content,
|
|
499
|
+
proxied: true,
|
|
500
|
+
comment: generateDnsComment()
|
|
501
|
+
}, DNSRecordSchema);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
await ensureDnsRecord("CNAME", `${tunnelId}.cfargotunnel.com`);
|
|
505
|
+
} else {
|
|
506
|
+
const wildcardDns = `*.${parentDomain}`;
|
|
507
|
+
if ((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${wildcardDns}`, void 0, z.array(DNSRecordSchema))).length === 0) {
|
|
508
|
+
if (!((await cf(apiToken, "GET", `/zones/${zoneId}/dns_records?type=CNAME&name=${hostname}`, void 0, z.array(DNSRecordSchema))).length > 0)) {
|
|
509
|
+
console.log(`[unplugin-cloudflare-tunnel] Creating DNS record for ${hostname}...`);
|
|
510
|
+
await cf(apiToken, "POST", `/zones/${zoneId}/dns_records`, {
|
|
511
|
+
type: "CNAME",
|
|
512
|
+
name: hostname,
|
|
513
|
+
content: `${tunnelId}.cfargotunnel.com`,
|
|
514
|
+
proxied: true,
|
|
515
|
+
comment: generateDnsComment()
|
|
516
|
+
}, DNSRecordSchema);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const token = await cf(apiToken, "GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`, void 0, z.string());
|
|
521
|
+
try {
|
|
522
|
+
const certListRaw = await cf(apiToken, "GET", `/zones/${zoneId}/ssl/certificate_packs?status=all`, void 0, z.any());
|
|
523
|
+
const certPacks = Array.isArray(certListRaw) ? certListRaw : certListRaw.result || [];
|
|
524
|
+
const certContainingHost = (host) => certPacks.filter((c) => (c.hostnames || c.hosts || []).includes(host))?.[0];
|
|
525
|
+
if (sslOption) {
|
|
526
|
+
const isWildcard = sslOption.startsWith("*.");
|
|
527
|
+
const certNeededHost = sslOption;
|
|
528
|
+
const matchingCert = certContainingHost(certNeededHost);
|
|
529
|
+
if (!matchingCert) {
|
|
530
|
+
console.log(`[unplugin-cloudflare-tunnel] Requesting ${isWildcard ? "wildcard " : ""}certificate for ${certNeededHost}...`);
|
|
531
|
+
const tagHostname = generateSslTagHostname();
|
|
532
|
+
const certificateHosts = [certNeededHost, tagHostname];
|
|
533
|
+
debugLog(`Adding tag hostname to certificate: ${tagHostname}`);
|
|
534
|
+
const newCert = await retryWithBackoff(() => cf(apiToken, "POST", `/zones/${zoneId}/ssl/certificate_packs/order`, {
|
|
535
|
+
hosts: certificateHosts,
|
|
536
|
+
certificate_authority: "lets_encrypt",
|
|
537
|
+
type: "advanced",
|
|
538
|
+
validation_method: isWildcard ? "txt" : "http",
|
|
539
|
+
validity_days: 90,
|
|
540
|
+
cloudflare_branding: false
|
|
541
|
+
}));
|
|
542
|
+
if (newCert?.id) trackSslCertificate(newCert.id, certificateHosts, tunnelName);
|
|
543
|
+
} else debugLog("← Edge certificate already exists", matchingCert);
|
|
544
|
+
} else {
|
|
545
|
+
const wildcardDomain = `*.${parentDomain}`;
|
|
546
|
+
const wildcardExists = certContainingHost(wildcardDomain);
|
|
547
|
+
if (!wildcardExists) {
|
|
548
|
+
const totalTls = await cf(apiToken, "GET", `/zones/${zoneId}/acm/total_tls`, void 0, z.object({ status: z.string() }));
|
|
549
|
+
debugLog("← Total TLS", totalTls);
|
|
550
|
+
const existingHostnameCert = certContainingHost(hostname);
|
|
551
|
+
if (totalTls.status !== "on" && !existingHostnameCert) {
|
|
552
|
+
console.log(`[unplugin-cloudflare-tunnel] Requesting edge certificate for ${hostname}...`);
|
|
553
|
+
const tagHostname = generateSslTagHostname();
|
|
554
|
+
const certificateHosts = [hostname, tagHostname];
|
|
555
|
+
debugLog(`Adding tag hostname to certificate: ${tagHostname}`);
|
|
556
|
+
const newCert = await retryWithBackoff(() => cf(apiToken, "POST", `/zones/${zoneId}/ssl/certificate_packs/order`, {
|
|
557
|
+
hosts: certificateHosts,
|
|
558
|
+
certificate_authority: "lets_encrypt",
|
|
559
|
+
type: "advanced",
|
|
560
|
+
validation_method: "txt",
|
|
561
|
+
validity_days: 90,
|
|
562
|
+
cloudflare_branding: false
|
|
563
|
+
}));
|
|
564
|
+
if (newCert?.id) trackSslCertificate(newCert.id, certificateHosts, tunnelName);
|
|
565
|
+
} else debugLog("← Edge certificate already exists", existingHostnameCert);
|
|
566
|
+
} else debugLog("← Edge certificate (wildcard) already exists", wildcardExists, wildcardDomain);
|
|
567
|
+
}
|
|
568
|
+
} catch (sslError) {
|
|
569
|
+
console.error(`[unplugin-cloudflare-tunnel] ⚠️ SSL management error: ${sslError.message}`);
|
|
570
|
+
throw sslError;
|
|
571
|
+
}
|
|
572
|
+
const cloudflaredArgs = ["tunnel"];
|
|
573
|
+
cloudflaredArgs.push("--loglevel", effectiveLogLevel);
|
|
574
|
+
if (logFile) cloudflaredArgs.push("--logfile", logFile);
|
|
575
|
+
debugLog("Spawning cloudflared", bin, cloudflaredArgs);
|
|
576
|
+
cloudflaredArgs.push("run", "--token", token);
|
|
577
|
+
child = spawn(bin, cloudflaredArgs, {
|
|
578
|
+
stdio: [
|
|
579
|
+
"ignore",
|
|
580
|
+
"pipe",
|
|
581
|
+
"pipe"
|
|
582
|
+
],
|
|
583
|
+
detached: false,
|
|
584
|
+
windowsHide: true,
|
|
585
|
+
shell: process.platform === "win32"
|
|
586
|
+
});
|
|
587
|
+
console.log(`[unplugin-cloudflare-tunnel] Process spawned with PID: ${child.pid}`);
|
|
588
|
+
globalState.child = child;
|
|
589
|
+
globalState.configHash = newConfigHash;
|
|
590
|
+
registerExitHandler();
|
|
591
|
+
let tunnelReady = false;
|
|
592
|
+
child.stdout?.on("data", (data) => {
|
|
593
|
+
const output = data.toString();
|
|
594
|
+
if (!globalState.shuttingDown || debug) console.log(`[cloudflared stdout] ${output.trim()}`);
|
|
595
|
+
if (output.includes("Connection") && output.includes("registered")) {
|
|
596
|
+
if (!tunnelReady) {
|
|
597
|
+
tunnelReady = true;
|
|
598
|
+
console.log(`🌐 Cloudflare tunnel started for https://${hostname}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
child.stderr?.on("data", (data) => {
|
|
603
|
+
const error = data.toString().trim();
|
|
604
|
+
if (error.includes("Failed to parse ICMP reply") || error.includes("unknow ip version 0")) {
|
|
605
|
+
if (logLevel === "debug") console.log(`[cloudflared debug] ${error}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!globalState.shuttingDown || debug) console.error(`[cloudflared stderr] ${error}`);
|
|
609
|
+
if (error.toLowerCase().includes("error") || error.toLowerCase().includes("failed") || error.toLowerCase().includes("fatal")) {
|
|
610
|
+
if (!globalState.shuttingDown || debug) console.error(`[unplugin-cloudflare-tunnel] ⚠️ ${error}`);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
child.on("error", (error) => {
|
|
614
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to start tunnel process: ${error.message}`);
|
|
615
|
+
if (error.message.includes("ENOENT")) console.error(`[unplugin-cloudflare-tunnel] Hint: cloudflared binary may not be installed correctly`);
|
|
616
|
+
});
|
|
617
|
+
child.on("exit", (code, signal) => {
|
|
618
|
+
if (code !== 0 && code !== null) {
|
|
619
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Tunnel process exited with code ${code}`);
|
|
620
|
+
if (signal) console.error(`[unplugin-cloudflare-tunnel] Process terminated by signal: ${signal}`);
|
|
621
|
+
} else if (code === 0) console.log(`[unplugin-cloudflare-tunnel] ✅ Tunnel process exited cleanly`);
|
|
622
|
+
});
|
|
623
|
+
setTimeout(() => {
|
|
624
|
+
if (!tunnelReady) console.log(`🌐 Cloudflare tunnel starting for https://${hostname}`);
|
|
625
|
+
}, 3e3);
|
|
626
|
+
server.httpServer?.once("close", () => {
|
|
627
|
+
killCloudflared("SIGTERM");
|
|
628
|
+
});
|
|
629
|
+
server.httpServer?.on("listening", async () => {
|
|
630
|
+
try {
|
|
631
|
+
const { host: actualServerHost, port: actualPort } = normalizeAddress(server.httpServer?.address());
|
|
632
|
+
if (actualPort !== port) {
|
|
633
|
+
console.log(`[unplugin-cloudflare-tunnel] ⚠️ Port conflict detected - server is using port ${actualPort} instead of ${port}`);
|
|
634
|
+
console.log(`[unplugin-cloudflare-tunnel] 🔄 Updating tunnel configuration...`);
|
|
635
|
+
const newLocalTarget = getLocalTarget(actualServerHost, actualPort ?? port);
|
|
636
|
+
debugLog("← Updating local target to", newLocalTarget);
|
|
637
|
+
await cf(apiToken, "PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, { config: { ingress: [{
|
|
638
|
+
hostname,
|
|
639
|
+
service: newLocalTarget
|
|
640
|
+
}, { service: "http_status:404" }] } });
|
|
641
|
+
console.log(`[unplugin-cloudflare-tunnel] ✅ Tunnel configuration updated to use port ${actualPort}`);
|
|
642
|
+
globalState.configHash = JSON.stringify({
|
|
643
|
+
hostname,
|
|
644
|
+
port: actualPort,
|
|
645
|
+
tunnelName,
|
|
646
|
+
dnsOption,
|
|
647
|
+
sslOption
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Failed to update tunnel for port change: ${error.message}`);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error(`[unplugin-cloudflare-tunnel] ❌ Setup failed: ${error.message}`);
|
|
656
|
+
if (error.message.includes("API token")) {
|
|
657
|
+
console.error(`[unplugin-cloudflare-tunnel] 💡 Check your API token at: https://dash.cloudflare.com/profile/api-tokens`);
|
|
658
|
+
console.error(`[unplugin-cloudflare-tunnel] 💡 Required permissions: Zone:Zone:Read, Zone:DNS:Edit, Account:Cloudflare Tunnel:Edit`);
|
|
659
|
+
} else if (error.message.includes("Zone") && error.message.includes("not found")) console.error(`[unplugin-cloudflare-tunnel] 💡 Make sure '${hostname}' domain is added to your Cloudflare account`);
|
|
660
|
+
else if (error.message.includes("cloudflared")) console.error(`[unplugin-cloudflare-tunnel] 💡 Try deleting node_modules and reinstalling to get a fresh cloudflared binary`);
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
return {
|
|
665
|
+
name: "unplugin-cloudflare-tunnel",
|
|
666
|
+
enforce: "pre",
|
|
667
|
+
resolveId(id) {
|
|
668
|
+
if (id === VIRTUAL_MODULE_ID) return "\0" + VIRTUAL_MODULE_ID;
|
|
669
|
+
},
|
|
670
|
+
async load(id) {
|
|
671
|
+
const url = await globalState.tunnelUrl;
|
|
672
|
+
if (id === "\0" + VIRTUAL_MODULE_ID) return `export function getTunnelUrl() { return ${JSON.stringify(url || "")}; }`;
|
|
673
|
+
},
|
|
674
|
+
vite: {
|
|
675
|
+
config(config) {
|
|
676
|
+
if (!config.server) config.server = {};
|
|
677
|
+
if (isQuickMode) {
|
|
678
|
+
config.server.allowedHosts = [".trycloudflare.com"];
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!config.server.allowedHosts) {
|
|
682
|
+
config.server.allowedHosts = [hostname];
|
|
683
|
+
console.log(`[unplugin-cloudflare-tunnel] Configured Vite to allow requests from ${hostname}`);
|
|
684
|
+
} else if (Array.isArray(config.server.allowedHosts)) {
|
|
685
|
+
if (!config.server.allowedHosts.includes(hostname)) {
|
|
686
|
+
config.server.allowedHosts.push(hostname);
|
|
687
|
+
console.log(`[unplugin-cloudflare-tunnel] Added ${hostname} to allowed hosts`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
configureServer(server) {
|
|
692
|
+
const configuredPromise = configureServer(server);
|
|
693
|
+
globalState.tunnelUrl = configuredPromise.then(() => tunnelUrl).catch(() => "");
|
|
694
|
+
return async () => {
|
|
695
|
+
await configuredPromise;
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
closeBundle() {
|
|
700
|
+
killCloudflared("SIGTERM");
|
|
701
|
+
delete globalState.child;
|
|
702
|
+
delete globalState.configHash;
|
|
703
|
+
delete globalState.shuttingDown;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
function normalizeAddress(address) {
|
|
708
|
+
if (address && typeof address === "object") return {
|
|
709
|
+
host: "address" in address && address.address ? address.address : "localhost",
|
|
710
|
+
port: "port" in address && typeof address.port === "number" ? address.port : void 0
|
|
711
|
+
};
|
|
712
|
+
return { host: "localhost" };
|
|
713
|
+
}
|
|
714
|
+
async function ensureCloudflaredBinary(binPath) {
|
|
715
|
+
try {
|
|
716
|
+
await fs.access(binPath);
|
|
717
|
+
} catch {
|
|
718
|
+
console.log("[unplugin-cloudflare-tunnel] Installing cloudflared binary...");
|
|
719
|
+
await install(binPath);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function getLocalTarget(host, port) {
|
|
723
|
+
return `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
724
|
+
}
|
|
725
|
+
const CloudflareTunnel = createUnplugin(unpluginFactory);
|
|
726
|
+
var src_default = CloudflareTunnel;
|
|
727
|
+
|
|
728
|
+
//#endregion
|
|
729
|
+
export { src_default as n, CloudflareTunnel as t };
|