@swimmingliu/autovpn 1.6.4 → 1.6.6
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 +1 -1
- package/dist/doctor/checks.js +17 -2
- package/dist/pipeline/availability.js +106 -1
- package/dist/pipeline/deploy.js +27 -50
- package/dist/pipeline/orchestrator.js +6 -3
- package/dist/runtime/templates.js +37 -0
- package/dist/templates/share-worker/vpn.js +4813 -0
- package/dist/templates/vmess_node.js +27 -0
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Current Node backend notes:
|
|
|
72
72
|
- Add `--skip-deploy --skip-verify` when you want an offline Node pipeline check.
|
|
73
73
|
- Plain Node foreground deploy/verify runs use Node for Wrangler deploy, primary blocked-project fallback, share-project sync/fallback, custom-domain binding, custom-domain DNS upsert, and verify.
|
|
74
74
|
- Deploy and verify can be rolled back with `AUTOVPN_STAGE_BACKEND_DEPLOY=python` and `AUTOVPN_STAGE_BACKEND_VERIFY=python`.
|
|
75
|
-
- `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node
|
|
75
|
+
- `AUTOVPN_NO_PYTHON=1` disables implicit Python backend resolution and default Python runtime stage fallback. Use it as a v3 readiness gate. Empty offline runs now complete in Node. Speedtest and availability use the per-node Mihomo runtime by default so candidate measurements and provider checks traverse each candidate node. Set `AUTOVPN_SPEEDTEST_RUNTIME=direct` or `AUTOVPN_AVAILABILITY_RUNTIME=direct` only for direct-host diagnostic checks.
|
|
76
76
|
- Project `.env` is loaded before resolving profile and artifact paths. Explicit process environment values still win over `.env`.
|
|
77
77
|
- `autovpn serve` is Node-native and exposes the browser UI plus `/api/health`,
|
|
78
78
|
`/api/state`, `/api/runs`, `/api/runs/current/stop`, and `/api/events`.
|
package/dist/doctor/checks.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { parse } from '@iarna/toml';
|
|
6
6
|
import { profileSummary } from '../config/profile.js';
|
|
7
7
|
import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
|
|
8
|
+
import { resolveShareWorkerTemplatePath, resolveWorkerTemplatePath } from '../runtime/templates.js';
|
|
8
9
|
import { normalizeManagedToolCommandForSpawn, resolveManagedNpmTool } from '../runtime/managed-tools.js';
|
|
9
10
|
const JAVASCRIPT_OBFUSCATOR_VERSION = '5.4.3';
|
|
10
11
|
const WRANGLER_VERSION = '4.106.0';
|
|
@@ -201,13 +202,27 @@ export async function runDoctor(projectRoot, argv, env = process.env, options =
|
|
|
201
202
|
const configuredSources = sourceValues.filter((source) => source.enabled && source.url === 'set' && source.key === 'set');
|
|
202
203
|
const nodeToolChecks = await checkNodeTools(projectRoot, env, options);
|
|
203
204
|
const cloudflareChecks = await checkCloudflare(summary, deploy, env, projectRoot, options);
|
|
205
|
+
let workerTemplatePath = '';
|
|
206
|
+
let shareWorkerTemplatePath = '';
|
|
207
|
+
try {
|
|
208
|
+
workerTemplatePath = resolveWorkerTemplatePath(projectRoot);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
workerTemplatePath = '';
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
shareWorkerTemplatePath = resolveShareWorkerTemplatePath(projectRoot, env);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
shareWorkerTemplatePath = '';
|
|
218
|
+
}
|
|
204
219
|
const checks = [
|
|
205
220
|
check('node_version', 'pass', `Node ${process.versions.node}`, { required: '>=20' }),
|
|
206
221
|
check('project_root', 'pass', 'Project root resolved', { path: projectRoot }),
|
|
207
222
|
check('profile_path', pathWritable(profilePath) ? 'pass' : 'fail', pathWritable(profilePath) ? 'Profile is readable and writable' : 'Profile path is not writable', { profile_path: profilePath, exists: fs.existsSync(profilePath) }),
|
|
208
223
|
check('artifacts_root', pathWritable(artifactsRoot) ? 'pass' : 'fail', pathWritable(artifactsRoot) ? 'Artifacts root is writable' : 'Artifacts root is not writable', { path: artifactsRoot }),
|
|
209
|
-
check('worker_template',
|
|
210
|
-
check('share_worker_template',
|
|
224
|
+
check('worker_template', workerTemplatePath ? 'pass' : 'fail', workerTemplatePath ? 'Worker template exists' : 'Worker template is missing', { path: workerTemplatePath || path.join(projectRoot, 'templates', 'vmess_node.js') }),
|
|
225
|
+
check('share_worker_template', shareWorkerTemplatePath ? 'pass' : 'warn', shareWorkerTemplatePath ? 'Share worker template exists' : 'Share worker template is missing', { path: shareWorkerTemplatePath || path.join(projectRoot, 'templates', 'share-worker', 'vpn.js') }),
|
|
211
226
|
configuredSources.length
|
|
212
227
|
? check('sources', 'pass', 'At least one enabled source is configured', { configured_count: configuredSources.length })
|
|
213
228
|
: check('sources', 'warn', 'No enabled source has both URL and key configured', { configured_count: 0, key_state: 'missing' }),
|
|
@@ -17,6 +17,9 @@ const CHALLENGE_PHRASES = [
|
|
|
17
17
|
'verify you are human',
|
|
18
18
|
'enable javascript and cookies'
|
|
19
19
|
];
|
|
20
|
+
const GEMINI_REGION_MARKERS = [',2,1,200,"', ',2,1,200,\\"'];
|
|
21
|
+
const CLAUDE_BLOCKED_CODES = new Set(['AF', 'BY', 'CN', 'CU', 'HK', 'IR', 'KP', 'MO', 'RU', 'SY']);
|
|
22
|
+
const GEMINI_BLOCKED_CODES = new Set(['CHN', 'RUS', 'BLR', 'CUB', 'IRN', 'PRK', 'SYR', 'HKG', 'MAC']);
|
|
20
23
|
const PYTHON_AVAILABILITY_HELPER = `
|
|
21
24
|
import json
|
|
22
25
|
import sys
|
|
@@ -75,6 +78,30 @@ function hostnameFor(url) {
|
|
|
75
78
|
return '';
|
|
76
79
|
}
|
|
77
80
|
}
|
|
81
|
+
function targetKey(target) {
|
|
82
|
+
return target.name.trim().toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
|
|
83
|
+
}
|
|
84
|
+
function extractTraceLocation(body) {
|
|
85
|
+
for (const line of body.split(/\r?\n/)) {
|
|
86
|
+
if (line.startsWith('loc=')) {
|
|
87
|
+
return line.slice(4).trim().toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
function extractGeminiCountryCode(body) {
|
|
93
|
+
for (const marker of GEMINI_REGION_MARKERS) {
|
|
94
|
+
const index = body.indexOf(marker);
|
|
95
|
+
if (index < 0) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const code = body.slice(index + marker.length, index + marker.length + 3);
|
|
99
|
+
if (/^[A-Z]{3}$/.test(code)) {
|
|
100
|
+
return code;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
78
105
|
export function normalizeProviderTargets(targets) {
|
|
79
106
|
if (targets == null) {
|
|
80
107
|
return PROVIDER_TARGETS.map((target) => ({ ...target, allowed_hosts: [...target.allowed_hosts], negative_phrases: [...target.negative_phrases] }));
|
|
@@ -126,6 +153,83 @@ export function evaluateProviderResponse(target, response) {
|
|
|
126
153
|
matched_phrase: ''
|
|
127
154
|
};
|
|
128
155
|
}
|
|
156
|
+
const key = targetKey(target);
|
|
157
|
+
if (key === 'chatgpt_ios') {
|
|
158
|
+
const body = response.body.toLowerCase();
|
|
159
|
+
if (body.includes('you may be connected to a disallowed isp')) {
|
|
160
|
+
return {
|
|
161
|
+
provider: target.name,
|
|
162
|
+
passed: false,
|
|
163
|
+
reason: 'disallowed_isp',
|
|
164
|
+
status_code: statusCode,
|
|
165
|
+
final_url: finalUrl,
|
|
166
|
+
matched_phrase: ''
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (body.includes('request is not allowed. please try again later.')) {
|
|
170
|
+
return {
|
|
171
|
+
provider: target.name,
|
|
172
|
+
passed: true,
|
|
173
|
+
reason: 'ok',
|
|
174
|
+
status_code: statusCode,
|
|
175
|
+
final_url: finalUrl,
|
|
176
|
+
matched_phrase: ''
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (body.includes('sorry, you have been blocked')) {
|
|
180
|
+
return {
|
|
181
|
+
provider: target.name,
|
|
182
|
+
passed: false,
|
|
183
|
+
reason: 'blocked',
|
|
184
|
+
status_code: statusCode,
|
|
185
|
+
final_url: finalUrl,
|
|
186
|
+
matched_phrase: ''
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
provider: target.name,
|
|
191
|
+
passed: false,
|
|
192
|
+
reason: 'unlock_failed',
|
|
193
|
+
status_code: statusCode,
|
|
194
|
+
final_url: finalUrl,
|
|
195
|
+
matched_phrase: ''
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (key === 'chatgpt' || key === 'chatgpt_web') {
|
|
199
|
+
const unsupported = response.body.toLowerCase().includes('unsupported_country');
|
|
200
|
+
return {
|
|
201
|
+
provider: target.name,
|
|
202
|
+
passed: !unsupported,
|
|
203
|
+
reason: unsupported ? 'unsupported_region' : 'ok',
|
|
204
|
+
status_code: statusCode,
|
|
205
|
+
final_url: finalUrl,
|
|
206
|
+
matched_phrase: unsupported ? 'unsupported_country' : ''
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (key === 'claude') {
|
|
210
|
+
const countryCode = extractTraceLocation(response.body);
|
|
211
|
+
const blocked = CLAUDE_BLOCKED_CODES.has(countryCode);
|
|
212
|
+
return {
|
|
213
|
+
provider: target.name,
|
|
214
|
+
passed: Boolean(countryCode) && !blocked,
|
|
215
|
+
reason: !countryCode ? 'unlock_failed' : blocked ? 'unsupported_region' : 'ok',
|
|
216
|
+
status_code: statusCode,
|
|
217
|
+
final_url: finalUrl,
|
|
218
|
+
matched_phrase: countryCode
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (key === 'gemini') {
|
|
222
|
+
const countryCode = extractGeminiCountryCode(response.body);
|
|
223
|
+
const blocked = GEMINI_BLOCKED_CODES.has(countryCode);
|
|
224
|
+
return {
|
|
225
|
+
provider: target.name,
|
|
226
|
+
passed: Boolean(countryCode) && !blocked,
|
|
227
|
+
reason: !countryCode ? 'unlock_failed' : blocked ? 'unsupported_region' : 'ok',
|
|
228
|
+
status_code: statusCode,
|
|
229
|
+
final_url: finalUrl,
|
|
230
|
+
matched_phrase: countryCode
|
|
231
|
+
};
|
|
232
|
+
}
|
|
129
233
|
if (statusCode >= 400) {
|
|
130
234
|
return {
|
|
131
235
|
provider: target.name,
|
|
@@ -594,7 +698,8 @@ async function checkBatchInNode(input, options) {
|
|
|
594
698
|
return [];
|
|
595
699
|
}
|
|
596
700
|
const targets = normalizeProviderTargets(input.targets);
|
|
597
|
-
const
|
|
701
|
+
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_AVAILABILITY_RUNTIME ?? '').trim().toLowerCase();
|
|
702
|
+
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
598
703
|
const checkLinkAvailability = options.checkLinkAvailability ?? ((speedResult, config) => (useMihomoRuntime
|
|
599
704
|
? checkLinkAvailabilityMihomo(speedResult, config, {
|
|
600
705
|
...options,
|
package/dist/pipeline/deploy.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { mergeProjectEnv } from '../runtime/env.js';
|
|
5
|
+
import { resolveShareWorkerTemplatePath } from '../runtime/templates.js';
|
|
5
6
|
import { normalizeManagedToolCommandForSpawn, resolveManagedNpmTool } from '../runtime/managed-tools.js';
|
|
6
7
|
const PYTHON_DEPLOY_HELPER = `
|
|
7
8
|
import json
|
|
@@ -47,6 +48,7 @@ json.dump(result, sys.stdout, ensure_ascii=False)
|
|
|
47
48
|
sys.stdout.write("\\n")
|
|
48
49
|
`;
|
|
49
50
|
const PAGES_PRODUCTION_BRANCH = 'main';
|
|
51
|
+
const CLOUDFLARE_API_MAX_ATTEMPTS = 3;
|
|
50
52
|
const FALLBACK_SUFFIX_PATTERN = /^(?<prefix>.+)-(?<suffix>\d+)$/;
|
|
51
53
|
const CLOUDFLARE_API_BASE_URL = 'https://api.cloudflare.com/client/v4';
|
|
52
54
|
const NETWORK_ERROR_MARKERS = [
|
|
@@ -354,51 +356,11 @@ function buildShareProjectUpdatePayload(sourceProject, runtimeEnv, options) {
|
|
|
354
356
|
deployment_configs: rewriteShareProjectSubValue(clonedConfigs, options)
|
|
355
357
|
};
|
|
356
358
|
}
|
|
357
|
-
async function fileExists(candidate) {
|
|
358
|
-
try {
|
|
359
|
-
await access(candidate);
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function expandHomePath(candidate) {
|
|
367
|
-
if (candidate === '~') {
|
|
368
|
-
return process.env.HOME || candidate;
|
|
369
|
-
}
|
|
370
|
-
if (candidate.startsWith('~/')) {
|
|
371
|
-
return process.env.HOME ? path.join(process.env.HOME, candidate.slice(2)) : candidate;
|
|
372
|
-
}
|
|
373
|
-
return candidate;
|
|
374
|
-
}
|
|
375
|
-
async function resolveShareProjectWorkerSourcePath(projectRoot, env) {
|
|
376
|
-
const candidates = [
|
|
377
|
-
clean(env.VPN_AUTOMATION_SHARE_WORKER_PATH),
|
|
378
|
-
path.join(projectRoot, 'electron', 'runtime', 'share-worker', 'vpn.js'),
|
|
379
|
-
path.join(projectRoot, 'templates', 'share-worker', 'vpn.js'),
|
|
380
|
-
path.join(path.dirname(projectRoot), 'cloudflarevpn', 'edgetunnel', 'vpn.js')
|
|
381
|
-
].filter(Boolean).map((candidate) => path.resolve(expandHomePath(candidate)));
|
|
382
|
-
const seen = new Set();
|
|
383
|
-
const uniqueCandidates = candidates.filter((candidate) => {
|
|
384
|
-
if (seen.has(candidate)) {
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
seen.add(candidate);
|
|
388
|
-
return true;
|
|
389
|
-
});
|
|
390
|
-
for (const candidate of uniqueCandidates) {
|
|
391
|
-
if (await fileExists(candidate)) {
|
|
392
|
-
return candidate;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
throw new Error(`share worker source not found; tried: ${uniqueCandidates.join(', ')}`);
|
|
396
|
-
}
|
|
397
359
|
function buildShareProjectBundleDir(projectRoot) {
|
|
398
360
|
return path.join(projectRoot, 'electron', 'runtime', 'share-worker', 'share_pages_bundle');
|
|
399
361
|
}
|
|
400
362
|
async function stageShareProjectWorkerBundle(projectRoot, env) {
|
|
401
|
-
const sourcePath =
|
|
363
|
+
const sourcePath = resolveShareWorkerTemplatePath(projectRoot, env);
|
|
402
364
|
const bundleDir = buildShareProjectBundleDir(projectRoot);
|
|
403
365
|
const workerEntry = path.join(bundleDir, '_worker.js');
|
|
404
366
|
await mkdir(bundleDir, { recursive: true });
|
|
@@ -508,12 +470,30 @@ export class CloudflareHttpClient {
|
|
|
508
470
|
this.headers = cloudflareHeaders(credentials);
|
|
509
471
|
this.fetchImpl = options.fetch ?? fetch;
|
|
510
472
|
}
|
|
473
|
+
async fetchWithTransientRetries(input, options, timeoutMs) {
|
|
474
|
+
for (let attempt = 1; attempt <= CLOUDFLARE_API_MAX_ATTEMPTS; attempt += 1) {
|
|
475
|
+
try {
|
|
476
|
+
return await this.fetchImpl(input, {
|
|
477
|
+
...options,
|
|
478
|
+
signal: options.signal ?? AbortSignal.timeout(timeoutMs)
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
const transientFailure = error instanceof TypeError
|
|
483
|
+
|| (error instanceof Error && error.name === 'TimeoutError');
|
|
484
|
+
const retryable = transientFailure && !options.signal?.aborted;
|
|
485
|
+
if (!retryable || attempt === CLOUDFLARE_API_MAX_ATTEMPTS) {
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
throw new Error('Cloudflare request failed');
|
|
491
|
+
}
|
|
511
492
|
async apiRequest(pathname, options = {}) {
|
|
512
|
-
const response = await this.
|
|
493
|
+
const response = await this.fetchWithTransientRetries(`${CLOUDFLARE_API_BASE_URL}${pathname}`, {
|
|
513
494
|
...options,
|
|
514
|
-
headers: { ...this.headers, ...options.headers }
|
|
515
|
-
|
|
516
|
-
});
|
|
495
|
+
headers: { ...this.headers, ...options.headers }
|
|
496
|
+
}, 20_000);
|
|
517
497
|
if (!response.ok) {
|
|
518
498
|
const body = await responseBodyText(response);
|
|
519
499
|
throw new Error(`Cloudflare API request failed (${response.status}): ${body || response.statusText}`);
|
|
@@ -665,10 +645,7 @@ export class CloudflareHttpClient {
|
|
|
665
645
|
return this.apiRequest(`/accounts/${accountId}/pages/projects/${projectName}`, { method: 'DELETE' });
|
|
666
646
|
}
|
|
667
647
|
async verifyUrl(url, expectedFragment = '') {
|
|
668
|
-
const response = await this.
|
|
669
|
-
method: 'GET',
|
|
670
|
-
signal: AbortSignal.timeout(30_000)
|
|
671
|
-
});
|
|
648
|
+
const response = await this.fetchWithTransientRetries(url, { method: 'GET' }, 30_000);
|
|
672
649
|
if (!response.ok) {
|
|
673
650
|
const body = await responseBodyText(response);
|
|
674
651
|
throw new Error(`URL verification failed (${response.status}): ${body || response.statusText}`);
|
|
@@ -5,6 +5,7 @@ import { parse } from '@iarna/toml';
|
|
|
5
5
|
import { mergeProjectEnv } from '../runtime/env.js';
|
|
6
6
|
import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
|
|
7
7
|
import { redactText } from '../runtime/redaction.js';
|
|
8
|
+
import { resolveWorkerTemplatePath } from '../runtime/templates.js';
|
|
8
9
|
import { fetchSourceLinksWithBackend } from './extract.js';
|
|
9
10
|
import { canonicalVmessKey, dedupeVmessLinksWithBackend, parseVmessLink } from './dedupe.js';
|
|
10
11
|
import { normalizeSpeedTestConfig, probeSpeedtestLinksInNode, selectSpeedtestCandidates, speedtestLinksWithBackend, testSpeedtestLinkInNode } from './speedtest.js';
|
|
@@ -650,7 +651,7 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
650
651
|
await writeLines(artifactDir, 'vpn_node_emoji.txt', postprocessed.links);
|
|
651
652
|
await setStage('postprocess', 'success');
|
|
652
653
|
await setStage('render', 'running');
|
|
653
|
-
const template = await readFile(
|
|
654
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
654
655
|
const rendered = await renderMainDataWithBackend({ template, links: postprocessed.links }, { cwd: projectRoot, env });
|
|
655
656
|
await setStage('render', 'success');
|
|
656
657
|
await setStage('obfuscate', 'running');
|
|
@@ -819,6 +820,8 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
819
820
|
await writeReport();
|
|
820
821
|
throw new Error('No links passed availability');
|
|
821
822
|
}
|
|
823
|
+
const availableLinkSet = new Set(availableLinks);
|
|
824
|
+
availabilityResults = availabilityResults.filter((result) => availableLinkSet.has(result.link));
|
|
822
825
|
await setStage('availability', 'success');
|
|
823
826
|
}
|
|
824
827
|
else if (isStageAtOrAfter(stage, 'postprocess')) {
|
|
@@ -861,7 +864,7 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
861
864
|
throw new Error('No postprocess output available to retry render');
|
|
862
865
|
}
|
|
863
866
|
await setStage('render', 'running');
|
|
864
|
-
const template = await readFile(
|
|
867
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
865
868
|
const rendered = await renderMainDataWithBackend({ template, links: finalLinks }, { cwd: projectRoot, env });
|
|
866
869
|
await writeFile(path.join(retryArtifactDir, 'vmess_node.js'), rendered.rendered_source, 'utf8');
|
|
867
870
|
await setStage('render', 'success');
|
|
@@ -1185,7 +1188,7 @@ export async function resumeNodePipeline(options, context = {}) {
|
|
|
1185
1188
|
}
|
|
1186
1189
|
await setStage('postprocess', 'success');
|
|
1187
1190
|
await setStage('render', 'running');
|
|
1188
|
-
const template = await readFile(
|
|
1191
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
1189
1192
|
const rendered = await renderMainDataWithBackend({ template, links: postprocessed.links }, { cwd: projectRoot, env });
|
|
1190
1193
|
await writeFile(path.join(artifactDir, 'vmess_node.js'), rendered.rendered_source, 'utf8');
|
|
1191
1194
|
await setStage('render', 'success');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const packagedWorkerTemplatePath = fileURLToPath(new URL('../templates/vmess_node.js', import.meta.url));
|
|
5
|
+
const packagedShareWorkerTemplatePath = fileURLToPath(new URL('../templates/share-worker/vpn.js', import.meta.url));
|
|
6
|
+
function expandHomePath(candidate, env) {
|
|
7
|
+
if (candidate === '~') {
|
|
8
|
+
return env.HOME || candidate;
|
|
9
|
+
}
|
|
10
|
+
if (candidate.startsWith('~/')) {
|
|
11
|
+
return env.HOME ? path.join(env.HOME, candidate.slice(2)) : candidate;
|
|
12
|
+
}
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
function firstExistingPath(candidates, message) {
|
|
16
|
+
const uniqueCandidates = [...new Set(candidates.filter(Boolean).map((candidate) => path.resolve(candidate)))];
|
|
17
|
+
const resolved = uniqueCandidates.find((candidate) => fs.existsSync(candidate));
|
|
18
|
+
if (resolved) {
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`${message}; checked: ${uniqueCandidates.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
export function resolveWorkerTemplatePath(projectRoot) {
|
|
24
|
+
return firstExistingPath([
|
|
25
|
+
path.join(projectRoot, 'templates', 'vmess_node.js'),
|
|
26
|
+
packagedWorkerTemplatePath
|
|
27
|
+
], 'Worker template is missing');
|
|
28
|
+
}
|
|
29
|
+
export function resolveShareWorkerTemplatePath(projectRoot, env = process.env) {
|
|
30
|
+
return firstExistingPath([
|
|
31
|
+
expandHomePath(String(env.VPN_AUTOMATION_SHARE_WORKER_PATH ?? '').trim(), env),
|
|
32
|
+
path.join(projectRoot, 'electron', 'runtime', 'share-worker', 'vpn.js'),
|
|
33
|
+
path.join(projectRoot, 'templates', 'share-worker', 'vpn.js'),
|
|
34
|
+
packagedShareWorkerTemplatePath,
|
|
35
|
+
path.join(path.dirname(projectRoot), 'cloudflarevpn', 'edgetunnel', 'vpn.js')
|
|
36
|
+
], 'Share Worker template is missing');
|
|
37
|
+
}
|