@swimmingliu/autovpn 1.6.5 → 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.
@@ -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', fs.existsSync(path.join(projectRoot, 'templates', 'vmess_node.js')) ? 'pass' : 'fail', fs.existsSync(path.join(projectRoot, 'templates', 'vmess_node.js')) ? 'Worker template exists' : 'Worker template is missing', { path: path.join(projectRoot, 'templates', 'vmess_node.js') }),
210
- check('share_worker_template', fs.existsSync(path.join(projectRoot, 'templates', 'share-worker', 'vpn.js')) ? 'pass' : 'warn', fs.existsSync(path.join(projectRoot, 'templates', 'share-worker', 'vpn.js')) ? 'Share worker template exists' : 'Share worker template is missing', { path: path.join(projectRoot, 'templates', 'share-worker', 'vpn.js') }),
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' }),
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { spawn as defaultSpawn } from 'node:child_process';
3
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
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 = await resolveShareProjectWorkerSourcePath(projectRoot, env);
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.fetchImpl(`${CLOUDFLARE_API_BASE_URL}${pathname}`, {
493
+ const response = await this.fetchWithTransientRetries(`${CLOUDFLARE_API_BASE_URL}${pathname}`, {
513
494
  ...options,
514
- headers: { ...this.headers, ...options.headers },
515
- signal: options.signal ?? AbortSignal.timeout(20_000)
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.fetchImpl(url, {
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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
+ }