@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,259 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { spawn as defaultSpawn } from 'node:child_process';
4
+ import { mergeProjectEnv } from '../runtime/env.js';
5
+ const PYTHON_EXTRACT_HELPER = `
6
+ import json
7
+ import sys
8
+ from vpn_automation.config.models import SourceConfig
9
+ from vpn_automation.pipeline.extract import fetch_source_links
10
+
11
+ payload = json.load(sys.stdin)
12
+ result = fetch_source_links(payload["source_name"], SourceConfig(**payload["source"]))
13
+ json.dump(
14
+ {
15
+ "source_name": result.source_name,
16
+ "requested_iterations": result.requested_iterations,
17
+ "successful_iterations": result.successful_iterations,
18
+ "failed_iterations": result.failed_iterations,
19
+ "links": result.links,
20
+ },
21
+ sys.stdout,
22
+ ensure_ascii=False,
23
+ )
24
+ sys.stdout.write("\\n")
25
+ `;
26
+ function defaultRandomInt(start, end) {
27
+ return Math.floor(Math.random() * (end - start + 1)) + start;
28
+ }
29
+ export function buildRuntimeSourceUrl(source, iteration = 0, options = {}) {
30
+ const url = new URL(source.url);
31
+ if (source.use_random_area && iteration > 0) {
32
+ let areaMin = Number(source.area_min ?? 0);
33
+ let areaMax = Number(source.area_max ?? 100);
34
+ if (areaMin > areaMax) {
35
+ [areaMin, areaMax] = [areaMax, areaMin];
36
+ }
37
+ url.searchParams.set('area', String((options.randomInt ?? defaultRandomInt)(areaMin, areaMax)));
38
+ }
39
+ if (url.searchParams.has('t')) {
40
+ url.searchParams.set('t', (options.timeNow ?? (() => Date.now() / 1000))().toFixed(6));
41
+ }
42
+ return url.toString();
43
+ }
44
+ export function decryptPayload(cipherText, key) {
45
+ const keyBuffer = Buffer.from(key, 'utf8');
46
+ const decipher = crypto.createDecipheriv('aes-128-cbc', keyBuffer, keyBuffer);
47
+ decipher.setAutoPadding(false);
48
+ const plain = Buffer.concat([decipher.update(Buffer.from(cipherText, 'base64')), decipher.final()]);
49
+ return plain.toString('utf8').replace(/\0+$/g, '');
50
+ }
51
+ export function transformNodeId(original) {
52
+ return String(original).split('-').map((part) => {
53
+ const chunks = [];
54
+ for (let index = 0; index < part.length; index += 4) {
55
+ const chunk = part.slice(index, index + 4);
56
+ if (chunk) {
57
+ chunks.push(`${chunk.slice(2)}${chunk.slice(0, 2)}`);
58
+ }
59
+ }
60
+ return chunks.join('');
61
+ }).join('-');
62
+ }
63
+ function generateVmessLink(payload) {
64
+ const json = JSON.stringify(payload);
65
+ const encoded = Buffer.from(json, 'utf8').toString('base64').replaceAll('+', '-').replaceAll('/', '_');
66
+ return `vmess://${encoded}`;
67
+ }
68
+ function payloadFromOutboundConfig(psName, jsonText) {
69
+ const config = JSON.parse(jsonText);
70
+ const outbound = (config.outbounds ?? []).find((item) => item.protocol === 'vmess');
71
+ const vnext = outbound.settings.vnext[0];
72
+ const user = vnext.users[0];
73
+ const streamSettings = outbound.streamSettings ?? {};
74
+ const wsSettings = streamSettings.wsSettings ?? {};
75
+ const headers = wsSettings.headers ?? {};
76
+ const host = wsSettings.host || headers.Host || vnext.address;
77
+ const psValue = typeof config.ps === 'string' ? config.ps.trim() : (config.ps ?? psName);
78
+ return {
79
+ v: 2,
80
+ ps: psValue,
81
+ add: vnext.address,
82
+ port: String(vnext.port),
83
+ id: transformNodeId(user.id),
84
+ aid: String(user.alterId ?? 0),
85
+ scy: user.security ?? 'auto',
86
+ net: streamSettings.network ?? 'ws',
87
+ type: 'dtls',
88
+ host,
89
+ path: wsSettings.path ?? '',
90
+ tls: streamSettings.security ?? '',
91
+ sni: streamSettings.tlsSettings?.serverName ?? ''
92
+ };
93
+ }
94
+ export function extractLinksFromPlaintext(sourceName, plaintext) {
95
+ const cleaned = String(plaintext).trim();
96
+ if (!cleaned) {
97
+ return [];
98
+ }
99
+ if (cleaned.startsWith('vmess://')) {
100
+ return [cleaned];
101
+ }
102
+ const parts = cleaned.split('|');
103
+ if (parts.length < 2) {
104
+ return [];
105
+ }
106
+ const psName = parts[0].trim() || sourceName;
107
+ const jsonText = parts[1].trim();
108
+ return [generateVmessLink(payloadFromOutboundConfig(psName, jsonText))];
109
+ }
110
+ export function selectPipelineStageBackend(stage, env = process.env) {
111
+ const stageKey = `AUTOVPN_STAGE_BACKEND_${stage.toUpperCase()}`;
112
+ const stageOverride = String(env[stageKey] ?? '').trim().toLowerCase();
113
+ const pipelineOverride = String(env.AUTOVPN_PIPELINE_BACKEND ?? '').trim().toLowerCase();
114
+ const selected = stageOverride || pipelineOverride || 'node';
115
+ return selected === 'python' ? 'python' : 'node';
116
+ }
117
+ async function defaultResolvePythonCli(env) {
118
+ // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
119
+ const runner = await import('../../lib/runner.mjs');
120
+ return runner.resolveOrInstallPythonCli({ env });
121
+ }
122
+ function pythonCommandFor(resolved) {
123
+ const command = resolved.command;
124
+ const name = path.basename(command).toLowerCase();
125
+ if (['autovpn', 'autovpn.exe'].includes(name)) {
126
+ const executable = process.platform === 'win32' ? 'python.exe' : 'python';
127
+ return path.join(path.dirname(command), executable);
128
+ }
129
+ return process.platform === 'win32' ? 'python.exe' : 'python3';
130
+ }
131
+ function numberOrDefault(value, fallback) {
132
+ const parsed = Number(value);
133
+ return Number.isFinite(parsed) ? parsed : fallback;
134
+ }
135
+ async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
136
+ const controller = new AbortController();
137
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
138
+ try {
139
+ return await fetchImpl(url, { signal: controller.signal });
140
+ }
141
+ finally {
142
+ clearTimeout(timer);
143
+ }
144
+ }
145
+ async function fetchSourceLinksInNode(input, options) {
146
+ const source = input.source;
147
+ const maxIterations = Math.max(0, Math.trunc(numberOrDefault(source.max_iterations, 0)));
148
+ const minIterations = Math.max(0, Math.trunc(numberOrDefault(source.min_iterations, 0)));
149
+ const plateauLimit = Math.max(1, Math.trunc(numberOrDefault(source.plateau_limit, 1)));
150
+ const failureLimit = Math.max(1, Math.trunc(numberOrDefault(source.failure_limit, 1)));
151
+ const maxRuntimeSeconds = Math.max(0, numberOrDefault(source.max_runtime_seconds, 0));
152
+ const startIteration = Math.max(1, Math.trunc(numberOrDefault(source.resume_from_iteration, 1)));
153
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
154
+ if (!fetchImpl) {
155
+ throw new Error('Node extract backend requires fetch support');
156
+ }
157
+ if (!String(source.url ?? '').trim() || !String(source.key ?? '').trim()) {
158
+ return {
159
+ source_name: input.source_name,
160
+ requested_iterations: maxIterations,
161
+ successful_iterations: 0,
162
+ failed_iterations: 0,
163
+ links: []
164
+ };
165
+ }
166
+ const links = [];
167
+ const seen = new Set();
168
+ let plateau = 0;
169
+ let successes = 0;
170
+ let failures = 0;
171
+ const startedAt = Date.now();
172
+ for (let iteration = startIteration - 1; iteration < maxIterations; iteration += 1) {
173
+ const attempt = iteration + 1;
174
+ if (maxRuntimeSeconds > 0 && attempt > minIterations && (Date.now() - startedAt) / 1000 >= maxRuntimeSeconds) {
175
+ break;
176
+ }
177
+ try {
178
+ const url = buildRuntimeSourceUrl(source, iteration);
179
+ const response = await fetchWithTimeout(fetchImpl, url, 20_000);
180
+ if (response.ok === false || Number(response.status ?? 200) >= 400) {
181
+ throw new Error(`HTTP ${Number(response.status ?? 0)}`);
182
+ }
183
+ const plaintext = decryptPayload((await response.text()).trim(), source.key);
184
+ const extracted = extractLinksFromPlaintext(input.source_name, plaintext);
185
+ successes += 1;
186
+ failures = 0;
187
+ let newItems = 0;
188
+ for (const link of extracted) {
189
+ if (seen.has(link)) {
190
+ continue;
191
+ }
192
+ seen.add(link);
193
+ links.push(link);
194
+ newItems += 1;
195
+ }
196
+ plateau = newItems === 0 ? plateau + 1 : 0;
197
+ if (plateau >= plateauLimit && attempt >= minIterations) {
198
+ break;
199
+ }
200
+ }
201
+ catch (error) {
202
+ failures += 1;
203
+ if (failures >= failureLimit && attempt >= minIterations) {
204
+ break;
205
+ }
206
+ if (attempt >= maxIterations) {
207
+ throw new Error(`Node extract request failed: ${error instanceof Error ? error.message : String(error)}`);
208
+ }
209
+ }
210
+ }
211
+ return {
212
+ source_name: input.source_name,
213
+ requested_iterations: maxIterations,
214
+ successful_iterations: successes,
215
+ failed_iterations: failures,
216
+ links
217
+ };
218
+ }
219
+ async function extractWithPython(input, options) {
220
+ const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
221
+ const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
222
+ const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_EXTRACT_HELPER], {
223
+ cwd: options.cwd ?? process.cwd(),
224
+ env,
225
+ stdio: ['pipe', 'pipe', 'pipe']
226
+ });
227
+ let stdout = '';
228
+ let stderr = '';
229
+ child.stdout?.on('data', (chunk) => {
230
+ stdout += String(chunk);
231
+ });
232
+ child.stderr?.on('data', (chunk) => {
233
+ stderr += String(chunk);
234
+ });
235
+ const completion = new Promise((resolve, reject) => {
236
+ child.on('error', reject);
237
+ child.on('close', (code) => {
238
+ if (code !== 0) {
239
+ reject(new Error(`Python extract backend failed with exit code ${code}: ${stderr.trim()}`));
240
+ return;
241
+ }
242
+ try {
243
+ resolve(JSON.parse(stdout));
244
+ }
245
+ catch (error) {
246
+ reject(new Error(`Python extract backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
247
+ }
248
+ });
249
+ });
250
+ child.stdin?.write(JSON.stringify(input));
251
+ child.stdin?.end();
252
+ return completion;
253
+ }
254
+ export async function fetchSourceLinksWithBackend(input, options = {}) {
255
+ if (selectPipelineStageBackend('extract', options.env ?? process.env) === 'python') {
256
+ return options.pythonExtract ? options.pythonExtract(input) : extractWithPython(input, options);
257
+ }
258
+ return options.fetchSourceLinks ? options.fetchSourceLinks(input) : fetchSourceLinksInNode(input, options);
259
+ }
@@ -0,0 +1,203 @@
1
+ import path from 'node:path';
2
+ import { spawn as defaultSpawn } from 'node:child_process';
3
+ import { mergeProjectEnv } from '../runtime/env.js';
4
+ const DEFAULT_WORKER_BUILD_CONFIG = {
5
+ environment_name: 'production',
6
+ entry_filename: '_worker.js',
7
+ bundle_subdir: 'pages_bundle',
8
+ modules_subdir: 'modules',
9
+ manifest_filename: 'manifest.json',
10
+ variable_prefix: 'sg',
11
+ comment_template: 'subscription worker: returns encoded payload on secret match, random bytes otherwise',
12
+ random_noise_min_length: 24,
13
+ random_noise_max_length: 96,
14
+ enable_keyword_fragmentation: true,
15
+ enable_identifier_randomization: true,
16
+ emit_sidecar_modules: true
17
+ };
18
+ const PYTHON_OBFUSCATE_HELPER = `
19
+ import json
20
+ import sys
21
+ from vpn_automation.config.models import WorkerBuildConfig
22
+ from vpn_automation.pipeline.worker_build import build_worker_artifacts
23
+
24
+ payload = json.load(sys.stdin)
25
+ config = WorkerBuildConfig(**(payload.get("config") or {}))
26
+ artifacts = build_worker_artifacts(payload["rendered_source"], config, payload["secret_query"])
27
+ json.dump(
28
+ {
29
+ "transformed_source": artifacts.transformed_source,
30
+ "modules": artifacts.modules,
31
+ "manifest": artifacts.manifest,
32
+ },
33
+ sys.stdout,
34
+ ensure_ascii=False,
35
+ )
36
+ sys.stdout.write("\\n")
37
+ `;
38
+ function jsonStringLiteral(value) {
39
+ return JSON.stringify(value);
40
+ }
41
+ function escapeRegExp(value) {
42
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43
+ }
44
+ export function resolveWorkerBuildConfig(config = {}) {
45
+ return {
46
+ ...DEFAULT_WORKER_BUILD_CONFIG,
47
+ ...Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined))
48
+ };
49
+ }
50
+ export function stableIdentifierPrefix(prefix) {
51
+ const normalized = String(prefix || 'sg').replace(/[^a-zA-Z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
52
+ if (!normalized) {
53
+ return 'sg';
54
+ }
55
+ if (/^\d/.test(normalized)) {
56
+ return `sg_${normalized}`;
57
+ }
58
+ return normalized;
59
+ }
60
+ function splitLiteral(value) {
61
+ if (value.includes('_')) {
62
+ const separator = value.indexOf('_');
63
+ const head = value.slice(0, separator);
64
+ const tail = value.slice(separator + 1);
65
+ return [head.slice(0, 3), head.slice(3), `_${tail}`].filter((part) => part.length > 0);
66
+ }
67
+ if (value.length > 8) {
68
+ return [value.slice(0, 4), value.slice(4, 8), value.slice(8)].filter((part) => part.length > 0);
69
+ }
70
+ if (value.length > 4) {
71
+ return [value.slice(0, 4), value.slice(4)].filter((part) => part.length > 0);
72
+ }
73
+ return [value];
74
+ }
75
+ export function fragmentLiteral(value, enabled) {
76
+ if (!enabled) {
77
+ return jsonStringLiteral(value);
78
+ }
79
+ const quoted = splitLiteral(value).map((part) => jsonStringLiteral(part).replaceAll('"', "'")).join(', ');
80
+ return `[${quoted}].join('')`;
81
+ }
82
+ function formatComment(template, environmentName) {
83
+ return template
84
+ .replaceAll('{{', '\u0000OPEN_BRACE\u0000')
85
+ .replaceAll('}}', '\u0000CLOSE_BRACE\u0000')
86
+ .replaceAll('{environment_name}', environmentName)
87
+ .replaceAll('\u0000OPEN_BRACE\u0000', '{')
88
+ .replaceAll('\u0000CLOSE_BRACE\u0000', '}');
89
+ }
90
+ function buildTransformedSource(renderedSource, config, secretKey, secretValue) {
91
+ let source = renderedSource;
92
+ if (config.enable_identifier_randomization) {
93
+ const prefix = stableIdentifierPrefix(config.variable_prefix);
94
+ const replacements = {
95
+ secretToken: `${prefix}_secret_token`,
96
+ responsePayload: `${prefix}_response_payload`,
97
+ randomBytes: `${prefix}_random_bytes`,
98
+ error: `${prefix}_error`
99
+ };
100
+ for (const [oldName, newName] of Object.entries(replacements)) {
101
+ source = source.replace(new RegExp(`\\b${escapeRegExp(oldName)}\\b`, 'g'), newName);
102
+ }
103
+ }
104
+ source = source.replace(`searchParams.get("${secretKey}")`, `searchParams.get(${fragmentLiteral(secretKey, config.enable_keyword_fragmentation)})`);
105
+ source = source.replace(`=== "${secretValue}"`, `=== ${fragmentLiteral(secretValue, config.enable_keyword_fragmentation)}`);
106
+ return `// ${formatComment(config.comment_template, config.environment_name)}\n${source}`;
107
+ }
108
+ function extractMainData(renderedSource) {
109
+ const match = renderedSource.match(/const (?:MainData|SUBSCRIPTION_PAYLOAD) = `(?<payload>[\s\S]*?)`;/);
110
+ return match?.groups?.payload ?? '';
111
+ }
112
+ export function buildWorkerArtifacts(renderedSource, configInput = {}, secretQuery) {
113
+ const config = resolveWorkerBuildConfig(configInput);
114
+ const separator = secretQuery.indexOf('=');
115
+ if (separator === -1) {
116
+ throw new Error('secret_query must contain a key=value pair');
117
+ }
118
+ const secretKey = secretQuery.slice(0, separator);
119
+ const secretValue = secretQuery.slice(separator + 1);
120
+ const transformedSource = buildTransformedSource(renderedSource, config, secretKey, secretValue);
121
+ const modulesSubdir = config.modules_subdir.replace(/^\/+|\/+$/g, '') || 'modules';
122
+ const modules = {
123
+ [`${modulesSubdir}/runtime.js`]: `export const workerSource = ${jsonStringLiteral(transformedSource)};\n`,
124
+ [`${modulesSubdir}/guard.js`]: (`export const secretParam = ${fragmentLiteral(secretKey, config.enable_keyword_fragmentation)};\n` +
125
+ `export const secretValue = ${fragmentLiteral(secretValue, config.enable_keyword_fragmentation)};\n`),
126
+ [`${modulesSubdir}/noise.js`]: `export const noiseLengthRange = [${config.random_noise_min_length}, ${config.random_noise_max_length}];\n`,
127
+ [`${modulesSubdir}/payload.js`]: `export const mainData = ${jsonStringLiteral(extractMainData(renderedSource))};\n`
128
+ };
129
+ return {
130
+ transformed_source: transformedSource,
131
+ modules,
132
+ manifest: {
133
+ environment_name: config.environment_name,
134
+ entry_filename: config.entry_filename,
135
+ modules: Object.keys(modules).sort(),
136
+ variable_prefix: config.variable_prefix,
137
+ enable_keyword_fragmentation: config.enable_keyword_fragmentation,
138
+ enable_identifier_randomization: config.enable_identifier_randomization
139
+ }
140
+ };
141
+ }
142
+ export function selectPipelineStageBackend(stage, env = process.env) {
143
+ const stageKey = `AUTOVPN_STAGE_BACKEND_${stage.toUpperCase()}`;
144
+ const stageOverride = String(env[stageKey] ?? '').trim().toLowerCase();
145
+ const pipelineOverride = String(env.AUTOVPN_PIPELINE_BACKEND ?? '').trim().toLowerCase();
146
+ const selected = stageOverride || pipelineOverride || 'node';
147
+ return selected === 'python' ? 'python' : 'node';
148
+ }
149
+ async function defaultResolvePythonCli(env) {
150
+ // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
151
+ const runner = await import('../../lib/runner.mjs');
152
+ return runner.resolveOrInstallPythonCli({ env });
153
+ }
154
+ function pythonCommandFor(resolved) {
155
+ const command = resolved.command;
156
+ const name = path.basename(command).toLowerCase();
157
+ if (['autovpn', 'autovpn.exe'].includes(name)) {
158
+ const executable = process.platform === 'win32' ? 'python.exe' : 'python';
159
+ return path.join(path.dirname(command), executable);
160
+ }
161
+ return process.platform === 'win32' ? 'python.exe' : 'python3';
162
+ }
163
+ async function obfuscateWithPython(input, options) {
164
+ const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
165
+ const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
166
+ const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_OBFUSCATE_HELPER], {
167
+ cwd: options.cwd ?? process.cwd(),
168
+ env,
169
+ stdio: ['pipe', 'pipe', 'pipe']
170
+ });
171
+ let stdout = '';
172
+ let stderr = '';
173
+ child.stdout?.on('data', (chunk) => {
174
+ stdout += String(chunk);
175
+ });
176
+ child.stderr?.on('data', (chunk) => {
177
+ stderr += String(chunk);
178
+ });
179
+ const completion = new Promise((resolve, reject) => {
180
+ child.on('error', reject);
181
+ child.on('close', (code) => {
182
+ if (code !== 0) {
183
+ reject(new Error(`Python obfuscate backend failed with exit code ${code}: ${stderr.trim()}`));
184
+ return;
185
+ }
186
+ try {
187
+ resolve(JSON.parse(stdout));
188
+ }
189
+ catch (error) {
190
+ reject(new Error(`Python obfuscate backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
191
+ }
192
+ });
193
+ });
194
+ child.stdin?.write(JSON.stringify(input));
195
+ child.stdin?.end();
196
+ return completion;
197
+ }
198
+ export async function buildWorkerArtifactsWithBackend(input, options = {}) {
199
+ if (selectPipelineStageBackend('obfuscate', options.env ?? process.env) === 'python') {
200
+ return options.pythonObfuscate ? options.pythonObfuscate(input) : obfuscateWithPython(input, options);
201
+ }
202
+ return buildWorkerArtifacts(input.rendered_source, input.config, input.secret_query);
203
+ }