@swimmingliu/autovpn 1.4.0

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.
Files changed (40) hide show
  1. package/README.md +102 -0
  2. package/bin/autovpn.mjs +5 -0
  3. package/dist/artifacts/list.js +99 -0
  4. package/dist/artifacts/preview.js +85 -0
  5. package/dist/backend/node-backend.js +194 -0
  6. package/dist/backend/python-backend.js +242 -0
  7. package/dist/backend/select-backend.js +12 -0
  8. package/dist/backend/types.js +1 -0
  9. package/dist/cli/commands/index.js +143 -0
  10. package/dist/cli/errors.js +7 -0
  11. package/dist/cli/global-options.js +29 -0
  12. package/dist/cli/main.js +231 -0
  13. package/dist/cli/native-commands.js +215 -0
  14. package/dist/cli/output.js +31 -0
  15. package/dist/config/profile.js +80 -0
  16. package/dist/doctor/checks.js +184 -0
  17. package/dist/events/schema.js +45 -0
  18. package/dist/jobs/commands.js +159 -0
  19. package/dist/jobs/logs.js +48 -0
  20. package/dist/jobs/process.js +75 -0
  21. package/dist/jobs/read.js +282 -0
  22. package/dist/jobs/store.js +115 -0
  23. package/dist/pipeline/availability.js +679 -0
  24. package/dist/pipeline/dedupe.js +104 -0
  25. package/dist/pipeline/deploy.js +1103 -0
  26. package/dist/pipeline/extract.js +259 -0
  27. package/dist/pipeline/obfuscate.js +203 -0
  28. package/dist/pipeline/orchestrator.js +1014 -0
  29. package/dist/pipeline/postprocess.js +214 -0
  30. package/dist/pipeline/proxy-runtime.js +228 -0
  31. package/dist/pipeline/render.js +91 -0
  32. package/dist/pipeline/speedtest.js +579 -0
  33. package/dist/runtime/env.js +35 -0
  34. package/dist/runtime/paths.js +66 -0
  35. package/dist/runtime/redaction.js +56 -0
  36. package/lib/cache.mjs +14 -0
  37. package/lib/errors.mjs +11 -0
  38. package/lib/install-python-cli.mjs +63 -0
  39. package/lib/runner.mjs +113 -0
  40. package/package.json +39 -0
