@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.
- package/README.md +102 -0
- package/bin/autovpn.mjs +5 -0
- package/dist/artifacts/list.js +99 -0
- package/dist/artifacts/preview.js +85 -0
- package/dist/backend/node-backend.js +194 -0
- package/dist/backend/python-backend.js +242 -0
- package/dist/backend/select-backend.js +12 -0
- package/dist/backend/types.js +1 -0
- package/dist/cli/commands/index.js +143 -0
- package/dist/cli/errors.js +7 -0
- package/dist/cli/global-options.js +29 -0
- package/dist/cli/main.js +231 -0
- package/dist/cli/native-commands.js +215 -0
- package/dist/cli/output.js +31 -0
- package/dist/config/profile.js +80 -0
- package/dist/doctor/checks.js +184 -0
- package/dist/events/schema.js +45 -0
- package/dist/jobs/commands.js +159 -0
- package/dist/jobs/logs.js +48 -0
- package/dist/jobs/process.js +75 -0
- package/dist/jobs/read.js +282 -0
- package/dist/jobs/store.js +115 -0
- package/dist/pipeline/availability.js +679 -0
- package/dist/pipeline/dedupe.js +104 -0
- package/dist/pipeline/deploy.js +1103 -0
- package/dist/pipeline/extract.js +259 -0
- package/dist/pipeline/obfuscate.js +203 -0
- package/dist/pipeline/orchestrator.js +1014 -0
- package/dist/pipeline/postprocess.js +214 -0
- package/dist/pipeline/proxy-runtime.js +228 -0
- package/dist/pipeline/render.js +91 -0
- package/dist/pipeline/speedtest.js +579 -0
- package/dist/runtime/env.js +35 -0
- package/dist/runtime/paths.js +66 -0
- package/dist/runtime/redaction.js +56 -0
- package/lib/cache.mjs +14 -0
- package/lib/errors.mjs +11 -0
- package/lib/install-python-cli.mjs +63 -0
- package/lib/runner.mjs +113 -0
- 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
|
+
}
|