@@ -0,0 +1,56 @@
1
+ const SECRET_DEPLOYMENT_KEYS = new Set([
2
+ 'subscription_url',
3
+ 'verify_subscription_url',
4
+ 'secret_query',
5
+ 'share_project_sub_value',
6
+ 'pages_secret_admin'
7
+ ]);
8
+ const SECRET_FIELD_NAMES = [
9
+ 'token',
10
+ 'serect_key',
11
+ 'secret_key',
12
+ 'api_token',
13
+ 'api-token',
14
+ 'cloudflare_api_token',
15
+ 'subscription_url',
16
+ 'verify_subscription_url',
17
+ 'secret_query',
18
+ 'share_project_sub_value',
19
+ 'pages_secret_admin'
20
+ ];
21
+ const SECRET_FIELD_PATTERN = new RegExp(`(["']?)\\b(${SECRET_FIELD_NAMES.map((key) => key.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|')})\\b\\1(\\s*[:=]\\s*)(["']?)([^\\s"',}&]+)(\\4)`, 'gi');
22
+ const SECRET_TEXT_PATTERNS = [
23
+ [SECRET_FIELD_PATTERN, (_match, keyQuote, key, separator, valueQuote, _value, closingQuote) => `${keyQuote}${key}${keyQuote}${separator}${valueQuote}<redacted>${closingQuote}`],
24
+ [/(Bearer\s+)[A-Za-z0-9._~+/\-=]+/gi, '$1<redacted>'],
25
+ [/vmess:\/\/[A-Za-z0-9_\-+/=]+/g, 'vmess://<redacted>']
26
+ ];
27
+ export function redactText(value) {
28
+ return SECRET_TEXT_PATTERNS.reduce((result, [pattern, replacement]) => result.replace(pattern, replacement), value);
29
+ }
30
+ function redactNested(value) {
31
+ if (typeof value === 'string') {
32
+ return redactText(value);
33
+ }
34
+ if (value === null || ['number', 'boolean'].includes(typeof value)) {
35
+ return value;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ return value.map(redactNested);
39
+ }
40
+ if (typeof value === 'object') {
41
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactNested(item)]));
42
+ }
43
+ return typeof value;
44
+ }
45
+ export function safeDeployment(deployment) {
46
+ const safe = {};
47
+ for (const [key, value] of Object.entries(deployment)) {
48
+ if (SECRET_DEPLOYMENT_KEYS.has(key)) {
49
+ safe[key] = value ? 'set' : '';
50
+ }
51
+ else {
52
+ safe[key] = redactNested(value);
53
+ }
54
+ }
55
+ return safe;
56
+ }
package/lib/cache.mjs ADDED
@@ -0,0 +1,14 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export function getCacheDir(env = process.env) {
5
+ const override = String(env.AUTOVPN_CACHE_DIR ?? '').trim();
6
+ if (override) {
7
+ return override;
8
+ }
9
+ return path.join(os.homedir(), '.cache', 'autovpn', 'npm-wrapper');
10
+ }
11
+
12
+ export function pythonBinName(platform = process.platform) {
13
+ return platform === 'win32' ? 'Scripts' : 'bin';
14
+ }
package/lib/errors.mjs ADDED
@@ -0,0 +1,11 @@
1
+ export class WrapperError extends Error {
2
+ constructor(message, options = {}) {
3
+ super(message);
4
+ this.name = 'WrapperError';
5
+ this.cause = options.cause;
6
+ }
7
+ }
8
+
9
+ export function isEnabled(value) {
10
+ return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
11
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync as defaultSpawnSync } from 'node:child_process';
4
+
5
+ import { getCacheDir, pythonBinName } from './cache.mjs';
6
+ import { isEnabled, WrapperError } from './errors.mjs';
7
+
8
+ export function buildPipInstallArgs({ env = process.env, packageVersion }) {
9
+ const packageSpec = String(env.AUTOVPN_WHEEL_URL ?? '').trim()
10
+ || `${String(env.AUTOVPN_PYTHON_PACKAGE ?? 'vpn-subscription-automation').trim()}==${packageVersion}`;
11
+ const args = ['-m', 'pip', 'install'];
12
+ if (env.AUTOVPN_PIP_INDEX_URL) {
13
+ args.push('--index-url', String(env.AUTOVPN_PIP_INDEX_URL));
14
+ }
15
+ if (env.AUTOVPN_PIP_EXTRA_INDEX_URL) {
16
+ args.push('--extra-index-url', String(env.AUTOVPN_PIP_EXTRA_INDEX_URL));
17
+ }
18
+ args.push(packageSpec);
19
+ return args;
20
+ }
21
+
22
+ export function installPythonCli({
23
+ env = process.env,
24
+ packageVersion,
25
+ spawnSync = defaultSpawnSync,
26
+ cacheDir = getCacheDir(env),
27
+ platform = process.platform
28
+ } = {}) {
29
+ if (isEnabled(env.AUTOVPN_NO_INSTALL)) {
30
+ throw new WrapperError('No compatible Python autovpn CLI found and AUTOVPN_NO_INSTALL is enabled.');
31
+ }
32
+ if (!packageVersion) {
33
+ throw new WrapperError('Cannot install Python AutoVPN backend without an npm package version.');
34
+ }
35
+
36
+ const venvDir = path.join(cacheDir, `python-${packageVersion}`);
37
+ const executable = platform === 'win32' ? 'autovpn.exe' : 'autovpn';
38
+ const cliPath = path.join(venvDir, pythonBinName(platform), executable);
39
+ const forceInstall = isEnabled(env.AUTOVPN_FORCE_INSTALL);
40
+
41
+ if (!forceInstall && fs.existsSync(cliPath)) {
42
+ return { command: cliPath, args: [], source: 'managed-venv' };
43
+ }
44
+
45
+ fs.mkdirSync(cacheDir, { recursive: true });
46
+ const pythonCommand = String(env.PYTHON ?? env.PYTHON3 ?? 'python3');
47
+ const venvResult = spawnSync(pythonCommand, ['-m', 'venv', venvDir], { stdio: 'inherit', env });
48
+ if (venvResult.status !== 0) {
49
+ throw new WrapperError(`Failed to create AutoVPN Python backend venv with ${pythonCommand}.`);
50
+ }
51
+
52
+ const pythonPath = path.join(venvDir, pythonBinName(platform), platform === 'win32' ? 'python.exe' : 'python');
53
+ const installResult = spawnSync(pythonPath, buildPipInstallArgs({ env, packageVersion }), { stdio: 'inherit', env });
54
+ if (installResult.status !== 0) {
55
+ throw new WrapperError('Failed to install AutoVPN Python backend into the wrapper-managed venv.');
56
+ }
57
+
58
+ if (!fs.existsSync(cliPath)) {
59
+ throw new WrapperError(`Installed Python backend did not provide autovpn at ${cliPath}.`);
60
+ }
61
+
62
+ return { command: cliPath, args: [], source: 'managed-venv' };
63
+ }
package/lib/runner.mjs ADDED
@@ -0,0 +1,113 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ spawn as defaultSpawn,
6
+ spawnSync as defaultSpawnSync
7
+ } from 'node:child_process';
8
+
9
+ import { isEnabled, WrapperError } from './errors.mjs';
10
+ import { installPythonCli } from './install-python-cli.mjs';
11
+
12
+ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
13
+
14
+ export function readPackageVersion() {
15
+ const payload = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
16
+ return String(payload.version);
17
+ }
18
+
19
+ function normalizeVersionOutput(stdout) {
20
+ return String(stdout ?? '').trim();
21
+ }
22
+
23
+ export function versionMatches(stdout, packageVersion) {
24
+ return normalizeVersionOutput(stdout) === `autovpn ${packageVersion}`;
25
+ }
26
+
27
+ export function resolvePythonCli({
28
+ env = process.env,
29
+ packageVersion = readPackageVersion(),
30
+ spawnSync = defaultSpawnSync
31
+ } = {}) {
32
+ if (isEnabled(env.AUTOVPN_NO_PYTHON)) {
33
+ throw new WrapperError('Python backend is disabled by AUTOVPN_NO_PYTHON.');
34
+ }
35
+
36
+ const explicit = String(env.AUTOVPN_PYTHON_CLI ?? '').trim();
37
+ if (explicit) {
38
+ return { command: explicit, args: [], source: 'AUTOVPN_PYTHON_CLI' };
39
+ }
40
+
41
+ const probe = spawnSync('autovpn', ['--version'], {
42
+ encoding: 'utf-8',
43
+ env: { ...env, AUTOVPN_WRAPPER_PROBE: '1' },
44
+ stdio: ['ignore', 'pipe', 'pipe']
45
+ });
46
+ if (probe.status === 0) {
47
+ if (versionMatches(probe.stdout, packageVersion) || isEnabled(env.AUTOVPN_ALLOW_VERSION_MISMATCH)) {
48
+ return { command: 'autovpn', args: [], source: 'PATH' };
49
+ }
50
+ }
51
+
52
+ throw new WrapperError(
53
+ `No compatible Python autovpn CLI found for version ${packageVersion}. Set AUTOVPN_PYTHON_CLI or allow the wrapper to install the backend.`
54
+ );
55
+ }
56
+
57
+ export function resolveOrInstallPythonCli({
58
+ env = process.env,
59
+ packageVersion = readPackageVersion(),
60
+ spawnSync = defaultSpawnSync,
61
+ installer = installPythonCli
62
+ } = {}) {
63
+ try {
64
+ return resolvePythonCli({ env, packageVersion, spawnSync });
65
+ } catch (error) {
66
+ if (isEnabled(env.AUTOVPN_NO_PYTHON)) {
67
+ throw error;
68
+ }
69
+ return installer({ env, packageVersion, spawnSync });
70
+ }
71
+ }
72
+
73
+ export function runForwarder(argv = process.argv.slice(2), {
74
+ env = process.env,
75
+ cwd = process.cwd(),
76
+ packageVersion = readPackageVersion(),
77
+ spawn = defaultSpawn,
78
+ spawnSync = defaultSpawnSync,
79
+ installer = installPythonCli
80
+ } = {}) {
81
+ const resolved = resolveOrInstallPythonCli({ env, packageVersion, spawnSync, installer });
82
+ const child = spawn(resolved.command, [...resolved.args, ...argv], {
83
+ cwd,
84
+ env,
85
+ stdio: 'inherit'
86
+ });
87
+
88
+ return new Promise((resolve, reject) => {
89
+ child.on('error', reject);
90
+ child.on('exit', (code, signal) => {
91
+ if (signal) {
92
+ resolve(1);
93
+ return;
94
+ }
95
+ resolve(typeof code === 'number' ? code : 1);
96
+ });
97
+ });
98
+ }
99
+
100
+ export async function main(argv = process.argv.slice(2), options = {}) {
101
+ const env = options.env ?? process.env;
102
+ if (isEnabled(env.AUTOVPN_WRAPPER_PROBE) && argv.length === 1 && argv[0] === '--version') {
103
+ return 42;
104
+ }
105
+
106
+ try {
107
+ return await runForwarder(argv, { ...options, env });
108
+ } catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ console.error(`autovpn npm wrapper error: ${message}`);
111
+ return 1;
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@swimmingliu/autovpn",
3
+ "version": "1.4.0",
4
+ "description": "npm wrapper for the AutoVPN headless CLI",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/SwimmingLiu/auto-vpn.git",
9
+ "directory": "npm/autovpn-cli"
10
+ },
11
+ "bin": {
12
+ "autovpn": "bin/autovpn.mjs"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "dist/",
17
+ "lib/",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json",
25
+ "prepack": "npm run build",
26
+ "test": "npm run build && node --test test/*.test.mjs test/parity/*.test.mjs test/jobs/*.test.mjs test/pipeline/*.test.mjs test/runtime/*.test.mjs"
27
+ },
28
+ "engines": {
29
+ "node": ">=22.5.0"
30
+ },
31
+ "license": "UNLICENSED",
32
+ "devDependencies": {
33
+ "@types/node": "^24.0.0",
34
+ "typescript": "^5.9.2"
35
+ },
36
+ "dependencies": {
37
+ "@iarna/toml": "^2.2.5"
38
+ }
39
+ }