+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
diff --git a/README.md b/README.md
index 91a5e97c96..386d3660aa 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,110 @@
-# Sub-Store
-> This project is still under active development. Current version: v0.1 (backend only).
+
+
+
+
+
+
Sub-Store
+
+
+
+Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
+
+
+[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
+
+[](https://www.buymeacoffee.com/PengYM)
+
+[📚 文档/DOC](https://github.com/sub-store-org/Sub-Store/wiki)
+
+## sub.store Domain Safety Notice
+
+### Statement
+
+⚠️ `sub.store` is only the domain used by module-script rewrite MitM rules. It is not a public domain owned by us.
+
+### Risk
+
+If a request does not go through the rewrite, the data will be sent to the public `sub.store` service.
+
+You can map `sub.store` to `127.0.0.1` or another local address to prevent accidental access to the public `sub.store`. However, ordinary users may still send requests to the public `sub.store` after switching or toggling configuration modules.
+
+1. It could, in theory, redirect users to a fake frontend. This is only a possibility and does not imply that the owner of `sub.store` would do this. Note: The official frontend is `https://sub-store.vercel.app`.
+2. It could receive user data from `sub.store`.
+
+This creates a data leakage risk.
+
+### Plan
+
+After listening to suggestions from the group, we will not switch to a new domain for now. Choosing a new domain is also awkward: it needs to be related, short, and unlikely to be registered by someone else, at least in the short term.
+
+This notice is published only as an announcement. No changes will be made for now.
+
+Example:
+
+```
+[Host]
+sub.store = 127.0.0.1
+```
+
+## Core functionalities:
-Subscription manager for QX, Loon and Surge.
-Core functionality:
1. Conversion among various formats.
2. Subscription formatting.
-3. Collect multiple subscription in one URL.
+3. Collect multiple subscriptions in one URL.
+4. Host and modify subscriptions/files
+
+> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
+
## 1. Subscription Conversion
+
### Supported Input Formats
-- [x] SS URI
-- [x] SSR URI
-- [x] V2RayN URI
-- [x] QX (SS, SSR, VMess, Trojan, HTTP)
-- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
-- [x] Surge (SS, VMess, Trojan, HTTP)
+
+[本地节点怎么写/How To Write A Local Node](https://t.me/zhetengsha/824)
+
+> ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs. However, we have already supported some very common non-standard URIs (such as VMess, VLESS).
+
+- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
+
+ example: `socks5+tls://user:pass@ip:port#name`
+
+- [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
+ > Please note, HTTP(s) does not have a standard URI format, so it is not supported. Please use other formats.
+- [x] Clash Proxies YAML
+- [x] Clash Proxy JSON/JSON5/YAML(single line)
+ > [NaiveProxy](https://t.me/zhetengsha/4308)
+- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS, AnyTLS)
+- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2, AnyTLS)
+- [x] Surge (Direct, SS, VMess, Trojan, HTTP, HTTPS, HTTP/2 CONNECT, SOCKS5, SOCKS5-TLS, AnyTLS, TrustTunnel, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
+- [x] mihomo(Clash.Meta) Compatible (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, sudoku, AnyTLS, MASQUE, Tailscale, GOST Relay)
+
+Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
+
+- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms
-- [x] QX
-- [x] Loon
+
+- [x] Plain JSON
+- [x] Stash
+- [x] Clash.Meta(mihomo)
+- [x] Surfboard
- [x] Surge
+- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
+- [x] Loon
+- [x] Egern
+- [x] Shadowrocket
+- [x] QX
+- [x] sing-box
+- [x] V2Ray
+- [x] V2Ray URI
+
+Deprecated:
+
+- [x] Clash
## 2. Subscription Formatting
+
### Filtering
-- [x] **Keyword filter**
-- [x] **Discard keywords filter**
+
- [x] **Regex filter**
- [x] **Discard regex filter**
- [x] **Region filter**
@@ -32,13 +113,59 @@ Core functionality:
- [x] **Script filter**
### Proxy Operations
+
- [x] **Set property operator**: set some proxy properties such as `udp`,`tfo`, `skip-cert-verify` etc.
- [x] **Flag operator**: add flags or remove flags for proxies.
- [x] **Sort operator**: sort proxies by name.
-- [x] **Keyword sort operator**: sort proxies by keywords (fallback to normal sort).
-- [x] **Keyword rename operator**: replace by keywords in proxy names.
-- [x] **Keyword delete operator**: delete by keywords in proxy names.
+- [x] **Regex sort operator**: sort proxies by keywords (fallback to normal sort).
- [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script.
+- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
+
+### Development
+
+Install `pnpm`
+
+Go to `backend` directories, install node dependencies:
+
+```
+pnpm i
+```
+
+```
+SUB_STORE_BACKEND_API_PORT=3000 pnpm esbuild:dev
+```
+
+or this one if you're using `Termux`
+
+```
+SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
+```
+
+### Build
+
+```
+pnpm bundle:esbuild
+```
+
+## LICENSE
+
+This project is under the GPL V3 LICENSE.
+
+[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
+
+## Star History
+
+[](https://star-history.com/#sub-store-org/sub-store&Date)
+
+## Acknowledgements
+
+- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
+- Special thanks to @Orz-3 and @58xinian for their awesome icons.
+
+## Sponsors
+
+[](https://yxvm.com)
+[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project.
diff --git a/backend/.babelrc b/backend/.babelrc
new file mode 100644
index 0000000000..d45171f444
--- /dev/null
+++ b/backend/.babelrc
@@ -0,0 +1,27 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env"
+ ]
+ ],
+ "env": {
+ "test": {
+ "presets": [
+ "@babel/preset-env"
+ ]
+ }
+ },
+ "plugins": [
+ [
+ "babel-plugin-relative-path-import",
+ {
+ "paths": [
+ {
+ "rootPathPrefix": "@",
+ "rootPathSuffix": "src"
+ }
+ ]
+ }
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json
new file mode 100644
index 0000000000..919fea0ca9
--- /dev/null
+++ b/backend/.eslintrc.json
@@ -0,0 +1,15 @@
+{
+ "ignorePatterns": ["*.min.js", "src/vendor/*.js"],
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "node": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ }
+}
diff --git a/backend/.prettierrc.json b/backend/.prettierrc.json
new file mode 100644
index 0000000000..9848721705
--- /dev/null
+++ b/backend/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "singleQuote": true,
+ "trailingComma": "all",
+ "tabWidth": 4,
+ "bracketSpacing": true
+}
diff --git a/backend/banner b/backend/banner
new file mode 100644
index 0000000000..6356592313
--- /dev/null
+++ b/backend/banner
@@ -0,0 +1,15 @@
+/**
+ * ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
+ * ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
+ * ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
+ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
+ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
+ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
+ * Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!
+ * @updated: <%= updated %>
+ * @version: <%= pkg.version %>
+ * @author: Peng-YM
+ * @github: https://github.com/sub-store-org/Sub-Store
+ * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
+ */
+
diff --git a/backend/bundle-esbuild.js b/backend/bundle-esbuild.js
new file mode 100644
index 0000000000..aab76b70d2
--- /dev/null
+++ b/backend/bundle-esbuild.js
@@ -0,0 +1,96 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+const { build } = require('esbuild');
+
+!(async () => {
+ const version = JSON.parse(
+ fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
+ ).version.trim();
+
+ const artifacts = [
+ { src: 'src/main.js', dest: 'sub-store.min.js' },
+ {
+ src: 'src/products/resource-parser.loon.js',
+ dest: 'dist/sub-store-parser.loon.min.js',
+ },
+ {
+ src: 'src/products/cron-sync-artifacts.js',
+ dest: 'dist/cron-sync-artifacts.min.js',
+ },
+ { src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },
+ { src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },
+ ];
+
+ for await (const artifact of artifacts) {
+ await build({
+ entryPoints: [artifact.src],
+ bundle: true,
+ minify: true,
+ sourcemap: false,
+ platform: 'browser',
+ format: 'iife',
+ outfile: artifact.dest,
+ });
+ }
+
+ const browserEsmArtifacts = [
+ {
+ src: 'src/products/proxy-utils.esm.js',
+ dest: 'dist/proxy-utils.esm.mjs',
+ },
+ ];
+
+ for await (const artifact of browserEsmArtifacts) {
+ await build({
+ entryPoints: [artifact.src],
+ bundle: true,
+ minify: true,
+ sourcemap: false,
+ platform: 'browser',
+ format: 'esm',
+ outfile: artifact.dest,
+ });
+ }
+
+ let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
+ encoding: 'utf8',
+ });
+ content = content.replace(
+ /eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
+ '$2',
+ );
+ fs.writeFileSync(
+ path.join(__dirname, 'dist/sub-store.no-bundle.js'),
+ content,
+ {
+ encoding: 'utf8',
+ },
+ );
+
+ await build({
+ entryPoints: ['dist/sub-store.no-bundle.js'],
+ bundle: true,
+ minify: true,
+ sourcemap: false,
+ platform: 'node',
+ format: 'cjs',
+ outfile: 'dist/sub-store.bundle.js',
+ });
+ fs.writeFileSync(
+ path.join(__dirname, 'dist/sub-store.bundle.js'),
+ `// SUB_STORE_BACKEND_VERSION: ${version}
+${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
+ encoding: 'utf8',
+})}`,
+ {
+ encoding: 'utf8',
+ },
+ );
+})()
+ .catch((e) => {
+ console.log(e);
+ })
+ .finally(() => {
+ console.log('done');
+ });
diff --git a/backend/dev-esbuild.js b/backend/dev-esbuild.js
new file mode 100644
index 0000000000..63b4c2ee6a
--- /dev/null
+++ b/backend/dev-esbuild.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+const { build } = require('esbuild');
+
+!(async () => {
+ const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }];
+
+ for await (const artifact of artifacts) {
+ await build({
+ entryPoints: [artifact.src],
+ bundle: true,
+ minify: false,
+ sourcemap: false,
+ platform: 'node',
+ format: 'cjs',
+ outfile: artifact.dest,
+ logOverride: {
+ 'direct-eval': 'silent',
+ },
+ });
+ }
+})()
+ .catch((e) => {
+ console.log(e);
+ })
+ .finally(() => {
+ console.log('done');
+ });
diff --git a/backend/dist/.gitkeep b/backend/dist/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/esbuild-dev.js b/backend/esbuild-dev.js
new file mode 100644
index 0000000000..4bb60596c4
--- /dev/null
+++ b/backend/esbuild-dev.js
@@ -0,0 +1,148 @@
+#!/usr/bin/env node
+const path = require('path');
+const { spawn } = require('child_process');
+const { context } = require('esbuild');
+
+const outfile = path.join(__dirname, 'sub-store.min.js');
+
+let serverProcess = null;
+let buildContext = null;
+let shuttingDown = false;
+let restartQueue = Promise.resolve();
+
+function log(message) {
+ console.log(`[dev:esbuild] ${message}`);
+}
+
+function startServer() {
+ const child = spawn(process.execPath, [outfile], {
+ cwd: __dirname,
+ stdio: 'inherit',
+ });
+
+ serverProcess = child;
+ log(`server started (pid ${child.pid})`);
+
+ child.on('exit', (code, signal) => {
+ if (serverProcess === child) {
+ serverProcess = null;
+ }
+
+ if (!shuttingDown && signal !== 'SIGTERM') {
+ log(
+ `server exited${code === null ? '' : ` with code ${code}`}${
+ signal ? ` (${signal})` : ''
+ }`,
+ );
+ }
+ });
+}
+
+async function stopServer() {
+ const child = serverProcess;
+ if (!child) {
+ return;
+ }
+
+ serverProcess = null;
+
+ if (child.exitCode !== null || child.signalCode !== null) {
+ return;
+ }
+
+ await new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ child.kill('SIGKILL');
+ }, 3000);
+
+ child.once('exit', () => {
+ clearTimeout(timeout);
+ resolve();
+ });
+
+ child.kill('SIGTERM');
+ });
+}
+
+async function restartServer() {
+ if (serverProcess) {
+ log('restarting server');
+ await stopServer();
+ }
+
+ if (!shuttingDown) {
+ startServer();
+ }
+}
+
+async function shutdown(signal) {
+ if (shuttingDown) {
+ return;
+ }
+
+ shuttingDown = true;
+ log(`received ${signal}, shutting down`);
+
+ if (buildContext) {
+ await buildContext.dispose();
+ }
+
+ await restartQueue.catch((error) => {
+ console.error(error);
+ });
+ await stopServer();
+}
+
+!(async () => {
+ buildContext = await context({
+ entryPoints: ['src/main.js'],
+ bundle: true,
+ minify: false,
+ sourcemap: false,
+ platform: 'node',
+ format: 'cjs',
+ outfile,
+ logOverride: {
+ 'direct-eval': 'silent',
+ },
+ plugins: [
+ {
+ name: 'restart-server-on-build',
+ setup(build) {
+ build.onStart(() => {
+ log('building');
+ });
+
+ build.onEnd((result) => {
+ if (result.errors.length > 0) {
+ log(`build failed with ${result.errors.length} error(s)`);
+ return;
+ }
+
+ log('build succeeded');
+ restartQueue = restartQueue
+ .catch((error) => {
+ console.error(error);
+ })
+ .then(() => restartServer());
+ return restartQueue;
+ });
+ },
+ },
+ ],
+ });
+
+ process.on('SIGINT', () => {
+ shutdown('SIGINT').finally(() => process.exit(0));
+ });
+ process.on('SIGTERM', () => {
+ shutdown('SIGTERM').finally(() => process.exit(0));
+ });
+
+ await buildContext.watch();
+ log('watching for changes');
+})()
+ .catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+ });
diff --git a/backend/jsconfig.json b/backend/jsconfig.json
new file mode 100644
index 0000000000..df83de409a
--- /dev/null
+++ b/backend/jsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/package.json b/backend/package.json
index c5780687ec..aa9d5da0fc 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,11 +1,63 @@
{
- "name": "sub-store-backend",
- "version": "0.0.1",
- "description": "Advanced Subscription Manager for QX, Loon, and Surge.",
- "main": "sub-store.js",
+ "name": "sub-store",
+ "version": "2.23.20",
+ "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
+ "main": "src/main.js",
+ "packageManager": "pnpm@11.0.9",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "preinstall": "npx only-allow pnpm",
+ "test": "mocha src/test/**/*.spec.js --require @babel/register --recursive",
+ "serve": "node sub-store.min.js",
+ "start": "nodemon -w src -w package.json --exec babel-node src/main.js",
+ "dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
+ "dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
+ "esbuild:dev": "node esbuild-dev.js",
+ "bundle:esbuild": "node bundle-esbuild.js",
+ "changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
},
- "author": "",
- "license": "GPL"
-}
+ "author": "Peng-YM",
+ "license": "GPL-3.0",
+ "pnpm": {
+ "patchedDependencies": {
+ "http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch"
+ }
+ },
+ "dependencies": {
+ "@maxmind/geoip2-node": "^5.0.0",
+ "body-parser": "^1.19.0",
+ "buffer": "^6.0.3",
+ "connect-history-api-fallback": "^2.0.0",
+ "cron": "^3.1.6",
+ "dns-packet": "^5.6.1",
+ "dotenv": "^16.4.7",
+ "express": "^4.17.1",
+ "fastestsmallesttextencoderdecoder": "^1.0.22",
+ "fetch-socks": "^1.3.2",
+ "http-proxy-middleware": "^3.0.3",
+ "ip-address": "^9.0.5",
+ "js-base64": "^3.7.2",
+ "json5": "^2.2.3",
+ "jsrsasign": "^11.1.0",
+ "lodash": "^4.17.21",
+ "mime-types": "^2.1.35",
+ "ms": "^2.1.3",
+ "nanoid": "^3.3.3",
+ "semver": "^7.6.3",
+ "static-js-yaml": "^1.0.0",
+ "undici": "^7.4.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.18.0",
+ "@babel/node": "^7.17.10",
+ "@babel/preset-env": "^7.18.0",
+ "@babel/register": "^7.17.7",
+ "babel-plugin-relative-path-import": "^2.0.1",
+ "chai": "^4.3.6",
+ "esbuild": "^0.19.8",
+ "eslint": "^8.16.0",
+ "mocha": "^11.7.5",
+ "nodemon": "^2.0.16",
+ "peggy": "^2.0.1",
+ "prettier": "2.6.2"
+ }
+}
\ No newline at end of file
diff --git a/backend/patches/http-proxy@1.18.1.patch b/backend/patches/http-proxy@1.18.1.patch
new file mode 100644
index 0000000000..04e1791370
--- /dev/null
+++ b/backend/patches/http-proxy@1.18.1.patch
@@ -0,0 +1,46 @@
+diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js
+index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644
+--- a/lib/http-proxy/common.js
++++ b/lib/http-proxy/common.js
+@@ -1,6 +1,5 @@
+ var common = exports,
+ url = require('url'),
+- extend = require('util')._extend,
+ required = require('requires-port');
+
+ var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i,
+@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) {
+ );
+
+ outgoing.method = options.method || req.method;
+- outgoing.headers = extend({}, req.headers);
++ outgoing.headers = Object.assign({}, req.headers);
+
+ if (options.headers){
+- extend(outgoing.headers, options.headers);
++ Object.assign(outgoing.headers, options.headers);
+ }
+
+ if (options.auth) {
+diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js
+index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644
+--- a/lib/http-proxy/index.js
++++ b/lib/http-proxy/index.js
+@@ -1,5 +1,4 @@
+ var httpProxy = module.exports,
+- extend = require('util')._extend,
+ parse_url = require('url').parse,
+ EE3 = require('eventemitter3'),
+ http = require('http'),
+@@ -47,9 +46,9 @@ function createRightProxy(type) {
+ args[cntr] !== res
+ ) {
+ //Copy global options
+- requestOptions = extend({}, options);
++ requestOptions = Object.assign({}, options);
+ //Overwrite with request options
+- extend(requestOptions, args[cntr]);
++ Object.assign(requestOptions, args[cntr]);
+
+ cntr--;
+ }
diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml
new file mode 100644
index 0000000000..81e188d21a
--- /dev/null
+++ b/backend/pnpm-lock.yaml
@@ -0,0 +1,5912 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@maxmind/geoip2-node':
+ specifier: ^5.0.0
+ version: 5.0.0
+ body-parser:
+ specifier: ^1.19.0
+ version: 1.20.5
+ buffer:
+ specifier: ^6.0.3
+ version: 6.0.3
+ connect-history-api-fallback:
+ specifier: ^2.0.0
+ version: 2.0.0
+ cron:
+ specifier: ^3.1.6
+ version: 3.1.6
+ dns-packet:
+ specifier: ^5.6.1
+ version: 5.6.1
+ dotenv:
+ specifier: ^16.4.7
+ version: 16.4.7
+ express:
+ specifier: ^4.17.1
+ version: 4.22.1
+ fastestsmallesttextencoderdecoder:
+ specifier: ^1.0.22
+ version: 1.0.22
+ fetch-socks:
+ specifier: ^1.3.2
+ version: 1.3.3
+ http-proxy-middleware:
+ specifier: ^3.0.3
+ version: 3.0.3
+ ip-address:
+ specifier: ^9.0.5
+ version: 9.0.5
+ js-base64:
+ specifier: ^3.7.2
+ version: 3.7.8
+ json5:
+ specifier: ^2.2.3
+ version: 2.2.3
+ jsrsasign:
+ specifier: ^11.1.0
+ version: 11.1.3
+ lodash:
+ specifier: ^4.17.21
+ version: 4.18.1
+ mime-types:
+ specifier: ^2.1.35
+ version: 2.1.35
+ ms:
+ specifier: ^2.1.3
+ version: 2.1.3
+ nanoid:
+ specifier: ^3.3.3
+ version: 3.3.12
+ semver:
+ specifier: ^7.6.3
+ version: 7.8.0
+ static-js-yaml:
+ specifier: ^1.0.0
+ version: 1.0.0
+ undici:
+ specifier: ^7.4.0
+ version: 7.25.0
+ devDependencies:
+ '@babel/core':
+ specifier: ^7.18.0
+ version: 7.29.0
+ '@babel/node':
+ specifier: ^7.17.10
+ version: 7.29.0(@babel/core@7.29.0)
+ '@babel/preset-env':
+ specifier: ^7.18.0
+ version: 7.29.5(@babel/core@7.29.0)
+ '@babel/register':
+ specifier: ^7.17.7
+ version: 7.29.3(@babel/core@7.29.0)
+ babel-plugin-relative-path-import:
+ specifier: ^2.0.1
+ version: 2.0.1
+ chai:
+ specifier: ^4.3.6
+ version: 4.3.6
+ esbuild:
+ specifier: ^0.19.8
+ version: 0.19.8
+ eslint:
+ specifier: ^8.16.0
+ version: 8.16.0
+ mocha:
+ specifier: ^11.7.5
+ version: 11.7.5
+ nodemon:
+ specifier: ^2.0.16
+ version: 2.0.16
+ peggy:
+ specifier: ^2.0.1
+ version: 2.0.1
+ prettier:
+ specifier: 2.6.2
+ version: 2.6.2
+
+packages:
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.3':
+ resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.18.6':
+ resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.1':
+ resolution: {integrity: sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.29.3':
+ resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.18.6':
+ resolution: {integrity: sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.27.1':
+ resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5':
+ resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-define-polyfill-provider@0.6.8':
+ resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-remap-async-to-generator@7.27.1':
+ resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-wrap-function@7.28.6':
+ resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.2':
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/node@7.29.0':
+ resolution: {integrity: sha512-9UeU8F3rx2lOZXneEW2HTnTYdA8+fXP0kr54tk7d0fPomWNlZ6WJ2H9lunr5dSvr8FNY0CDnop3Km6jZ5NAUsQ==}
+ engines: {node: '>=6.9.0'}
+ hasBin: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/parser@7.29.3':
+ resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5':
+ resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1':
+ resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1':
+ resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3':
+ resolution: {integrity: sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1':
+ resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6':
+ resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
+ resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-assertions@7.28.6':
+ resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.28.6':
+ resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6':
+ resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-arrow-functions@7.27.1':
+ resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0':
+ resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-to-generator@7.28.6':
+ resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoped-functions@7.27.1':
+ resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoping@7.28.6':
+ resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.28.6':
+ resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-static-block@7.28.6':
+ resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+
+ '@babel/plugin-transform-classes@7.28.6':
+ resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-computed-properties@7.28.6':
+ resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.28.5':
+ resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-dotall-regex@7.28.6':
+ resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-keys@7.27.1':
+ resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-dynamic-import@7.27.1':
+ resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-explicit-resource-management@7.28.6':
+ resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-exponentiation-operator@7.28.6':
+ resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1':
+ resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-for-of@7.27.1':
+ resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-function-name@7.27.1':
+ resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-json-strings@7.28.6':
+ resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-literals@7.27.1':
+ resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6':
+ resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-member-expression-literals@7.27.1':
+ resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-amd@7.27.1':
+ resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-systemjs@7.29.4':
+ resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-umd@7.27.1':
+ resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-new-target@7.27.1':
+ resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6':
+ resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-numeric-separator@7.28.6':
+ resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6':
+ resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-super@7.27.1':
+ resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6':
+ resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.28.6':
+ resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-parameters@7.27.7':
+ resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-methods@7.28.6':
+ resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6':
+ resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-property-literals@7.27.1':
+ resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regenerator@7.29.0':
+ resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regexp-modifiers@7.28.6':
+ resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-reserved-words@7.27.1':
+ resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1':
+ resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-spread@7.28.6':
+ resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-sticky-regex@7.27.1':
+ resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-template-literals@7.27.1':
+ resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typeof-symbol@7.27.1':
+ resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-escapes@7.27.1':
+ resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-property-regex@7.28.6':
+ resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.27.1':
+ resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6':
+ resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/preset-env@7.29.5':
+ resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-modules@0.1.6-no-external-plugins':
+ resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
+
+ '@babel/register@7.29.3':
+ resolution: {integrity: sha512-F6C1KpIdoImKQfsD6HSxZ+mS4YY/2Q+JsqrmTC5ApVkTR2rG+nnbpjhWwzA5bDNu8mJjB3AryqDaWFLd4gCbJQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/regjsgen@0.8.0':
+ resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/android-arm64@0.19.8':
+ resolution: {integrity: sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.19.8':
+ resolution: {integrity: sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.19.8':
+ resolution: {integrity: sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.19.8':
+ resolution: {integrity: sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.19.8':
+ resolution: {integrity: sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.19.8':
+ resolution: {integrity: sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.19.8':
+ resolution: {integrity: sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.19.8':
+ resolution: {integrity: sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.19.8':
+ resolution: {integrity: sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.19.8':
+ resolution: {integrity: sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.19.8':
+ resolution: {integrity: sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.19.8':
+ resolution: {integrity: sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.19.8':
+ resolution: {integrity: sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.19.8':
+ resolution: {integrity: sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.19.8':
+ resolution: {integrity: sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.19.8':
+ resolution: {integrity: sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-x64@0.19.8':
+ resolution: {integrity: sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-x64@0.19.8':
+ resolution: {integrity: sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.19.8':
+ resolution: {integrity: sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.19.8':
+ resolution: {integrity: sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.19.8':
+ resolution: {integrity: sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.19.8':
+ resolution: {integrity: sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint/eslintrc@1.3.0':
+ resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ '@humanwhocodes/config-array@0.9.5':
+ resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==}
+ engines: {node: '>=10.10.0'}
+ deprecated: Use @eslint/config-array instead
+
+ '@humanwhocodes/object-schema@1.2.1':
+ resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
+ deprecated: Use @eslint/object-schema instead
+
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@leichtgewicht/ip-codec@2.0.5':
+ resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
+
+ '@maxmind/geoip2-node@5.0.0':
+ resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
+ '@sindresorhus/is@0.14.0':
+ resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==}
+ engines: {node: '>=6'}
+
+ '@szmarczak/http-timer@1.1.2':
+ resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==}
+ engines: {node: '>=6'}
+
+ '@types/http-proxy@1.17.17':
+ resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
+
+ '@types/keyv@3.1.4':
+ resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
+
+ '@types/luxon@3.3.8':
+ resolution: {integrity: sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==}
+
+ '@types/node@25.6.2':
+ resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==}
+
+ '@types/responselike@1.0.3':
+ resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
+
+ accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@7.1.1:
+ resolution: {integrity: sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ amdefine@1.0.1:
+ resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
+ engines: {node: '>=0.4.2'}
+
+ ansi-align@3.0.1:
+ resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-flatten@1.1.1:
+ resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+
+ array.prototype.reduce@1.0.8:
+ resolution: {integrity: sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
+ assert-plus@1.0.0:
+ resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
+ engines: {node: '>=0.8'}
+
+ assertion-error@1.1.0:
+ resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ babel-plugin-polyfill-corejs2@0.4.17:
+ resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-corejs3@0.14.2:
+ resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.8:
+ resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-relative-path-import@2.0.1:
+ resolution: {integrity: sha512-jOtB/Lef7QFNAEGRV4VmUvP/VsizpvOVonrTeTTE8TxVIR0FL/wJTzFQp4ei5Jf5+EWklFORmnsXOjLxvNVeRg==}
+
+ balanced-match@1.0.0:
+ resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ baseline-browser-mapping@2.10.29:
+ resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ body-parser@1.20.5:
+ resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
+ boxen@5.1.2:
+ resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
+ engines: {node: '>=10'}
+
+ brace-expansion@1.1.14:
+ resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
+
+ brace-expansion@2.1.0:
+ resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browser-stdout@1.3.1:
+ resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
+
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ cacheable-request@6.0.0:
+ resolution: {integrity: sha512-2N7AmszH/WPPpl5Z3XMw1HAP+8d+xugnKQAeKvxFZ/04dbT/CAznqwbl+7eSr3HkwdepNwtb2yx3CAMQWvG01Q==}
+ engines: {node: '>=8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.9:
+ resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
+ caniuse-lite@1.0.30001792:
+ resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
+
+ chai@4.3.6:
+ resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==}
+ engines: {node: '>=4'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ check-error@1.0.2:
+ resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
+
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ ci-info@2.0.0:
+ resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
+
+ cli-boxes@2.2.1:
+ resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
+ engines: {node: '>=6'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clone-deep@4.0.1:
+ resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==}
+ engines: {node: '>=6'}
+
+ clone-response@1.0.3:
+ resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ commander@6.2.0:
+ resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==}
+ engines: {node: '>= 6'}
+
+ commander@9.3.0:
+ resolution: {integrity: sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==}
+ engines: {node: ^12.20.0 || >=14}
+
+ commondir@1.0.1:
+ resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ concat-stream@1.6.2:
+ resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
+ engines: {'0': node >= 0.8}
+
+ configstore@5.0.1:
+ resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==}
+ engines: {node: '>=8'}
+
+ connect-history-api-fallback@2.0.0:
+ resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==}
+ engines: {node: '>=0.8'}
+
+ content-disposition@0.5.4:
+ resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
+ engines: {node: '>= 0.6'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie-signature@1.0.7:
+ resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ core-js-compat@3.49.0:
+ resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
+
+ core-js@3.49.0:
+ resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
+
+ core-util-is@1.0.2:
+ resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
+ cron@3.1.6:
+ resolution: {integrity: sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ crypto-random-string@2.0.0:
+ resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
+ engines: {node: '>=8'}
+
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
+ debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@3.2.7:
+ resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decamelize@4.0.0:
+ resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
+ engines: {node: '>=10'}
+
+ decompress-response@3.3.0:
+ resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==}
+ engines: {node: '>=4'}
+
+ deep-eql@3.0.1:
+ resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==}
+ engines: {node: '>=0.12'}
+
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ defer-to-connect@1.1.3:
+ resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
+ diff@7.0.0:
+ resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==}
+ engines: {node: '>=0.3.1'}
+
+ dns-packet@5.6.1:
+ resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
+ engines: {node: '>=6'}
+
+ doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+
+ dot-prop@5.3.0:
+ resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
+ engines: {node: '>=8'}
+
+ dotenv@16.4.7:
+ resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
+ engines: {node: '>=12'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ duplexer2@0.0.2:
+ resolution: {integrity: sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==}
+
+ duplexer3@0.1.5:
+ resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
+
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ electron-to-chromium@1.5.353:
+ resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+ es-abstract@1.24.2:
+ resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==}
+ engines: {node: '>= 0.4'}
+
+ es-array-method-boxes-properly@1.0.0:
+ resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.0:
+ resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.19.8:
+ resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-goat@2.1.1:
+ resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==}
+ engines: {node: '>=8'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ escodegen@0.0.28:
+ resolution: {integrity: sha512-6ioQhg16lFs5c7XJlJFXIDxBjO4yRvXC9yK6dLNNGuhI3a/fJukHanPF6qtpjGDgAFzI8Wuq3PSIarWmaOq/5A==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ escodegen@1.3.3:
+ resolution: {integrity: sha512-z9FWgKc48wjMlpzF5ymKS1AF8OIgnKLp9VyN7KbdtyrP/9lndwUFqCtMm+TAJmJf7KJFFYc4cFJfVTTGkKEwsA==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+
+ eslint-scope@7.1.1:
+ resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-utils@3.0.0:
+ resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
+ engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
+ peerDependencies:
+ eslint: '>=5'
+
+ eslint-visitor-keys@2.0.0:
+ resolution: {integrity: sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==}
+ engines: {node: '>=10'}
+
+ eslint-visitor-keys@3.3.0:
+ resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint@8.16.0:
+ resolution: {integrity: sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+ hasBin: true
+
+ espree@9.3.2:
+ resolution: {integrity: sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ esprima@1.0.4:
+ resolution: {integrity: sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ esprima@1.1.1:
+ resolution: {integrity: sha512-qxxB994/7NtERxgXdFgLHIs9M6bhLXc6qtUmWZ3L8+gTQ9qaoyki2887P2IqAYsoENyr8SUbTutStDniOHSDHg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@1.3.2:
+ resolution: {integrity: sha512-OkbCPVUu8D9tbsLcUR+CKFRBbhZlogmkbWaP3BPERlkqzWL5Q6IdTz6eUk+b5cid2MTaCqJb2nNRGoJ8TpfPrg==}
+ engines: {node: '>=0.4.0'}
+
+ estraverse@1.5.1:
+ resolution: {integrity: sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ==}
+ engines: {node: '>=0.4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@1.0.0:
+ resolution: {integrity: sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==}
+ engines: {node: '>=0.10.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ eventemitter3@4.0.0:
+ resolution: {integrity: sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==}
+
+ express@4.22.1:
+ resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
+ engines: {node: '>= 0.10.0'}
+
+ extsprintf@1.3.0:
+ resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
+ engines: {'0': node >=0.6.0}
+
+ falafel@2.2.5:
+ resolution: {integrity: sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==}
+ engines: {node: '>=0.4.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastestsmallesttextencoderdecoder@1.0.22:
+ resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
+
+ fetch-socks@1.3.3:
+ resolution: {integrity: sha512-jI22jzTf7x3GAcUoZARwuxKBPotb3PFkemVWqxlF2qV55FhRytRJrn95HeLoUMq2vkIbY3aQqNYytvZKqUoMaQ==}
+
+ file-entry-cache@6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ finalhandler@1.3.2:
+ resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
+ engines: {node: '>= 0.8'}
+
+ find-cache-dir@2.1.0:
+ resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
+ engines: {node: '>=6'}
+
+ find-up@3.0.0:
+ resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
+ engines: {node: '>=6'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@3.0.4:
+ resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+
+ flat@5.0.2:
+ resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
+ hasBin: true
+
+ flatted@3.4.2:
+ resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+
+ follow-redirects@1.16.0:
+ resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
+ foreground-child@3.1.0:
+ resolution: {integrity: sha512-lXeSPRCndWPaipZbtI4CkvTZpF6OPsy19dkvf7+5AHeJD+w+iAKPc9Q78xWBmX4SdR+8xrtY9jTXs/YDv8q+Ug==}
+ engines: {node: '>=14'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ function.prototype.name@1.1.8:
+ resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+ engines: {node: '>= 0.4'}
+
+ functional-red-black-tree@1.0.1:
+ resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+ generator-function@2.0.1:
+ resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
+ engines: {node: '>= 0.4'}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-func-name@2.0.2:
+ resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-stream@4.1.0:
+ resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
+ engines: {node: '>=6'}
+
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
+ glob@7.2.0:
+ resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+
+ global-dirs@3.0.1:
+ resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
+ engines: {node: '>=10'}
+
+ globals@13.15.0:
+ resolution: {integrity: sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==}
+ engines: {node: '>=8'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ got@9.6.0:
+ resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==}
+ engines: {node: '>=8.6'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
+ has-flag@3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ has-yarn@2.1.0:
+ resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==}
+ engines: {node: '>=8'}
+
+ has@1.0.4:
+ resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==}
+ engines: {node: '>= 0.4.0'}
+
+ hasown@2.0.3:
+ resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
+ engines: {node: '>= 0.4'}
+
+ he@1.2.0:
+ resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+ hasBin: true
+
+ homedir-polyfill@1.0.3:
+ resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
+ engines: {node: '>=0.10.0'}
+
+ http-cache-semantics@4.2.0:
+ resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
+
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
+ engines: {node: '>= 0.8'}
+
+ http-proxy-middleware@3.0.3:
+ resolution: {integrity: sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ http-proxy@1.18.1:
+ resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
+ engines: {node: '>=8.0.0'}
+
+ iconv-lite@0.4.24:
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+ engines: {node: '>=0.10.0'}
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ ignore-by-default@1.0.1:
+ resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.0.0:
+ resolution: {integrity: sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==}
+ engines: {node: '>=6'}
+
+ import-fresh@3.2.1:
+ resolution: {integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==}
+ engines: {node: '>=6'}
+
+ import-lazy@2.1.0:
+ resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==}
+ engines: {node: '>=4'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.0:
+ resolution: {integrity: sha512-6tyfJkFAmQV64x9Li007PNZgNxqRywig4Rv8PDfzU9kgDBmURaIvjSL2wKyx45LPSIXsvDkFGbOMqMHC1PsORA==}
+ deprecated: Please update to ini >=1.3.6 to avoid a prototype pollution issue
+
+ ini@2.0.0:
+ resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
+ engines: {node: '>=10'}
+
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ ip-address@10.2.0:
+ resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
+ engines: {node: '>= 12'}
+
+ ip-address@9.0.5:
+ resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
+ engines: {node: '>= 12'}
+
+ ip6addr@0.2.5:
+ resolution: {integrity: sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-ci@2.0.0:
+ resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==}
+ hasBin: true
+
+ is-core-module@2.16.2:
+ resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-generator-function@1.1.2:
+ resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-installed-globally@0.4.0:
+ resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==}
+ engines: {node: '>=10'}
+
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
+ is-npm@5.0.0:
+ resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==}
+ engines: {node: '>=10'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-obj@2.0.0:
+ resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
+ engines: {node: '>=8'}
+
+ is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+
+ is-plain-obj@2.1.0:
+ resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+ engines: {node: '>=8'}
+
+ is-plain-object@2.0.4:
+ resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
+ engines: {node: '>=0.10.0'}
+
+ is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-typedarray@1.0.0:
+ resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
+
+ is-unicode-supported@0.1.0:
+ resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+ engines: {node: '>=10'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ is-yarn-global@0.3.0:
+ resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==}
+
+ isarray@0.0.1:
+ resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
+
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isobject@3.0.1:
+ resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
+ engines: {node: '>=0.10.0'}
+
+ jackspeak@3.1.2:
+ resolution: {integrity: sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==}
+ engines: {node: '>=14'}
+
+ js-base64@3.7.8:
+ resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@3.14.2:
+ resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
+ hasBin: true
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsbn@1.1.0:
+ resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
+
+ jsesc@0.5.0:
+ resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.0:
+ resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema@0.4.0:
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsprim@2.0.2:
+ resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
+ engines: {'0': node >=0.6.0}
+
+ jsrsasign@11.1.3:
+ resolution: {integrity: sha512-nPnK5D/4lv0Dwr7TlzrKtAd8JlLZwFTqTUUB3NQCbtdobcRcohGFxjbPySDVh74iWUudcCsapYT6OxoyhJLhhA==}
+
+ keyv@3.0.0:
+ resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
+ latest-version@5.1.0:
+ resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==}
+ engines: {node: '>=8'}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@3.0.0:
+ resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
+ engines: {node: '>=6'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
+ log-symbols@4.1.0:
+ resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+ engines: {node: '>=10'}
+
+ loupe@2.3.7:
+ resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+
+ lowercase-keys@1.0.1:
+ resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==}
+ engines: {node: '>=0.10.0'}
+
+ lru-cache@10.2.0:
+ resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
+ engines: {node: 14 || >=16.14}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ luxon@3.4.4:
+ resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
+ engines: {node: '>=12'}
+
+ make-dir@2.1.0:
+ resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
+ engines: {node: '>=6'}
+
+ make-dir@3.1.0:
+ resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+ engines: {node: '>=8'}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ maxmind@4.2.0:
+ resolution: {integrity: sha512-TADiE11Q10IjvLtlo05tTD52xLqfCJMhE3eYJHmpYIKg668STi/fQZGH9X3FpqpIP/2WPgKFxf899awFvfMtQA==}
+ engines: {node: '>=10', npm: '>=6'}
+
+ media-typer@0.3.0:
+ resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ engines: {node: '>= 0.6'}
+
+ merge-descriptors@1.0.3:
+ resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
+
+ methods@1.1.2:
+ resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
+ engines: {node: '>= 0.6'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ mimic-response@1.0.1:
+ resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
+ engines: {node: '>=4'}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ minimatch@9.0.9:
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minimist@0.0.8:
+ resolution: {integrity: sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mocha@11.7.5:
+ resolution: {integrity: sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+
+ ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+
+ node-environment-flags@1.0.6:
+ resolution: {integrity: sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==}
+
+ node-releases@2.0.38:
+ resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
+
+ nodemon@2.0.16:
+ resolution: {integrity: sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w==}
+ engines: {node: '>=8.10.0'}
+ hasBin: true
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ normalize-url@3.1.0:
+ resolution: {integrity: sha512-y7nyoGsYRm0hUhAxKFabfkVIoD89c6K5q8GoVALjPEOWfWfnz+ZUUmaQaKUKaEed1Fr+AOXaoEO8Q9jpum719A==}
+ engines: {node: '>=6'}
+
+ object-inspect@0.4.0:
+ resolution: {integrity: sha512-8WvkvUZiKAjjsy/63rJjA7jw9uyF0CLVLjBKEfnPHE3Jxvs1LgwqL2OmJN+LliIX1vrzKW+AAu02Cc+xv27ncQ==}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@0.4.0:
+ resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.getownpropertydescriptors@2.1.9:
+ resolution: {integrity: sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==}
+ engines: {node: '>= 0.4'}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ own-keys@1.0.1:
+ resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+ engines: {node: '>= 0.4'}
+
+ p-cancelable@1.1.0:
+ resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==}
+ engines: {node: '>=6'}
+
+ p-limit@2.0.0:
+ resolution: {integrity: sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==}
+ engines: {node: '>=6'}
+
+ p-limit@3.0.2:
+ resolution: {integrity: sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==}
+ engines: {node: '>=10'}
+
+ p-locate@3.0.0:
+ resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
+ engines: {node: '>=6'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ package-json@6.5.0:
+ resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==}
+ engines: {node: '>=8'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-passwd@1.0.0:
+ resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
+ engines: {node: '>=0.10.0'}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-exists@3.0.0:
+ resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
+ engines: {node: '>=4'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ path-to-regexp@0.1.13:
+ resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
+
+ pathval@1.1.1:
+ resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+
+ peggy@2.0.1:
+ resolution: {integrity: sha512-mBqfmdUAOVn7RILpXTbcRBhLfTR4Go0SresSnivGDdRylBOyVFJncFiVyCNNpPWq8HmgeRleXHs/Go4o8kQVXA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ pify@4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+
+ pirates@4.0.7:
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
+ engines: {node: '>= 6'}
+
+ pkg-dir@3.0.0:
+ resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
+ engines: {node: '>=6'}
+
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prepend-http@2.0.0:
+ resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
+ engines: {node: '>=4'}
+
+ prettier@2.6.2:
+ resolution: {integrity: sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ process-nextick-args@2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ pstree.remy@1.1.8:
+ resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
+
+ pump@3.0.4:
+ resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ pupa@2.1.1:
+ resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
+ engines: {node: '>=8'}
+
+ qs@6.14.2:
+ resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
+ engines: {node: '>=0.6'}
+
+ qs@6.15.1:
+ resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
+ engines: {node: '>=0.6'}
+
+ quote-stream@0.0.0:
+ resolution: {integrity: sha512-m4VtvjAMx00wgAS6eOy50ZDat1EBQeFKBIrtF/oxUt0MenEI33y7runJcRiOihc+JBBIt2aFFJhILIh4e9shJA==}
+
+ randombytes@2.1.0:
+ resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@2.5.3:
+ resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
+ engines: {node: '>= 0.8'}
+
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
+ readable-stream@1.0.34:
+ resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==}
+
+ readable-stream@1.1.14:
+ resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
+
+ readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
+ regenerate-unicode-properties@10.2.2:
+ resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
+ engines: {node: '>=4'}
+
+ regenerate@1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+
+ regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
+ regexpp@3.2.0:
+ resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
+ engines: {node: '>=8'}
+
+ regexpu-core@5.3.2:
+ resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==}
+ engines: {node: '>=4'}
+
+ regexpu-core@6.4.0:
+ resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
+ engines: {node: '>=4'}
+
+ registry-auth-token@4.0.0:
+ resolution: {integrity: sha512-lpQkHxd9UL6tb3k/aHAVfnVtn+Bcs9ob5InuFLLEDqSqeq+AljB8GZW9xY0x7F+xYwEcjKe07nyoxzEYz6yvkw==}
+ engines: {node: '>=6.0.0'}
+
+ registry-url@5.1.0:
+ resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==}
+ engines: {node: '>=8'}
+
+ regjsgen@0.8.0:
+ resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
+
+ regjsparser@0.13.1:
+ resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==}
+ hasBin: true
+
+ regjsparser@0.9.1:
+ resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
+ hasBin: true
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ requires-port@1.0.0:
+ resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ responselike@1.0.2:
+ resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==}
+
+ rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
+ safe-array-concat@1.1.4:
+ resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
+ engines: {node: '>=0.4'}
+
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ semver-diff@3.1.1:
+ resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==}
+ engines: {node: '>=8'}
+
+ semver@5.6.0:
+ resolution: {integrity: sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==}
+ hasBin: true
+
+ semver@5.7.0:
+ resolution: {integrity: sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==}
+ hasBin: true
+
+ semver@5.7.1:
+ resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ hasBin: true
+
+ semver@6.0.0:
+ resolution: {integrity: sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==}
+ hasBin: true
+
+ semver@6.2.0:
+ resolution: {integrity: sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==}
+ hasBin: true
+
+ semver@6.3.0:
+ resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+ hasBin: true
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.8.0:
+ resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ send@0.19.2:
+ resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
+ engines: {node: '>= 0.8.0'}
+
+ serialize-javascript@6.0.2:
+ resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
+
+ serve-static@1.16.3:
+ resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
+ engines: {node: '>= 0.8.0'}
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ shallow-clone@3.0.1:
+ resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
+ engines: {node: '>=8'}
+
+ shallow-copy@0.0.1:
+ resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.1:
+ resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ slash@1.0.0:
+ resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==}
+ engines: {node: '>=0.10.0'}
+
+ smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+
+ socks@2.8.9:
+ resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+
+ source-map-generator@0.8.0:
+ resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==}
+ engines: {node: '>= 10'}
+
+ source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+ source-map@0.1.43:
+ resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==}
+ engines: {node: '>=0.8.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
+ sprintf-js@1.1.3:
+ resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
+
+ static-eval@0.2.4:
+ resolution: {integrity: sha512-6dWWPfa/0+1zULdQi7ssT5EQZHsGK8LygBzhE/HdafNCo4e/Ibt7vLPfxBw9VcdVV+t0ARtN4ZAJKtApVc0A5Q==}
+
+ static-js-yaml@1.0.0:
+ resolution: {integrity: sha512-SHd8UBXpEUeFEhexli7Nn1+fysyhU3LiQFKcwnqM3l5wzvJU6QoXg4cMtjjZjfc4ngRj934188v7FHgDyZ6U9A==}
+
+ static-module@1.5.0:
+ resolution: {integrity: sha512-XTj7pQOHT33l77lK/Pu8UXqzI44C6LYAqwAc9hLTTESHRqJAFudBpReuopFPpoRr5CtOoSmGfFQC6FPlbDnyCw==}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
+ string-width@4.2.0:
+ resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==}
+ engines: {node: '>=8'}
+
+ string-width@4.2.2:
+ resolution: {integrity: sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==}
+ engines: {node: '>=8'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
+ string_decoder@0.10.31:
+ resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
+
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
+ strip-ansi@6.0.0:
+ resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==}
+ engines: {node: '>=8'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ text-table@0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+
+ through2@0.4.1:
+ resolution: {integrity: sha512-AvsMUJD6YXayd4aFmYWyHMLKfcSLNhEgvEzzT+X2BK5f7BKrUT9foCDZDqY7ohNTFJv2nVWbYhGHX0fHgO+8nw==}
+
+ through2@0.6.5:
+ resolution: {integrity: sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==}
+
+ tiny-lru@7.0.6:
+ resolution: {integrity: sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==}
+ engines: {node: '>=6'}
+
+ to-readable-stream@1.0.0:
+ resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==}
+ engines: {node: '>=6'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ touch@3.1.1:
+ resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
+ hasBin: true
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ type-detect@4.1.0:
+ resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
+ engines: {node: '>=4'}
+
+ type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+
+ type-is@1.6.18:
+ resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+ engines: {node: '>= 0.6'}
+
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.4:
+ resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.7:
+ resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+ engines: {node: '>= 0.4'}
+
+ typedarray-to-buffer@3.1.5:
+ resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
+
+ typedarray@0.0.6:
+ resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
+ undefsafe@2.0.5:
+ resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
+
+ undici-types@7.19.2:
+ resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
+
+ undici@7.25.0:
+ resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
+ engines: {node: '>=20.18.1'}
+
+ unicode-canonical-property-names-ecmascript@2.0.1:
+ resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-ecmascript@2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-value-ecmascript@2.2.1:
+ resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
+ engines: {node: '>=4'}
+
+ unicode-property-aliases-ecmascript@2.2.0:
+ resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
+ engines: {node: '>=4'}
+
+ unique-string@2.0.0:
+ resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
+ engines: {node: '>=8'}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ update-notifier@5.1.0:
+ resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==}
+ engines: {node: '>=10'}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ url-parse-lax@3.0.0:
+ resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==}
+ engines: {node: '>=4'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ utils-merge@1.0.1:
+ resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+ engines: {node: '>= 0.4.0'}
+
+ v8-compile-cache@2.4.0:
+ resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
+
+ v8flags@3.2.0:
+ resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==}
+ engines: {node: '>= 0.10'}
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ verror@1.10.0:
+ resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
+ engines: {'0': node >=0.6.0}
+
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.1:
+ resolution: {integrity: sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ widest-line@3.1.0:
+ resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
+ engines: {node: '>=8'}
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ workerpool@9.2.0:
+ resolution: {integrity: sha512-PKZqBOCo6CYkVOwAxWxQaSF2Fvb5Iv2fCeTP7buyWI2GiynWr46NcXSgK/idoV6e60dgCBfgYc+Un3HMvmqP8w==}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ write-file-atomic@3.0.0:
+ resolution: {integrity: sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q==}
+
+ xdg-basedir@4.0.0:
+ resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==}
+ engines: {node: '>=8'}
+
+ xtend@2.1.2:
+ resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==}
+ engines: {node: '>=0.4'}
+
+ xtend@4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs-unparser@2.0.0:
+ resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
+ engines: {node: '>=10'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+snapshots:
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.3': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.29.2
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3(supports-color@8.1.1)
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.18.6':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-annotate-as-pure@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.29.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-create-regexp-features-plugin@7.18.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.18.6
+ regexpu-core: 5.3.2
+
+ '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.1
+ regexpu-core: 6.4.0
+ semver: 6.3.1
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ regexpu-core: 6.4.0
+ semver: 6.3.1
+
+ '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ debug: 4.4.3(supports-color@8.1.1)
+ lodash.debounce: 4.0.8
+ resolve: 1.22.12
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.1
+ '@babel/helper-wrap-function': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helper-wrap-function@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helpers@7.29.2':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/node@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/register': 7.29.3(@babel/core@7.29.0)
+ commander: 6.2.0
+ core-js: 3.49.0
+ node-environment-flags: 1.0.6
+ regenerator-runtime: 0.14.1
+ v8flags: 3.2.0
+
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+
+ '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.18.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-globals': 7.28.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/template': 7.28.6
+
+ '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/preset-env@7.29.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/core': 7.29.0
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0)
+ '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0)
+ babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0)
+ core-js-compat: 3.49.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/types': 7.29.0
+ esutils: 2.0.3
+
+ '@babel/register@7.29.3(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ clone-deep: 4.0.1
+ find-cache-dir: 2.1.0
+ make-dir: 2.1.0
+ pirates: 4.0.7
+ source-map-support: 0.5.21
+
+ '@babel/regjsgen@0.8.0': {}
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3(supports-color@8.1.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@esbuild/android-arm64@0.19.8':
+ optional: true
+
+ '@esbuild/android-arm@0.19.8':
+ optional: true
+
+ '@esbuild/android-x64@0.19.8':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.19.8':
+ optional: true
+
+ '@esbuild/darwin-x64@0.19.8':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.19.8':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.19.8':
+ optional: true
+
+ '@esbuild/linux-arm64@0.19.8':
+ optional: true
+
+ '@esbuild/linux-arm@0.19.8':
+ optional: true
+
+ '@esbuild/linux-ia32@0.19.8':
+ optional: true
+
+ '@esbuild/linux-loong64@0.19.8':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.19.8':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.19.8':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.19.8':
+ optional: true
+
+ '@esbuild/linux-s390x@0.19.8':
+ optional: true
+
+ '@esbuild/linux-x64@0.19.8':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.19.8':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.19.8':
+ optional: true
+
+ '@esbuild/sunos-x64@0.19.8':
+ optional: true
+
+ '@esbuild/win32-arm64@0.19.8':
+ optional: true
+
+ '@esbuild/win32-ia32@0.19.8':
+ optional: true
+
+ '@esbuild/win32-x64@0.19.8':
+ optional: true
+
+ '@eslint/eslintrc@1.3.0':
+ dependencies:
+ ajv: 6.15.0
+ debug: 4.4.3(supports-color@8.1.1)
+ espree: 9.3.2
+ globals: 13.15.0
+ ignore: 5.3.2
+ import-fresh: 3.2.1
+ js-yaml: 4.1.1
+ minimatch: 3.1.5
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@humanwhocodes/config-array@0.9.5':
+ dependencies:
+ '@humanwhocodes/object-schema': 1.2.1
+ debug: 4.4.3(supports-color@8.1.1)
+ minimatch: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@humanwhocodes/object-schema@1.2.1': {}
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.0
+ strip-ansi: 7.2.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@leichtgewicht/ip-codec@2.0.5': {}
+
+ '@maxmind/geoip2-node@5.0.0':
+ dependencies:
+ ip6addr: 0.2.5
+ maxmind: 4.2.0
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
+ '@sindresorhus/is@0.14.0': {}
+
+ '@szmarczak/http-timer@1.1.2':
+ dependencies:
+ defer-to-connect: 1.1.3
+
+ '@types/http-proxy@1.17.17':
+ dependencies:
+ '@types/node': 25.6.2
+
+ '@types/keyv@3.1.4':
+ dependencies:
+ '@types/node': 25.6.2
+
+ '@types/luxon@3.3.8': {}
+
+ '@types/node@25.6.2':
+ dependencies:
+ undici-types: 7.19.2
+
+ '@types/responselike@1.0.3':
+ dependencies:
+ '@types/node': 25.6.2
+
+ accepts@1.3.8:
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@7.1.1: {}
+
+ acorn@8.16.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ amdefine@1.0.1:
+ optional: true
+
+ ansi-align@3.0.1:
+ dependencies:
+ string-width: 4.2.2
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@6.2.3: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.2
+
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
+ argparse@2.0.1: {}
+
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-flatten@1.1.1: {}
+
+ array.prototype.reduce@1.0.8:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-array-method-boxes-properly: 1.0.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ is-string: 1.1.1
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
+ assert-plus@1.0.0: {}
+
+ assertion-error@1.1.0: {}
+
+ async-function@1.0.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0):
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0)
+ core-js-compat: 3.49.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-relative-path-import@2.0.1:
+ dependencies:
+ slash: 1.0.0
+
+ balanced-match@1.0.0: {}
+
+ base64-js@1.5.1: {}
+
+ baseline-browser-mapping@2.10.29: {}
+
+ binary-extensions@2.3.0: {}
+
+ body-parser@1.20.5:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ http-errors: 2.0.1
+ iconv-lite: 0.4.24
+ on-finished: 2.4.1
+ qs: 6.15.1
+ raw-body: 2.5.3
+ type-is: 1.6.18
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ boxen@5.1.2:
+ dependencies:
+ ansi-align: 3.0.1
+ camelcase: 6.3.0
+ chalk: 4.1.2
+ cli-boxes: 2.2.1
+ string-width: 4.2.2
+ type-fest: 0.20.2
+ widest-line: 3.1.0
+ wrap-ansi: 7.0.0
+
+ brace-expansion@1.1.14:
+ dependencies:
+ balanced-match: 1.0.0
+ concat-map: 0.0.1
+
+ brace-expansion@2.1.0:
+ dependencies:
+ balanced-match: 1.0.0
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browser-stdout@1.3.1: {}
+
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.29
+ caniuse-lite: 1.0.30001792
+ electron-to-chromium: 1.5.353
+ node-releases: 2.0.38
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ buffer-from@1.1.2: {}
+
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ bytes@3.1.2: {}
+
+ cacheable-request@6.0.0:
+ dependencies:
+ clone-response: 1.0.3
+ get-stream: 4.1.0
+ http-cache-semantics: 4.2.0
+ keyv: 3.0.0
+ lowercase-keys: 1.0.1
+ normalize-url: 3.1.0
+ responselike: 1.0.2
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.9:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ camelcase@6.3.0: {}
+
+ caniuse-lite@1.0.30001792: {}
+
+ chai@4.3.6:
+ dependencies:
+ assertion-error: 1.1.0
+ check-error: 1.0.2
+ deep-eql: 3.0.1
+ get-func-name: 2.0.2
+ loupe: 2.3.7
+ pathval: 1.1.1
+ type-detect: 4.1.0
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ check-error@1.0.2: {}
+
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ ci-info@2.0.0: {}
+
+ cli-boxes@2.2.1: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clone-deep@4.0.1:
+ dependencies:
+ is-plain-object: 2.0.4
+ kind-of: 6.0.3
+ shallow-clone: 3.0.1
+
+ clone-response@1.0.3:
+ dependencies:
+ mimic-response: 1.0.1
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ commander@6.2.0: {}
+
+ commander@9.3.0: {}
+
+ commondir@1.0.1: {}
+
+ concat-map@0.0.1: {}
+
+ concat-stream@1.6.2:
+ dependencies:
+ buffer-from: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 2.3.8
+ typedarray: 0.0.6
+
+ configstore@5.0.1:
+ dependencies:
+ dot-prop: 5.3.0
+ graceful-fs: 4.2.11
+ make-dir: 3.1.0
+ unique-string: 2.0.0
+ write-file-atomic: 3.0.0
+ xdg-basedir: 4.0.0
+
+ connect-history-api-fallback@2.0.0: {}
+
+ content-disposition@0.5.4:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ content-type@1.0.5: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie-signature@1.0.7: {}
+
+ cookie@0.7.2: {}
+
+ core-js-compat@3.49.0:
+ dependencies:
+ browserslist: 4.28.2
+
+ core-js@3.49.0: {}
+
+ core-util-is@1.0.2: {}
+
+ core-util-is@1.0.3: {}
+
+ cron@3.1.6:
+ dependencies:
+ '@types/luxon': 3.3.8
+ luxon: 3.4.4
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.1
+
+ crypto-random-string@2.0.0: {}
+
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ debug@2.6.9:
+ dependencies:
+ ms: 2.0.0
+
+ debug@3.2.7(supports-color@5.5.0):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 5.5.0
+
+ debug@4.4.3(supports-color@8.1.1):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 8.1.1
+
+ decamelize@4.0.0: {}
+
+ decompress-response@3.3.0:
+ dependencies:
+ mimic-response: 1.0.1
+
+ deep-eql@3.0.1:
+ dependencies:
+ type-detect: 4.1.0
+
+ deep-extend@0.6.0: {}
+
+ deep-is@0.1.4: {}
+
+ defer-to-connect@1.1.3: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
+ depd@2.0.0: {}
+
+ destroy@1.2.0: {}
+
+ diff@7.0.0: {}
+
+ dns-packet@5.6.1:
+ dependencies:
+ '@leichtgewicht/ip-codec': 2.0.5
+
+ doctrine@3.0.0:
+ dependencies:
+ esutils: 2.0.3
+
+ dot-prop@5.3.0:
+ dependencies:
+ is-obj: 2.0.0
+
+ dotenv@16.4.7: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ duplexer2@0.0.2:
+ dependencies:
+ readable-stream: 1.1.14
+
+ duplexer3@0.1.5: {}
+
+ eastasianwidth@0.2.0: {}
+
+ ee-first@1.1.1: {}
+
+ electron-to-chromium@1.5.353: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ encodeurl@2.0.0: {}
+
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
+ es-abstract@1.24.2:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.3
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.4
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.20
+
+ es-array-method-boxes-properly@1.0.0: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.3
+
+ es-to-primitive@1.3.0:
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
+ esbuild@0.19.8:
+ optionalDependencies:
+ '@esbuild/android-arm': 0.19.8
+ '@esbuild/android-arm64': 0.19.8
+ '@esbuild/android-x64': 0.19.8
+ '@esbuild/darwin-arm64': 0.19.8
+ '@esbuild/darwin-x64': 0.19.8
+ '@esbuild/freebsd-arm64': 0.19.8
+ '@esbuild/freebsd-x64': 0.19.8
+ '@esbuild/linux-arm': 0.19.8
+ '@esbuild/linux-arm64': 0.19.8
+ '@esbuild/linux-ia32': 0.19.8
+ '@esbuild/linux-loong64': 0.19.8
+ '@esbuild/linux-mips64el': 0.19.8
+ '@esbuild/linux-ppc64': 0.19.8
+ '@esbuild/linux-riscv64': 0.19.8
+ '@esbuild/linux-s390x': 0.19.8
+ '@esbuild/linux-x64': 0.19.8
+ '@esbuild/netbsd-x64': 0.19.8
+ '@esbuild/openbsd-x64': 0.19.8
+ '@esbuild/sunos-x64': 0.19.8
+ '@esbuild/win32-arm64': 0.19.8
+ '@esbuild/win32-ia32': 0.19.8
+ '@esbuild/win32-x64': 0.19.8
+
+ escalade@3.2.0: {}
+
+ escape-goat@2.1.1: {}
+
+ escape-html@1.0.3: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ escodegen@0.0.28:
+ dependencies:
+ esprima: 1.0.4
+ estraverse: 1.3.2
+ optionalDependencies:
+ source-map: 0.7.6
+
+ escodegen@1.3.3:
+ dependencies:
+ esprima: 1.1.1
+ estraverse: 1.5.1
+ esutils: 1.0.0
+ optionalDependencies:
+ source-map: 0.1.43
+
+ eslint-scope@7.1.1:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-utils@3.0.0(eslint@8.16.0):
+ dependencies:
+ eslint: 8.16.0
+ eslint-visitor-keys: 2.0.0
+
+ eslint-visitor-keys@2.0.0: {}
+
+ eslint-visitor-keys@3.3.0: {}
+
+ eslint@8.16.0:
+ dependencies:
+ '@eslint/eslintrc': 1.3.0
+ '@humanwhocodes/config-array': 0.9.5
+ ajv: 6.15.0
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3(supports-color@8.1.1)
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.1.1
+ eslint-utils: 3.0.0(eslint@8.16.0)
+ eslint-visitor-keys: 3.3.0
+ espree: 9.3.2
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ functional-red-black-tree: 1.0.1
+ glob-parent: 6.0.2
+ globals: 13.15.0
+ ignore: 5.3.2
+ import-fresh: 3.0.0
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ js-yaml: 4.1.1
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ regexpp: 3.2.0
+ strip-ansi: 6.0.1
+ strip-json-comments: 3.1.1
+ text-table: 0.2.0
+ v8-compile-cache: 2.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@9.3.2:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 3.3.0
+
+ esprima@1.0.4: {}
+
+ esprima@1.1.1: {}
+
+ esprima@4.0.1: {}
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@1.3.2: {}
+
+ estraverse@1.5.1: {}
+
+ estraverse@5.3.0: {}
+
+ esutils@1.0.0: {}
+
+ esutils@2.0.3: {}
+
+ etag@1.8.1: {}
+
+ eventemitter3@4.0.0: {}
+
+ express@4.22.1:
+ dependencies:
+ accepts: 1.3.8
+ array-flatten: 1.1.1
+ body-parser: 1.20.5
+ content-disposition: 0.5.4
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.0.7
+ debug: 2.6.9
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 1.3.2
+ fresh: 0.5.2
+ http-errors: 2.0.1
+ merge-descriptors: 1.0.3
+ methods: 1.1.2
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ path-to-regexp: 0.1.13
+ proxy-addr: 2.0.7
+ qs: 6.14.2
+ range-parser: 1.2.1
+ safe-buffer: 5.2.1
+ send: 0.19.2
+ serve-static: 1.16.3
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ type-is: 1.6.18
+ utils-merge: 1.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ extsprintf@1.3.0: {}
+
+ falafel@2.2.5:
+ dependencies:
+ acorn: 7.1.1
+ isarray: 2.0.5
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastestsmallesttextencoderdecoder@1.0.22: {}
+
+ fetch-socks@1.3.3:
+ dependencies:
+ socks: 2.8.9
+ undici: 7.25.0
+
+ file-entry-cache@6.0.1:
+ dependencies:
+ flat-cache: 3.0.4
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ finalhandler@1.3.2:
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ find-cache-dir@2.1.0:
+ dependencies:
+ commondir: 1.0.1
+ make-dir: 2.1.0
+ pkg-dir: 3.0.0
+
+ find-up@3.0.0:
+ dependencies:
+ locate-path: 3.0.0
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@3.0.4:
+ dependencies:
+ flatted: 3.4.2
+ rimraf: 3.0.2
+
+ flat@5.0.2: {}
+
+ flatted@3.4.2: {}
+
+ follow-redirects@1.16.0(debug@4.4.3):
+ optionalDependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
+ foreground-child@3.1.0:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ forwarded@0.2.0: {}
+
+ fresh@0.5.2: {}
+
+ fs.realpath@1.0.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ function.prototype.name@1.1.8:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ functions-have-names: 1.2.3
+ hasown: 2.0.3
+ is-callable: 1.2.7
+
+ functional-red-black-tree@1.0.1: {}
+
+ functions-have-names@1.2.3: {}
+
+ generator-function@2.0.1: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-func-name@2.0.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.3
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-stream@4.1.0:
+ dependencies:
+ pump: 3.0.4
+
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.1.0
+ jackspeak: 3.1.2
+ minimatch: 9.0.9
+ minipass: 7.1.3
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ glob@7.2.0:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.5
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ global-dirs@3.0.1:
+ dependencies:
+ ini: 2.0.0
+
+ globals@13.15.0:
+ dependencies:
+ type-fest: 0.20.2
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ gopd@1.2.0: {}
+
+ got@9.6.0:
+ dependencies:
+ '@sindresorhus/is': 0.14.0
+ '@szmarczak/http-timer': 1.1.2
+ '@types/keyv': 3.1.4
+ '@types/responselike': 1.0.3
+ cacheable-request: 6.0.0
+ decompress-response: 3.3.0
+ duplexer3: 0.1.5
+ get-stream: 4.1.0
+ lowercase-keys: 1.0.1
+ mimic-response: 1.0.1
+ p-cancelable: 1.1.0
+ to-readable-stream: 1.0.0
+ url-parse-lax: 3.0.0
+
+ graceful-fs@4.2.11: {}
+
+ has-bigints@1.1.0: {}
+
+ has-flag@3.0.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ has-yarn@2.1.0: {}
+
+ has@1.0.4: {}
+
+ hasown@2.0.3:
+ dependencies:
+ function-bind: 1.1.2
+
+ he@1.2.0: {}
+
+ homedir-polyfill@1.0.3:
+ dependencies:
+ parse-passwd: 1.0.0
+
+ http-cache-semantics@4.2.0: {}
+
+ http-errors@2.0.1:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ toidentifier: 1.0.1
+
+ http-proxy-middleware@3.0.3:
+ dependencies:
+ '@types/http-proxy': 1.17.17
+ debug: 4.4.3(supports-color@8.1.1)
+ http-proxy: 1.18.1(debug@4.4.3)
+ is-glob: 4.0.3
+ is-plain-object: 5.0.0
+ micromatch: 4.0.8
+ transitivePeerDependencies:
+ - supports-color
+
+ http-proxy@1.18.1(debug@4.4.3):
+ dependencies:
+ eventemitter3: 4.0.0
+ follow-redirects: 1.16.0(debug@4.4.3)
+ requires-port: 1.0.0
+ transitivePeerDependencies:
+ - debug
+
+ iconv-lite@0.4.24:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ieee754@1.2.1: {}
+
+ ignore-by-default@1.0.1: {}
+
+ ignore@5.3.2: {}
+
+ import-fresh@3.0.0:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ import-fresh@3.2.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ import-lazy@2.1.0: {}
+
+ imurmurhash@0.1.4: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
+
+ ini@1.3.0: {}
+
+ ini@2.0.0: {}
+
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.3
+ side-channel: 1.1.0
+
+ ip-address@10.2.0: {}
+
+ ip-address@9.0.5:
+ dependencies:
+ jsbn: 1.1.0
+ sprintf-js: 1.1.3
+
+ ip6addr@0.2.5:
+ dependencies:
+ assert-plus: 1.0.0
+ jsprim: 2.0.2
+
+ ipaddr.js@1.9.1: {}
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
+ is-ci@2.0.0:
+ dependencies:
+ ci-info: 2.0.0
+
+ is-core-module@2.16.2:
+ dependencies:
+ hasown: 2.0.3
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-extglob@2.1.1: {}
+
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-generator-function@1.1.2:
+ dependencies:
+ call-bound: 1.0.4
+ generator-function: 2.0.1
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-installed-globally@0.4.0:
+ dependencies:
+ global-dirs: 3.0.1
+ is-path-inside: 3.0.3
+
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
+ is-npm@5.0.0: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-number@7.0.0: {}
+
+ is-obj@2.0.0: {}
+
+ is-path-inside@3.0.3: {}
+
+ is-plain-obj@2.1.0: {}
+
+ is-plain-object@2.0.4:
+ dependencies:
+ isobject: 3.0.1
+
+ is-plain-object@5.0.0: {}
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.3
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.20
+
+ is-typedarray@1.0.0: {}
+
+ is-unicode-supported@0.1.0: {}
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-yarn-global@0.3.0: {}
+
+ isarray@0.0.1: {}
+
+ isarray@1.0.0: {}
+
+ isarray@2.0.5: {}
+
+ isexe@2.0.0: {}
+
+ isobject@3.0.1: {}
+
+ jackspeak@3.1.2:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ js-base64@3.7.8: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@3.14.2:
+ dependencies:
+ argparse: 1.0.10
+ esprima: 4.0.1
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsbn@1.1.0: {}
+
+ jsesc@0.5.0: {}
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.0: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema@0.4.0: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ jsprim@2.0.2:
+ dependencies:
+ assert-plus: 1.0.0
+ extsprintf: 1.3.0
+ json-schema: 0.4.0
+ verror: 1.10.0
+
+ jsrsasign@11.1.3: {}
+
+ keyv@3.0.0:
+ dependencies:
+ json-buffer: 3.0.0
+
+ kind-of@6.0.3: {}
+
+ latest-version@5.1.0:
+ dependencies:
+ package-json: 6.5.0
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@3.0.0:
+ dependencies:
+ p-locate: 3.0.0
+ path-exists: 3.0.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.debounce@4.0.8: {}
+
+ lodash.merge@4.6.2: {}
+
+ lodash@4.18.1: {}
+
+ log-symbols@4.1.0:
+ dependencies:
+ chalk: 4.1.2
+ is-unicode-supported: 0.1.0
+
+ loupe@2.3.7:
+ dependencies:
+ get-func-name: 2.0.2
+
+ lowercase-keys@1.0.1: {}
+
+ lru-cache@10.2.0: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ luxon@3.4.4: {}
+
+ make-dir@2.1.0:
+ dependencies:
+ pify: 4.0.1
+ semver: 5.6.0
+
+ make-dir@3.1.0:
+ dependencies:
+ semver: 6.0.0
+
+ math-intrinsics@1.1.0: {}
+
+ maxmind@4.2.0:
+ dependencies:
+ tiny-lru: 7.0.6
+
+ media-typer@0.3.0: {}
+
+ merge-descriptors@1.0.3: {}
+
+ methods@1.1.2: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.2
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime@1.6.0: {}
+
+ mimic-response@1.0.1: {}
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.14
+
+ minimatch@9.0.9:
+ dependencies:
+ brace-expansion: 2.1.0
+
+ minimist@0.0.8: {}
+
+ minimist@1.2.8: {}
+
+ minipass@7.1.3: {}
+
+ mocha@11.7.5:
+ dependencies:
+ browser-stdout: 1.3.1
+ chokidar: 4.0.3
+ debug: 4.4.3(supports-color@8.1.1)
+ diff: 7.0.0
+ escape-string-regexp: 4.0.0
+ find-up: 5.0.0
+ glob: 10.5.0
+ he: 1.2.0
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.1
+ log-symbols: 4.1.0
+ minimatch: 9.0.9
+ ms: 2.1.3
+ picocolors: 1.1.1
+ serialize-javascript: 6.0.2
+ strip-json-comments: 3.1.1
+ supports-color: 8.1.1
+ workerpool: 9.2.0
+ yargs: 17.7.2
+ yargs-parser: 21.1.1
+ yargs-unparser: 2.0.0
+
+ ms@2.0.0: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.12: {}
+
+ natural-compare@1.4.0: {}
+
+ negotiator@0.6.3: {}
+
+ node-environment-flags@1.0.6:
+ dependencies:
+ object.getownpropertydescriptors: 2.1.9
+ semver: 5.7.0
+
+ node-releases@2.0.38: {}
+
+ nodemon@2.0.16:
+ dependencies:
+ chokidar: 3.6.0
+ debug: 3.2.7(supports-color@5.5.0)
+ ignore-by-default: 1.0.1
+ minimatch: 3.1.5
+ pstree.remy: 1.1.8
+ semver: 5.7.1
+ supports-color: 5.5.0
+ touch: 3.1.1
+ undefsafe: 2.0.5
+ update-notifier: 5.1.0
+
+ normalize-path@3.0.0: {}
+
+ normalize-url@3.1.0: {}
+
+ object-inspect@0.4.0: {}
+
+ object-inspect@1.13.4: {}
+
+ object-keys@0.4.0: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.getownpropertydescriptors@2.1.9:
+ dependencies:
+ array.prototype.reduce: 1.0.8
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-object-atoms: 1.1.1
+ gopd: 1.2.0
+ safe-array-concat: 1.1.4
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ own-keys@1.0.1:
+ dependencies:
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-cancelable@1.1.0: {}
+
+ p-limit@2.0.0:
+ dependencies:
+ p-try: 2.2.0
+
+ p-limit@3.0.2:
+ dependencies:
+ p-try: 2.2.0
+
+ p-locate@3.0.0:
+ dependencies:
+ p-limit: 2.0.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.0.2
+
+ p-try@2.2.0: {}
+
+ package-json-from-dist@1.0.1: {}
+
+ package-json@6.5.0:
+ dependencies:
+ got: 9.6.0
+ registry-auth-token: 4.0.0
+ registry-url: 5.1.0
+ semver: 6.2.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-passwd@1.0.0: {}
+
+ parseurl@1.3.3: {}
+
+ path-exists@3.0.0: {}
+
+ path-exists@4.0.0: {}
+
+ path-is-absolute@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.2.0
+ minipass: 7.1.3
+
+ path-to-regexp@0.1.13: {}
+
+ pathval@1.1.1: {}
+
+ peggy@2.0.1:
+ dependencies:
+ commander: 9.3.0
+ source-map-generator: 0.8.0
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ pify@4.0.1: {}
+
+ pirates@4.0.7: {}
+
+ pkg-dir@3.0.0:
+ dependencies:
+ find-up: 3.0.0
+
+ possible-typed-array-names@1.1.0: {}
+
+ prelude-ls@1.2.1: {}
+
+ prepend-http@2.0.0: {}
+
+ prettier@2.6.2: {}
+
+ process-nextick-args@2.0.1: {}
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ pstree.remy@1.1.8: {}
+
+ pump@3.0.4:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
+ punycode@2.3.1: {}
+
+ pupa@2.1.1:
+ dependencies:
+ escape-goat: 2.1.1
+
+ qs@6.14.2:
+ dependencies:
+ side-channel: 1.1.0
+
+ qs@6.15.1:
+ dependencies:
+ side-channel: 1.1.0
+
+ quote-stream@0.0.0:
+ dependencies:
+ minimist: 0.0.8
+ through2: 0.4.1
+
+ randombytes@2.1.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ range-parser@1.2.1: {}
+
+ raw-body@2.5.3:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.4.24
+ unpipe: 1.0.0
+
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.0
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
+ readable-stream@1.0.34:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 0.0.1
+ string_decoder: 0.10.31
+
+ readable-stream@1.1.14:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 0.0.1
+ string_decoder: 0.10.31
+
+ readable-stream@2.3.8:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.2
+
+ readdirp@4.1.2: {}
+
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
+ regenerate-unicode-properties@10.2.2:
+ dependencies:
+ regenerate: 1.4.2
+
+ regenerate@1.4.2: {}
+
+ regenerator-runtime@0.14.1: {}
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
+ regexpp@3.2.0: {}
+
+ regexpu-core@5.3.2:
+ dependencies:
+ '@babel/regjsgen': 0.8.0
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.2
+ regjsparser: 0.9.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.1
+
+ regexpu-core@6.4.0:
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.2
+ regjsgen: 0.8.0
+ regjsparser: 0.13.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.1
+
+ registry-auth-token@4.0.0:
+ dependencies:
+ rc: 1.2.8
+ safe-buffer: 5.2.1
+
+ registry-url@5.1.0:
+ dependencies:
+ rc: 1.2.8
+
+ regjsgen@0.8.0: {}
+
+ regjsparser@0.13.1:
+ dependencies:
+ jsesc: 3.1.0
+
+ regjsparser@0.9.1:
+ dependencies:
+ jsesc: 0.5.0
+
+ require-directory@2.1.1: {}
+
+ requires-port@1.0.0: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.2
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ responselike@1.0.2:
+ dependencies:
+ lowercase-keys: 1.0.1
+
+ rimraf@3.0.2:
+ dependencies:
+ glob: 7.2.0
+
+ safe-array-concat@1.1.4:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-buffer@5.1.2: {}
+
+ safe-buffer@5.2.1: {}
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
+ safer-buffer@2.1.2: {}
+
+ semver-diff@3.1.1:
+ dependencies:
+ semver: 6.3.0
+
+ semver@5.6.0: {}
+
+ semver@5.7.0: {}
+
+ semver@5.7.1: {}
+
+ semver@6.0.0: {}
+
+ semver@6.2.0: {}
+
+ semver@6.3.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.8.0: {}
+
+ send@0.19.2:
+ dependencies:
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 0.5.2
+ http-errors: 2.0.1
+ mime: 1.6.0
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ serialize-javascript@6.0.2:
+ dependencies:
+ randombytes: 2.1.0
+
+ serve-static@1.16.3:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 0.19.2
+ transitivePeerDependencies:
+ - supports-color
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
+ setprototypeof@1.2.0: {}
+
+ shallow-clone@3.0.1:
+ dependencies:
+ kind-of: 6.0.3
+
+ shallow-copy@0.0.1: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.1
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ signal-exit@3.0.7: {}
+
+ signal-exit@4.1.0: {}
+
+ slash@1.0.0: {}
+
+ smart-buffer@4.2.0: {}
+
+ socks@2.8.9:
+ dependencies:
+ ip-address: 10.2.0
+ smart-buffer: 4.2.0
+
+ source-map-generator@0.8.0: {}
+
+ source-map-support@0.5.21:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
+ source-map@0.1.43:
+ dependencies:
+ amdefine: 1.0.1
+ optional: true
+
+ source-map@0.6.1: {}
+
+ source-map@0.7.6:
+ optional: true
+
+ sprintf-js@1.0.3: {}
+
+ sprintf-js@1.1.3: {}
+
+ static-eval@0.2.4:
+ dependencies:
+ escodegen: 0.0.28
+
+ static-js-yaml@1.0.0:
+ dependencies:
+ js-yaml: 3.14.2
+ static-module: 1.5.0
+ through2: 0.6.5
+
+ static-module@1.5.0:
+ dependencies:
+ concat-stream: 1.6.2
+ duplexer2: 0.0.2
+ escodegen: 1.3.3
+ falafel: 2.2.5
+ has: 1.0.4
+ object-inspect: 0.4.0
+ quote-stream: 0.0.0
+ readable-stream: 1.0.34
+ shallow-copy: 0.0.1
+ static-eval: 0.2.4
+ through2: 0.4.1
+
+ statuses@2.0.2: {}
+
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
+ string-width@4.2.0:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@4.2.2:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.0
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.2.0
+
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string_decoder@0.10.31: {}
+
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
+ strip-ansi@6.0.0:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-json-comments@2.0.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@5.5.0:
+ dependencies:
+ has-flag: 3.0.0
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-color@8.1.1:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ text-table@0.2.0: {}
+
+ through2@0.4.1:
+ dependencies:
+ readable-stream: 1.0.34
+ xtend: 2.1.2
+
+ through2@0.6.5:
+ dependencies:
+ readable-stream: 1.0.34
+ xtend: 4.0.2
+
+ tiny-lru@7.0.6: {}
+
+ to-readable-stream@1.0.0: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ toidentifier@1.0.1: {}
+
+ touch@3.1.1: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-detect@4.1.0: {}
+
+ type-fest@0.20.2: {}
+
+ type-is@1.6.18:
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.35
+
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.4:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.7:
+ dependencies:
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
+ typedarray-to-buffer@3.1.5:
+ dependencies:
+ is-typedarray: 1.0.0
+
+ typedarray@0.0.6: {}
+
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
+ undefsafe@2.0.5: {}
+
+ undici-types@7.19.2: {}
+
+ undici@7.25.0: {}
+
+ unicode-canonical-property-names-ecmascript@2.0.1: {}
+
+ unicode-match-property-ecmascript@2.0.0:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.1
+ unicode-property-aliases-ecmascript: 2.2.0
+
+ unicode-match-property-value-ecmascript@2.2.1: {}
+
+ unicode-property-aliases-ecmascript@2.2.0: {}
+
+ unique-string@2.0.0:
+ dependencies:
+ crypto-random-string: 2.0.0
+
+ unpipe@1.0.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ update-notifier@5.1.0:
+ dependencies:
+ boxen: 5.1.2
+ chalk: 4.1.2
+ configstore: 5.0.1
+ has-yarn: 2.1.0
+ import-lazy: 2.1.0
+ is-ci: 2.0.0
+ is-installed-globally: 0.4.0
+ is-npm: 5.0.0
+ is-yarn-global: 0.3.0
+ latest-version: 5.1.0
+ pupa: 2.1.1
+ semver: 7.8.0
+ semver-diff: 3.1.1
+ xdg-basedir: 4.0.0
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ url-parse-lax@3.0.0:
+ dependencies:
+ prepend-http: 2.0.0
+
+ util-deprecate@1.0.2: {}
+
+ utils-merge@1.0.1: {}
+
+ v8-compile-cache@2.4.0: {}
+
+ v8flags@3.2.0:
+ dependencies:
+ homedir-polyfill: 1.0.3
+
+ vary@1.1.2: {}
+
+ verror@1.10.0:
+ dependencies:
+ assert-plus: 1.0.0
+ core-util-is: 1.0.2
+ extsprintf: 1.3.0
+
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.1.8
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.2
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.20
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.20:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.1:
+ dependencies:
+ isexe: 2.0.0
+
+ widest-line@3.1.0:
+ dependencies:
+ string-width: 4.2.2
+
+ word-wrap@1.2.5: {}
+
+ workerpool@9.2.0: {}
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.2.0
+
+ wrappy@1.0.2: {}
+
+ write-file-atomic@3.0.0:
+ dependencies:
+ imurmurhash: 0.1.4
+ is-typedarray: 1.0.0
+ signal-exit: 3.0.7
+ typedarray-to-buffer: 3.1.5
+
+ xdg-basedir@4.0.0: {}
+
+ xtend@2.1.2:
+ dependencies:
+ object-keys: 0.4.0
+
+ xtend@4.0.2: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs-unparser@2.0.0:
+ dependencies:
+ camelcase: 6.3.0
+ decamelize: 4.0.0
+ flat: 5.0.2
+ is-plain-obj: 2.1.0
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml
new file mode 100644
index 0000000000..2485b6e796
--- /dev/null
+++ b/backend/pnpm-workspace.yaml
@@ -0,0 +1,4 @@
+allowBuilds:
+ core-js: true
+ esbuild: true
+ nodemon: true
diff --git a/backend/src/constants.js b/backend/src/constants.js
new file mode 100644
index 0000000000..ef42231212
--- /dev/null
+++ b/backend/src/constants.js
@@ -0,0 +1,21 @@
+export const SCHEMA_VERSION_KEY = 'schemaVersion';
+export const SETTINGS_KEY = 'settings';
+export const SUBS_KEY = 'subs';
+export const COLLECTIONS_KEY = 'collections';
+export const FILES_KEY = 'files';
+export const MODULES_KEY = 'modules';
+export const ARTIFACTS_KEY = 'artifacts';
+export const RULES_KEY = 'rules';
+export const TOKENS_KEY = 'tokens';
+export const ARCHIVES_KEY = 'archives';
+export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
+export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
+export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
+export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
+export const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource';
+export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource';
+export const LOGS_KEY = '#sub-store-logs';
+export const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour
+export const DEFAULT_HEADERS_CACHE_TTL = 60 * 1000; // 1 min
+export const DEFAULT_SCRIPT_CACHE_TTL = 48 * 3600 * 1000; // 48 hours
+export const DEFAULT_LOGS_MAX_COUNT = 0;
diff --git a/backend/src/core/app.js b/backend/src/core/app.js
new file mode 100644
index 0000000000..5257e7368d
--- /dev/null
+++ b/backend/src/core/app.js
@@ -0,0 +1,5 @@
+import 'fastestsmallesttextencoderdecoder';
+import { OpenAPI } from '@/vendor/open-api';
+
+const $ = new OpenAPI('sub-store');
+export default $;
diff --git a/backend/src/core/proxy-utils/ech-utils.js b/backend/src/core/proxy-utils/ech-utils.js
new file mode 100644
index 0000000000..b1f3ba5429
--- /dev/null
+++ b/backend/src/core/proxy-utils/ech-utils.js
@@ -0,0 +1,149 @@
+import { isNotBlank, isPlainObject } from '@/utils';
+
+export const ECH_DNS_FIELD = '_dns';
+export const ECH_FORCE_QUERY_FIELD = '_force-query';
+export const ECH_SOCKOPT_FIELD = '_sockopt';
+export const DEFAULT_XRAY_ECH_DNS = 'https://dns.alidns.com/dns-query';
+
+export function parseXrayEchConfigList(echConfigList) {
+ if (!isNotBlank(echConfigList)) {
+ return undefined;
+ }
+
+ if (!echConfigList.includes('://')) {
+ return {
+ type: 'config',
+ config: echConfigList,
+ };
+ }
+
+ const parts = echConfigList.split('+');
+ if (parts.length === 1 && isNotBlank(parts[0])) {
+ return {
+ type: 'dns',
+ dns: parts[0],
+ };
+ }
+
+ if (parts.length === 2 && isNotBlank(parts[0]) && isNotBlank(parts[1])) {
+ return {
+ type: 'dns',
+ queryServerName: parts[0],
+ dns: parts[1],
+ };
+ }
+
+ return undefined;
+}
+
+export function isSupportedXrayEchConfigList(echConfigList) {
+ return parseXrayEchConfigList(echConfigList) != null;
+}
+
+export function isSupportedXrayEchForceQuery(forceQuery) {
+ return ['none', 'half', 'full'].includes(forceQuery);
+}
+
+function isMihomoEchEnabled(value) {
+ // Match mihomo's current `ech-opts.enable` handling:
+ // - adapter/parser.go decodes proxies with WeaklyTypedInput=true.
+ // - common/structure/structure.go decodeBool accepts bool directly and
+ // converts int/uint to `value != 0`, but does not convert strings.
+ // - adapter/outbound/ech.go then gates ECH with `if !o.Enable`.
+ if (typeof value === 'boolean') {
+ return value;
+ }
+
+ return typeof value === 'number' && Number.isInteger(value) && value !== 0;
+}
+
+export function buildMihomoEchOptsFromXrayFields({
+ echConfigList,
+ echForceQuery,
+ echSockopt,
+} = {}) {
+ const parsedEchConfigList = parseXrayEchConfigList(echConfigList);
+ if (!parsedEchConfigList) {
+ return undefined;
+ }
+
+ const echOpts = {
+ enable: true,
+ };
+ if (parsedEchConfigList.type === 'config') {
+ echOpts.config = parsedEchConfigList.config;
+ } else {
+ echOpts[ECH_DNS_FIELD] = parsedEchConfigList.dns;
+ if (parsedEchConfigList.queryServerName) {
+ echOpts['query-server-name'] = parsedEchConfigList.queryServerName;
+ }
+ }
+
+ if (isSupportedXrayEchForceQuery(echForceQuery)) {
+ echOpts[ECH_FORCE_QUERY_FIELD] = echForceQuery;
+ }
+
+ if (isPlainObject(echSockopt)) {
+ echOpts[ECH_SOCKOPT_FIELD] = echSockopt;
+ }
+
+ return echOpts;
+}
+
+export function buildXrayEchFieldsFromMihomo(
+ echOpts,
+ fallbackEchConfigList,
+ { dnsFieldPath = 'ech-opts._dns', warnDefaultDns } = {},
+) {
+ const fields = {};
+
+ if (isPlainObject(echOpts)) {
+ if (!isMihomoEchEnabled(echOpts.enable)) {
+ return fields;
+ }
+
+ const queryServerName = echOpts['query-server-name'];
+ if (isNotBlank(echOpts.config)) {
+ fields.echConfigList = echOpts.config;
+ } else if (isNotBlank(echOpts[ECH_DNS_FIELD])) {
+ fields.echConfigList = isNotBlank(queryServerName)
+ ? `${queryServerName}+${echOpts[ECH_DNS_FIELD]}`
+ : echOpts[ECH_DNS_FIELD];
+ } else if (isNotBlank(queryServerName)) {
+ fields.echConfigList = `${queryServerName}+${DEFAULT_XRAY_ECH_DNS}`;
+ warnDefaultDns?.({
+ defaultDns: DEFAULT_XRAY_ECH_DNS,
+ dnsFieldPath,
+ queryServerName,
+ });
+ }
+
+ if (
+ fields.echConfigList &&
+ isSupportedXrayEchForceQuery(echOpts[ECH_FORCE_QUERY_FIELD])
+ ) {
+ fields.echForceQuery = echOpts[ECH_FORCE_QUERY_FIELD];
+ }
+
+ if (fields.echConfigList && isPlainObject(echOpts[ECH_SOCKOPT_FIELD])) {
+ fields.echSockopt = echOpts[ECH_SOCKOPT_FIELD];
+ }
+
+ return fields;
+ }
+
+ if (isNotBlank(fallbackEchConfigList)) {
+ fields.echConfigList = fallbackEchConfigList;
+ }
+
+ return fields;
+}
+
+export function buildXrayEchConfigListFromMihomo(
+ echOpts,
+ fallbackEchConfigList,
+ options,
+) {
+ return buildXrayEchFieldsFromMihomo(echOpts, fallbackEchConfigList, options)
+ .echConfigList;
+}
diff --git a/backend/src/core/proxy-utils/index.js b/backend/src/core/proxy-utils/index.js
new file mode 100644
index 0000000000..a140c8a1b9
--- /dev/null
+++ b/backend/src/core/proxy-utils/index.js
@@ -0,0 +1,1013 @@
+import { Base64 } from 'js-base64';
+import { Buffer } from 'buffer';
+import rs from '@/utils/rs';
+import YAML from '@/utils/yaml';
+import download, { downloadFile } from '@/utils/download';
+import {
+ isIPv4,
+ isIPv6,
+ isValidPortNumber,
+ isValidUUID,
+ isNotBlank,
+ ipAddress,
+ getRandomPort,
+ numberToString,
+} from '@/utils';
+import PROXY_PROCESSORS, {
+ ApplyProcessor,
+ ApplyResponseTransformer,
+ isResponseTransformerType,
+} from './processors';
+import PROXY_PREPROCESSORS from './preprocessors';
+import PROXY_PRODUCERS from './producers';
+import PROXY_PARSERS from './parsers';
+import $ from '@/core/app';
+import { FILES_KEY, MODULES_KEY } from '@/constants';
+import { findByName } from '@/utils/database';
+import { produceArtifact } from '@/restful/sync';
+import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
+import Gist from '@/utils/gist';
+import {
+ isPresent,
+ isShadowsocksOverTls,
+ normalizeWireGuardInterface,
+} from './producers/utils';
+import { doh } from '@/utils/dns';
+import JSON5 from 'json5';
+import { hex_md5 } from '@/vendor/md5';
+import SurgeMac_Producer from './producers/surgemac';
+
+function preprocess(raw) {
+ for (const processor of PROXY_PREPROCESSORS) {
+ try {
+ if (processor.test(raw)) {
+ $.info(`Pre-processor [${processor.name}] activated`);
+ return processor.parse(raw);
+ }
+ } catch (e) {
+ $.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
+ }
+ }
+ return raw;
+}
+
+function parse(raw) {
+ raw = preprocess(raw);
+ // parse
+ const lines = raw.split('\n');
+ const proxies = [];
+ let lastParser;
+
+ for (let line of lines) {
+ line = line.trim();
+ if (line.length === 0) continue; // skip empty line
+ let success = false;
+
+ // try to parse with last used parser
+ if (lastParser) {
+ const [proxy, error] = tryParse(lastParser, line);
+ if (!error) {
+ proxies.push(lastParse(proxy));
+ success = true;
+ }
+ }
+
+ if (!success) {
+ // search for a new parser
+ for (const parser of PROXY_PARSERS) {
+ const [proxy, error] = tryParse(parser, line);
+ if (!error) {
+ proxies.push(lastParse(proxy));
+ lastParser = parser;
+ success = true;
+ $.info(`${parser.name} is activated`);
+ break;
+ }
+ }
+ }
+
+ if (!success) {
+ $.error(`Failed to parse line: ${line}`);
+ }
+ }
+ return proxies.filter((proxy) => {
+ if (['vless', 'vmess'].includes(proxy.type)) {
+ const isProxyUUIDValid = isValidUUID(proxy.uuid);
+ if (!isProxyUUIDValid) {
+ $.warn(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
+ }
+ // return isProxyUUIDValid;
+ } else if (['hysteria2'].includes(proxy.type)) {
+ if (proxy.obfs && !proxy['obfs-password']) {
+ $.error(
+ `Proxy ${proxy.name} has obfs ${proxy.obfs} but missing obfs-password`,
+ );
+ return false;
+ }
+ }
+ return true;
+ });
+}
+
+async function processFn(
+ proxies,
+ operators = [],
+ targetPlatform,
+ source,
+ $options,
+) {
+ let context = {};
+ for (const item of operators) {
+ if (isResponseTransformerType(item.type)) {
+ $.log(
+ `Skipping response transformer during proxy/file processing: "${
+ item.type
+ }" with arguments:\n >>> ${
+ JSON.stringify(item.args, null, 2) || 'None'
+ }`,
+ );
+ continue;
+ }
+ if (item.disabled) {
+ $.log(
+ `Skipping disabled operator: "${
+ item.type
+ }" with arguments:\n >>> ${
+ JSON.stringify(item.args, null, 2) || 'None'
+ }`,
+ );
+ continue;
+ }
+ // process script
+ let script;
+ let $arguments = {};
+ if (item.type.indexOf('Script') !== -1) {
+ ({ script, $arguments } = await loadScriptItem(item));
+ }
+
+ if (!PROXY_PROCESSORS[item.type]) {
+ $.error(`Unknown operator: "${item.type}"`);
+ continue;
+ }
+
+ $.log(
+ `Applying "${item.type}" with arguments:\n >>> ${
+ JSON.stringify(item.args, null, 2) || 'None'
+ }`,
+ );
+ let processor;
+ if (item.type.indexOf('Script') !== -1) {
+ processor = PROXY_PROCESSORS[item.type](
+ script,
+ targetPlatform,
+ $arguments,
+ source,
+ $options,
+ context,
+ );
+ } else {
+ processor = PROXY_PROCESSORS[item.type](item.args || {});
+ }
+ proxies = await ApplyProcessor(processor, proxies);
+ }
+ return proxies;
+}
+
+async function processResponseFn(
+ response,
+ operators = [],
+ targetPlatform,
+ source,
+ $options,
+) {
+ let context = {};
+ let output = normalizeResponse(response);
+ for (const item of operators) {
+ if (!isResponseTransformerType(item.type)) continue;
+ if (item.disabled) {
+ $.log(
+ `Skipping disabled response transformer: "${
+ item.type
+ }" with arguments:\n >>> ${
+ JSON.stringify(item.args, null, 2) || 'None'
+ }`,
+ );
+ continue;
+ }
+
+ const { script, $arguments } = await loadScriptItem(item);
+ $.log(
+ `Applying "${item.type}" with arguments:\n >>> ${
+ JSON.stringify(item.args, null, 2) || 'None'
+ }`,
+ );
+ const transformer = PROXY_PROCESSORS[item.type](
+ script,
+ targetPlatform,
+ $arguments,
+ source,
+ $options,
+ context,
+ );
+ output = normalizeResponse(
+ await ApplyResponseTransformer(transformer, output),
+ );
+ }
+ return output;
+}
+
+async function loadScriptItem(item) {
+ let script;
+ let $arguments = {};
+ const { mode, content } = item.args || {};
+ if (mode === 'link') {
+ let url = content || '';
+ // extract link arguments
+ const rawArgs = url.split('#');
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ url = `${url.split('#')[0]}${
+ rawArgs[2]
+ ? `#${rawArgs[2]}`
+ : $arguments?.noCache != null || $arguments?.insecure != null
+ ? `#${rawArgs[1]}`
+ : ''
+ }`;
+ const downloadUrlMatch = url
+ .split('#')[0]
+ .match(/^\/api\/(file|module)\/(.+)/);
+ if (downloadUrlMatch) {
+ let type = '';
+ try {
+ type = downloadUrlMatch?.[1];
+ let name = downloadUrlMatch?.[2];
+ if (name == null) {
+ throw new Error(`本地 ${type} URL 无效: ${url}`);
+ }
+ name = decodeURIComponent(name);
+ const key = type === 'module' ? MODULES_KEY : FILES_KEY;
+ const localItem = findByName($.read(key), name);
+ if (!localItem) {
+ throw new Error(`找不到 ${type}: ${name}`);
+ }
+
+ if (type === 'module') {
+ script = localItem.content;
+ } else {
+ script = await produceArtifact({
+ type: 'file',
+ name,
+ });
+ }
+ } catch (err) {
+ $.error(
+ `Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
+ );
+ throw new Error(`无法加载 ${type}: ${url}`);
+ }
+ } else if (url?.startsWith('/')) {
+ try {
+ const fs = eval(`require("fs")`);
+ script = fs.readFileSync(url.split('#')[0], 'utf8');
+ // $.info(`Script loaded: >>>\n ${script}`);
+ } catch (err) {
+ $.error(
+ `Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
+ );
+ throw new Error(`无法从该路径读取脚本文件: ${url}`);
+ }
+ } else {
+ // if this is a remote script, download it
+ try {
+ script = await download(url);
+ // $.info(`Script loaded: >>>\n ${script}`);
+ } catch (err) {
+ $.error(
+ `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
+ );
+ throw new Error(`无法下载脚本: ${url}`);
+ }
+ }
+ } else {
+ script = content;
+ $arguments = item.args?.arguments || {};
+ }
+ return { script, $arguments };
+}
+
+function normalizeResponse(response = {}) {
+ let headers = response.header || response.headers || {};
+ if (!headers || typeof headers !== 'object') headers = {};
+ const normalized = {
+ status: response.status || response.statusCode || 200,
+ body: Object.prototype.hasOwnProperty.call(response, 'body')
+ ? response.body
+ : '',
+ };
+ Object.defineProperty(normalized, 'headers', {
+ enumerable: true,
+ get() {
+ return headers;
+ },
+ set(value) {
+ headers = value && typeof value === 'object' ? value : {};
+ },
+ });
+ Object.defineProperty(normalized, 'header', {
+ enumerable: true,
+ get() {
+ return headers;
+ },
+ set(value) {
+ headers = value && typeof value === 'object' ? value : {};
+ },
+ });
+ return normalized;
+}
+
+function produce(proxies, targetPlatform, type, opts = {}) {
+ const producer = PROXY_PRODUCERS[targetPlatform];
+ if (!producer) {
+ throw new Error(`Target platform: ${targetPlatform} is not supported!`);
+ }
+
+ const normalizedTarget = String(targetPlatform).toLowerCase();
+ const supportedShadowsocksOverTlsTargets = new Set([
+ 'qx',
+ 'quantumultx',
+ 'shadowrocket',
+ ]);
+
+ const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
+ targetPlatform,
+ );
+
+ // filter unsupported proxies
+ proxies = proxies.filter((proxy) => {
+ const includeUnsupportedProxy = opts['include-unsupported-proxy'];
+
+ // 检查代理是否支持目标平台
+ if (
+ !includeUnsupportedProxy &&
+ proxy.supported &&
+ proxy.supported[targetPlatform] === false
+ ) {
+ return false;
+ }
+
+ if (
+ !includeUnsupportedProxy &&
+ hasRootProxyHeaders(proxy) &&
+ isRootHeaderSensitiveProxy(proxy) &&
+ !supportsRootProxyHeaders(proxy, targetPlatform)
+ ) {
+ $.error(
+ `Target platform ${targetPlatform} does not support headers for ${getRootHeaderProxyLabel(proxy)} proxy ${proxy.name || `${proxy.server}:${proxy.port}`}. Proxy has been filtered.`,
+ );
+ return false;
+ }
+
+ if (
+ !includeUnsupportedProxy &&
+ isShadowsocksOverTls(proxy) &&
+ !supportedShadowsocksOverTlsTargets.has(normalizedTarget)
+ ) {
+ return false;
+ }
+
+ // 对于 vless 和 vmess 代理,需要额外验证 UUID
+ if (['vless', 'vmess'].includes(proxy.type)) {
+ const isProxyUUIDValid = isValidUUID(proxy.uuid);
+ if (!isProxyUUIDValid)
+ $.warn(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
+ // return isProxyUUIDValid;
+ const isVlessType = proxy.type === 'vless';
+ const realityChecks = isVlessType
+ ? [
+ ['reality-opts', proxy['reality-opts']],
+ [
+ 'xhttp download-settings reality-opts',
+ proxy['xhttp-opts']?.['download-settings']?.[
+ 'reality-opts'
+ ],
+ ],
+ ]
+ : [];
+ for (const [realityLabel, realityOpts] of realityChecks) {
+ if (realityOpts && !isNotBlank(realityOpts['public-key'])) {
+ // When the main proxy (上行) uses Reality with a valid
+ // public-key, an explicit empty-string public-key in xhttp
+ // download-settings reality-opts (下行) is intentional:
+ // it explicitly cancels Reality inheritance for the
+ // download stream. This is a legitimate Mihomo config.
+ // Distinguish from reality-opts:{} (missing public-key
+ // entirely) which represents a broken/incomplete Reality
+ // config parsed from a malformed URI and should still be
+ // rejected.
+ if (
+ realityLabel ===
+ 'xhttp download-settings reality-opts' &&
+ isNotBlank(proxy['reality-opts']?.['public-key']) &&
+ realityOpts['public-key'] === ''
+ ) {
+ continue;
+ }
+ // Intentional: a VLESS Reality node without public-key is
+ // not a valid Mihomo export or regenerated share link. We
+ // keep the marker while parsing so callers can inspect the
+ // broken config, then stop export here instead of silently
+ // emitting invalid Reality output.
+ $.error(
+ `Skipping VLESS Reality proxy ${proxy.name}: empty ${realityLabel}.public-key`,
+ );
+ return false;
+ }
+ }
+
+ const xhttpOpts = proxy['xhttp-opts'];
+ if (
+ isVlessType &&
+ proxy.network === 'xhttp' &&
+ xhttpOpts?.mode === 'stream-one' &&
+ xhttpOpts['download-settings']
+ ) {
+ // Match Mihomo's outbound validation: xhttp download-settings
+ // require split transports, so stream-one is rejected instead
+ // of emitting a config/share link that Mihomo will not accept.
+ $.error(
+ `Skipping VLESS xhttp proxy ${proxy.name}: mode "stream-one" cannot be used with download-settings`,
+ );
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ proxies = proxies.map((proxy) => {
+ proxy._resolved = proxy.resolved;
+
+ if (!isNotBlank(proxy.name)) {
+ proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
+ }
+ if (proxy['disable-sni']) {
+ if (sni_off_supported) {
+ proxy.sni = 'off';
+ } else if (!['tuic'].includes(proxy.type)) {
+ $.error(
+ `Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,
+ );
+ proxy.sni = '';
+ proxy['skip-cert-verify'] = true;
+ delete proxy['tls-fingerprint'];
+ }
+ }
+
+ // 处理 端口跳跃
+ if (proxy.ports) {
+ proxy.ports = String(proxy.ports);
+ if (!['ClashMeta'].includes(targetPlatform)) {
+ proxy.ports = proxy.ports.replace(/\//g, ',');
+ }
+ if (!proxy.port) {
+ proxy.port = getRandomPort(proxy.ports);
+ }
+ }
+ if (proxy.type === 'wireguard') {
+ normalizeWireGuardInterface(proxy);
+ }
+
+ return proxy;
+ });
+
+ $.log(`Producing proxies for target: ${targetPlatform}`);
+ if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
+ let list = proxies
+ .map((proxy) => {
+ try {
+ return producer.produce(proxy, type, opts);
+ } catch (err) {
+ $.error(
+ `Cannot produce proxy: ${proxy.name}\nReason: ${err}`,
+ );
+ return '';
+ }
+ })
+ .filter((line) => line.length > 0);
+ if (opts._merged && opts.localPort >= 1) {
+ list.push(
+ SurgeMac_Producer().produce(
+ {
+ name: opts._merged.name,
+ type: 'external',
+ udp: true,
+ exec: opts._merged.exec,
+ 'local-port': opts.localPort,
+ args: [
+ '-config',
+ Base64.encode(
+ JSON.stringify({
+ ...opts._merged.config,
+ 'mixed-port': opts.localPort,
+ }),
+ ),
+ ],
+ addresses: [],
+ },
+ type,
+ opts,
+ ),
+ );
+ }
+ list = type === 'internal' ? list : list.join('\n');
+ if (
+ targetPlatform.startsWith('Surge') &&
+ proxies.length > 0 &&
+ proxies.every((p) => p.type === 'wireguard')
+ ) {
+ list = `#!name=${proxies[0]?._subName}
+#!desc=${proxies[0]?._desc ?? ''}
+#!category=${proxies[0]?._category ?? ''}
+${list}`;
+ }
+ return list;
+ } else if (producer.type === 'ALL') {
+ return producer.produce(proxies, type, opts);
+ }
+}
+
+function hasRootProxyHeaders(proxy) {
+ return (
+ proxy?.headers &&
+ typeof proxy.headers === 'object' &&
+ Object.keys(proxy.headers).length > 0
+ );
+}
+
+function isRootHeaderSensitiveProxy(proxy) {
+ return ['http', 'h2-connect', 'trusttunnel'].includes(proxy?.type);
+}
+
+function supportsRootProxyHeaders(proxy, targetPlatform) {
+ const normalizedTarget = `${targetPlatform}`.toLowerCase();
+
+ if (normalizedTarget.startsWith('surge')) {
+ return ['http', 'h2-connect', 'trusttunnel'].includes(proxy.type);
+ }
+
+ if (normalizedTarget === 'egern') {
+ return proxy.type === 'http';
+ }
+
+ if (
+ ['clashmeta', 'clash.meta', 'meta', 'mihomo'].includes(
+ normalizedTarget,
+ )
+ ) {
+ return proxy.type === 'http';
+ }
+
+ if (['singbox', 'sing-box'].includes(normalizedTarget)) {
+ return proxy.type === 'http';
+ }
+
+ if (normalizedTarget === 'json') {
+ return ['http', 'h2-connect', 'trusttunnel'].includes(proxy.type);
+ }
+
+ return false;
+}
+
+function getRootHeaderProxyLabel(proxy) {
+ if (proxy.type === 'http') {
+ return proxy.tls ? 'HTTPS' : 'HTTP';
+ }
+
+ if (proxy.type === 'h2-connect') {
+ return 'HTTP/2 CONNECT';
+ }
+
+ if (proxy.type === 'trusttunnel') {
+ return 'TrustTunnel';
+ }
+
+ return proxy.type;
+}
+
+export const ProxyUtils = {
+ parse,
+ process: processFn,
+ processResponse: processResponseFn,
+ produce,
+ ipAddress,
+ getRandomPort,
+ isIPv4,
+ isIPv6,
+ isIP,
+ yaml: YAML,
+ getFlag,
+ removeFlag,
+ getISO,
+ MMDB,
+ Gist,
+ download,
+ downloadFile,
+ isValidUUID,
+ doh,
+ Buffer,
+ Base64,
+ JSON5,
+ hex_md5,
+};
+
+function tryParse(parser, line) {
+ if (!safeMatch(parser, line)) return [null, new Error('Parser mismatch')];
+ try {
+ const proxy = parser.parse(line);
+ return [proxy, null];
+ } catch (err) {
+ return [null, err];
+ }
+}
+
+function safeMatch(parser, line) {
+ try {
+ return parser.test(line);
+ } catch (err) {
+ return false;
+ }
+}
+
+function formatTransportPath(path) {
+ if (typeof path === 'string' || typeof path === 'number') {
+ path = String(path).trim();
+
+ if (path === '') {
+ return '/';
+ } else if (!path.startsWith('/')) {
+ return '/' + path;
+ }
+ }
+ return path;
+}
+
+function lastParse(proxy) {
+ if (typeof proxy.cipher === 'string') {
+ proxy.cipher = proxy.cipher.toLowerCase();
+ }
+ if (typeof proxy.password === 'number') {
+ proxy.password = numberToString(proxy.password);
+ }
+ if (proxy['hop-interval'] != null) {
+ const hopInterval = `${proxy['hop-interval']}`.trim();
+ const hopIntervalRangeMatch = hopInterval.match(/^(\d+)\s*-\s*(\d+)$/);
+
+ if (hopIntervalRangeMatch) {
+ const hopIntervalMin = parseInt(hopIntervalRangeMatch[1], 10);
+ const hopIntervalMax = parseInt(hopIntervalRangeMatch[2], 10);
+
+ if (hopIntervalMin > 0 && hopIntervalMin <= hopIntervalMax) {
+ // 暂时只在统一收口阶段拆分 mihomo 的 hop-interval 区间写法,
+ // 不对其他客户端做进一步转换,等 mihomo / sing-box 新版覆盖率上来后再统一处理。
+ proxy['hop-interval'] = hopIntervalMin;
+ proxy['hop-interval-max'] = hopIntervalMax;
+ } else {
+ delete proxy['hop-interval'];
+ delete proxy['hop-interval-max'];
+ }
+ } else if (/^\d+$/.test(hopInterval)) {
+ const parsedHopInterval = parseInt(hopInterval, 10);
+
+ if (parsedHopInterval > 0) {
+ proxy['hop-interval'] = parsedHopInterval;
+ delete proxy['hop-interval-max'];
+ } else {
+ delete proxy['hop-interval'];
+ delete proxy['hop-interval-max'];
+ }
+ } else {
+ delete proxy['hop-interval'];
+ delete proxy['hop-interval-max'];
+ }
+ }
+ if (
+ ['ss'].includes(proxy.type) &&
+ proxy.cipher === 'none' &&
+ !proxy.password
+ ) {
+ // https://github.com/MetaCubeX/mihomo/issues/1677
+ proxy.password = '';
+ }
+ if (proxy.interface) {
+ proxy['interface-name'] = proxy.interface;
+ delete proxy.interface;
+ }
+ if (isValidPortNumber(proxy.port)) {
+ proxy.port = parseInt(proxy.port, 10);
+ }
+ if (proxy.server) {
+ proxy.server = `${proxy.server}`
+ .trim()
+ .replace(/^\[/, '')
+ .replace(/\]$/, '');
+ }
+ if (proxy.network === 'ws') {
+ if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {
+ proxy['ws-opts'] = {};
+ if (proxy['ws-path']) {
+ proxy['ws-opts'].path = proxy['ws-path'];
+ }
+ if (proxy['ws-headers']) {
+ proxy['ws-opts'].headers = proxy['ws-headers'];
+ }
+ }
+ delete proxy['ws-path'];
+ delete proxy['ws-headers'];
+ }
+
+ const transportPath = proxy[`${proxy.network}-opts`]?.path;
+
+ if (Array.isArray(transportPath)) {
+ proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>
+ formatTransportPath(item),
+ );
+ } else if (transportPath != null) {
+ proxy[`${proxy.network}-opts`].path =
+ formatTransportPath(transportPath);
+ }
+
+ // network 逻辑有点乱了 可能还牵扯到别的逻辑 以后再优化...
+ // 以 mihomo 为准的话, 其实应该是
+ // network¶
+ // 传输层,支持 ws/grpc,不配置或配置其他值则为 tcp
+ if (proxy.type === 'trojan') {
+ proxy.network = proxy.network || 'tcp';
+ }
+ // network¶
+ // 传输层,支持 ws/http/h2/grpc,不配置或配置其他值则为 tcp
+ if (['vmess'].includes(proxy.type)) {
+ proxy.network = proxy.network || 'tcp';
+
+ proxy.cipher = proxy.cipher || 'none';
+ proxy.alterId = proxy.alterId || 0;
+ }
+ // network¶
+ // 传输层,支持 ws/http/h2/grpc,不配置或配置其他值则为 tcp
+ if (['vless'].includes(proxy.type)) {
+ proxy.network = proxy.network || 'tcp';
+ }
+ if (
+ [
+ 'trojan',
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'anytls',
+ 'trusttunnel',
+ 'h2-connect',
+ 'naive',
+ ].includes(proxy.type)
+ ) {
+ proxy.tls = true;
+ }
+ if (proxy.network) {
+ let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
+ let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
+ if (proxy.network === 'h2') {
+ if (!transporthost && transportHost) {
+ proxy[`${proxy.network}-opts`].headers.host = transportHost;
+ delete proxy[`${proxy.network}-opts`].headers.Host;
+ }
+ } else if (transporthost && !transportHost) {
+ proxy[`${proxy.network}-opts`].headers.Host = transporthost;
+ delete proxy[`${proxy.network}-opts`].headers.host;
+ }
+ }
+ if (proxy.network === 'h2') {
+ const host = proxy['h2-opts']?.headers?.host;
+ const path = proxy['h2-opts']?.path;
+ if (host && !Array.isArray(host)) {
+ proxy['h2-opts'].headers.host = [host];
+ }
+ if (Array.isArray(path)) {
+ proxy['h2-opts'].path = path[0];
+ }
+ }
+
+ // 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
+ if (
+ !proxy.tls &&
+ ['ws', 'http'].includes(proxy.network) &&
+ !proxy[`${proxy.network}-opts`]?.headers?.Host &&
+ !isIP(proxy.server)
+ ) {
+ proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
+ proxy[`${proxy.network}-opts`].headers =
+ proxy[`${proxy.network}-opts`].headers || {};
+ proxy[`${proxy.network}-opts`].headers.Host =
+ ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
+ ? [proxy.server]
+ : proxy.server;
+ }
+ // 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
+ if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
+ let transportPath = proxy[`${proxy.network}-opts`]?.path;
+ let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
+ if (transportHost && !Array.isArray(transportHost)) {
+ proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
+ }
+ if (transportPath && !Array.isArray(transportPath)) {
+ proxy[`${proxy.network}-opts`].path = [transportPath];
+ }
+ }
+ // 允许设置 sni 为空字符串且为防止影响其他逻辑, 这里先改成这样判断
+ // 本质上是为了防止本来应该使用 server 作为 sni 的情况下, 若之后进行了域名解析, 导致 server 变成 ip 丢失了 sni
+ // 为了兼容性, 暂时先这么改
+ if (proxy.tls && !proxy.sni && proxy.sni !== '') {
+ // 传输层若有设置就使用
+ if (proxy.network) {
+ let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
+ transportHost = Array.isArray(transportHost)
+ ? transportHost[0]
+ : transportHost;
+ if (transportHost) {
+ proxy.sni = transportHost;
+ }
+ }
+ // 不区分是不是域名, 总之如果到这里还没 sni, 可以设置域名 server 为 sni
+ if (!proxy.sni && !isIP(proxy.server)) {
+ proxy.sni = proxy.server;
+ }
+ }
+ // if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {
+ if (proxy.ports) {
+ proxy.ports = String(proxy.ports).replace(/\//g, ',');
+ } else {
+ delete proxy.ports;
+ }
+ // }
+ if (
+ ['hysteria2'].includes(proxy.type) &&
+ proxy.obfs &&
+ !['salamander'].includes(proxy.obfs) &&
+ !proxy['obfs-password']
+ ) {
+ proxy['obfs-password'] = proxy.obfs;
+ proxy.obfs = 'salamander';
+ }
+ if (
+ ['hysteria2'].includes(proxy.type) &&
+ !proxy['obfs-password'] &&
+ proxy['obfs_password']
+ ) {
+ proxy['obfs-password'] = proxy['obfs_password'];
+ delete proxy['obfs_password'];
+ }
+ if (['vless'].includes(proxy.type)) {
+ // 删除 reality-opts: {}
+ if (
+ proxy['reality-opts'] &&
+ Object.keys(proxy['reality-opts']).length === 0
+ ) {
+ delete proxy['reality-opts'];
+ }
+ // 删除 grpc-opts: {}
+ if (
+ proxy['grpc-opts'] &&
+ Object.keys(proxy['grpc-opts']).length === 0
+ ) {
+ delete proxy['grpc-opts'];
+ }
+ // 非 reality, 空 flow 没有意义
+ if (
+ (!proxy['reality-opts'] && !proxy.flow) ||
+ ['null', null].includes(proxy.flow)
+ ) {
+ delete proxy.flow;
+ }
+ if (['http'].includes(proxy.network)) {
+ let transportPath = proxy[`${proxy.network}-opts`]?.path;
+ if (!transportPath) {
+ if (!proxy[`${proxy.network}-opts`]) {
+ proxy[`${proxy.network}-opts`] = {};
+ }
+ proxy[`${proxy.network}-opts`].path = ['/'];
+ }
+ }
+ }
+
+ if (typeof proxy.name !== 'string') {
+ if (/^\d+$/.test(proxy.name)) {
+ proxy.name = `${proxy.name}`;
+ } else {
+ try {
+ if (proxy.name?.data) {
+ proxy.name = Buffer.from(proxy.name.data).toString('utf8');
+ } else {
+ proxy.name = Buffer.from(proxy.name).toString('utf8');
+ }
+ } catch (e) {
+ $.error(`proxy.name decode failed\nReason: ${e}`);
+ proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
+ }
+ }
+ }
+ if (['ws', 'http', 'h2'].includes(proxy.network)) {
+ if (
+ ['ws', 'h2'].includes(proxy.network) &&
+ !proxy[`${proxy.network}-opts`]?.path
+ ) {
+ proxy[`${proxy.network}-opts`] =
+ proxy[`${proxy.network}-opts`] || {};
+ proxy[`${proxy.network}-opts`].path = '/';
+ } else if (
+ proxy.network === 'http' &&
+ (!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
+ proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
+ ) {
+ proxy[`${proxy.network}-opts`] =
+ proxy[`${proxy.network}-opts`] || {};
+ proxy[`${proxy.network}-opts`].path = ['/'];
+ }
+ }
+ if (['', 'off'].includes(proxy.sni)) {
+ proxy['disable-sni'] = true;
+ }
+ let caStr = proxy['ca_str'];
+ if (proxy['ca-str']) {
+ caStr = proxy['ca-str'];
+ } else if (caStr) {
+ delete proxy['ca_str'];
+ proxy['ca-str'] = caStr;
+ }
+ try {
+ if ($.env.isNode && !caStr && proxy['_ca']) {
+ caStr = $.node.fs.readFileSync(proxy['_ca'], {
+ encoding: 'utf8',
+ });
+ }
+ } catch (e) {
+ $.error(`Read ca file failed\nReason: ${e}`);
+ }
+ if (!proxy['tls-fingerprint'] && caStr) {
+ proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
+ }
+ if (
+ ['ss'].includes(proxy.type) &&
+ isPresent(proxy, 'shadow-tls-password')
+ ) {
+ proxy.plugin = 'shadow-tls';
+ proxy['plugin-opts'] = {
+ host: proxy['shadow-tls-sni'],
+ password: proxy['shadow-tls-password'],
+ version: proxy['shadow-tls-version'],
+ };
+ delete proxy['shadow-tls-sni'];
+ delete proxy['shadow-tls-password'];
+ delete proxy['shadow-tls-version'];
+ }
+ if (['tuic'].includes(proxy.type)) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn || 'h3'];
+ proxy['congestion-controller'] =
+ proxy['congestion-controller'] || 'cubic';
+ proxy['udp-relay-mode'] = proxy['udp-relay-mode'] || 'native';
+ }
+ if (['wireguard'].includes(proxy.type)) {
+ if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
+ const validPeer =
+ proxy.peers.find((peer) => peer.ip && peer.ipv6) ||
+ proxy.peers.find((peer) => peer.ip || peer.ipv6);
+ if (validPeer) {
+ if (!proxy.ip) {
+ proxy.ip = proxy.peers[0]?.ip;
+ }
+ if (!proxy.ipv6) {
+ proxy.ipv6 = proxy.peers[0]?.ipv6;
+ }
+ }
+ }
+ normalizeWireGuardInterface(proxy);
+ }
+ return proxy;
+}
+
+function isIP(ip) {
+ return isIPv4(ip) || isIPv6(ip);
+}
diff --git a/backend/src/core/proxy-utils/parsers/index.js b/backend/src/core/proxy-utils/parsers/index.js
new file mode 100644
index 0000000000..cbc0680712
--- /dev/null
+++ b/backend/src/core/proxy-utils/parsers/index.js
@@ -0,0 +1,3028 @@
+import {
+ isIPv4,
+ isIPv6,
+ getIfNotBlank,
+ isPresent,
+ isNotBlank,
+ getIfPresent,
+ getRandomPort,
+ isPlainObject,
+} from '@/utils';
+import getSurgeParser from './peggy/surge';
+import getLoonParser from './peggy/loon';
+import getQXParser from './peggy/qx';
+import getTrojanURIParser from './peggy/trojan-uri';
+import $ from '@/core/app';
+import JSON5 from 'json5';
+import YAML from '@/utils/yaml';
+import _ from 'lodash';
+
+import { Base64 } from 'js-base64';
+import {
+ normalizeXhttpIntegerValue,
+ normalizeXhttpNonNegativeRange,
+ normalizeXhttpPositiveRange,
+ normalizeXhttpScalarUpperBound,
+} from '../xhttp-utils';
+import { extractPathQueryParam, getPathQueryParam } from '../transport-path';
+import {
+ buildMihomoEchOptsFromXrayFields,
+ isSupportedXrayEchConfigList,
+ isSupportedXrayEchForceQuery,
+} from '../ech-utils';
+
+function surge_port_hopping(raw) {
+ const [parts, port_hopping] =
+ raw.match(
+ /,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
+ ) || [];
+ return {
+ port_hopping: port_hopping
+ ? port_hopping.replace(/;/g, ',')
+ : undefined,
+ line: parts ? raw.replace(parts, '') : raw,
+ };
+}
+
+function splitURIFragment(raw) {
+ const [__, content, fragment] = /^(.*?)(?:#(.*?))?$/.exec(raw);
+ return {
+ content,
+ fragment: fragment != null ? decodeURIComponent(fragment) : undefined,
+ };
+}
+
+function decodeShadowsocksUserInfo(rawUserInfoStr) {
+ const separatorIndex = rawUserInfoStr.indexOf(':');
+ if (separatorIndex !== -1) {
+ return [
+ decodeURIComponent(rawUserInfoStr.slice(0, separatorIndex)),
+ decodeURIComponent(rawUserInfoStr.slice(separatorIndex + 1)),
+ ].join(':');
+ }
+
+ const decodedUserInfoStr = decodeURIComponent(rawUserInfoStr);
+ if (decodedUserInfoStr.includes(':')) {
+ return decodedUserInfoStr;
+ }
+
+ return Base64.decode(decodedUserInfoStr);
+}
+
+function isNumericEarlyData(value) {
+ if (value == null || !/^\d+$/.test(`${value}`)) return false;
+
+ return Number.isSafeInteger(parseInt(`${value}`, 10));
+}
+
+function extractEarlyDataFromPath(path) {
+ const ed = getPathQueryParam(path, 'ed');
+ if (!isNumericEarlyData(ed)) {
+ return {
+ path,
+ ed: '',
+ };
+ }
+
+ return {
+ path: extractPathQueryParam(path, 'ed').path,
+ ed,
+ };
+}
+
+function parseEarlyDataSize(value) {
+ const raw = `${value}`;
+ if (!/^\d+$/.test(raw)) {
+ throw new Error(`bad WebSocket max early data size: ${value}`);
+ }
+ const parsed = parseInt(raw, 10);
+ if (!Number.isSafeInteger(parsed)) {
+ throw new Error(`bad WebSocket max early data size: ${value}`);
+ }
+ return parsed;
+}
+
+function parseWireGuardURIAddressValue(value) {
+ if (value == null) return null;
+ const raw = `${value}`.trim();
+ if (!raw) return null;
+ const [, hostRaw = raw, cidrRaw] = /^(.*?)(?:\/(\d+))?$/.exec(raw) || [];
+ const host = `${hostRaw}`.trim().replace(/^\[/, '').replace(/\]$/, '');
+ const normalizeCIDR = (cidr, max) => {
+ if (cidr == null) return undefined;
+ if (!/^\d+$/.test(cidr)) return undefined;
+ const parsed = parseInt(cidr, 10);
+ if (parsed < 0 || parsed > max) return undefined;
+ return parsed;
+ };
+ if (isIPv4(host)) {
+ return {
+ family: 'ipv4',
+ address: host,
+ cidr: normalizeCIDR(cidrRaw, 32),
+ };
+ }
+ if (isIPv6(host)) {
+ return {
+ family: 'ipv6',
+ address: host,
+ cidr: normalizeCIDR(cidrRaw, 128),
+ };
+ }
+ return null;
+}
+
+function URI_PROXY() {
+ // socks5+tls
+ // socks5
+ // http, https(可以这么写)
+ const name = 'URI PROXY Parser';
+ const test = (line) => {
+ return /^(socks5\+tls|socks5|http|https):\/\//.test(line);
+ };
+ const parse = (line) => {
+ // parse url
+ // eslint-disable-next-line no-unused-vars
+ let [__, type, tls, username, password, server, port, query, name] =
+ line.match(
+ /^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?\/?(\?.*?)?(?:#(.*?))?$/,
+ );
+ if (port) {
+ port = parseInt(port, 10);
+ } else {
+ if (tls) {
+ port = 443;
+ } else if (type === 'http') {
+ port = 80;
+ } else {
+ $.error(`port is not present in line: ${line}`);
+ throw new Error(`port is not present in line: ${line}`);
+ }
+ $.info(`port is not present in line: ${line}, set to ${port}`);
+ }
+
+ const proxy = {
+ name:
+ name != null
+ ? decodeURIComponent(name)
+ : `${type} ${server}:${port}`,
+ type,
+ tls: tls ? true : false,
+ server,
+ port,
+ username:
+ username != null ? decodeURIComponent(username) : undefined,
+ password:
+ password != null ? decodeURIComponent(password) : undefined,
+ };
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_SOCKS() {
+ const name = 'URI SOCKS Parser';
+ const test = (line) => {
+ return /^socks:\/\//.test(line);
+ };
+ const parse = (line) => {
+ // parse url
+ // eslint-disable-next-line no-unused-vars
+ let [__, type, auth, server, port, query, name] = line.match(
+ /^(socks)?:\/\/(?:(.*)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
+ );
+ if (port) {
+ port = parseInt(port, 10);
+ } else {
+ $.error(`port is not present in line: ${line}`);
+ throw new Error(`port is not present in line: ${line}`);
+ }
+ let username, password;
+ if (auth) {
+ const parsed = Base64.decode(decodeURIComponent(auth)).split(':');
+ username = parsed[0];
+ password = parsed[1];
+ }
+
+ const proxy = {
+ name:
+ name != null
+ ? decodeURIComponent(name)
+ : `${type} ${server}:${port}`,
+ type: 'socks5',
+ server,
+ port,
+ username,
+ password,
+ };
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
+// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
+function URI_SS() {
+ const name = 'URI SS Parser';
+ const test = (line) => {
+ return /^ss:\/\//.test(line);
+ };
+ const parse = (line) => {
+ // parse url
+ let { content, fragment: name } = splitURIFragment(
+ line.split('ss://')[1],
+ );
+ const proxy = {
+ type: 'ss',
+ };
+ // handle IPV4 and IPV6
+ let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/);
+
+ let userInfoStr = decodeShadowsocksUserInfo(content.split('@')[0]);
+
+ let query = '';
+ if (!serverAndPortArray) {
+ if (content.includes('?')) {
+ const parsed = content.match(/^(.*)(\?.*)$/);
+ content = parsed[1];
+ query = parsed[2];
+ }
+ content = Base64.decode(content);
+
+ if (query) {
+ if (/(&|\?)v2ray-plugin=/.test(query)) {
+ const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
+ let v2rayPlugin = parsed[2];
+ if (v2rayPlugin) {
+ proxy.plugin = 'v2ray-plugin';
+ proxy['plugin-opts'] = JSON.parse(
+ Base64.decode(v2rayPlugin),
+ );
+ }
+ }
+ content = `${content}${query}`;
+ }
+ userInfoStr = content.match(/(^.*)@/)?.[1];
+ serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
+ } else if (content.includes('?')) {
+ const parsed = content.match(/(\?.*)$/);
+ query = parsed[1];
+ }
+ const params = {};
+ for (const addon of query.replace(/^\?/, '').split('&')) {
+ if (addon) {
+ const [key, valueRaw] = addon.split('=');
+ let value = valueRaw;
+ value = decodeURIComponent(valueRaw);
+ params[key] = value;
+ }
+ }
+ proxy.tls = params.security && params.security !== 'none';
+ proxy['skip-cert-verify'] = !!params['allowInsecure'];
+ proxy.sni = params['sni'] || params['peer'];
+ proxy['client-fingerprint'] = params.fp;
+ proxy.alpn = params.alpn
+ ? decodeURIComponent(params.alpn).split(',')
+ : undefined;
+
+ if (params['ws']) {
+ proxy.network = 'ws';
+ _.set(proxy, 'ws-opts.path', params['wspath']);
+ }
+
+ if (params['type']) {
+ let httpupgrade;
+ let httpUpgradeEd = '';
+ let pathEarlyData = '';
+ proxy.network = params['type'];
+ if (proxy.network === 'httpupgrade') {
+ proxy.network = 'ws';
+ httpupgrade = true;
+ }
+ if (['grpc'].includes(proxy.network)) {
+ proxy[proxy.network + '-opts'] = {
+ 'grpc-service-name': params['serviceName'],
+ '_grpc-type': params['mode'],
+ '_grpc-authority': params['authority'],
+ };
+ } else {
+ if (params['path']) {
+ let transportPath = params['path'];
+ if (proxy.network === 'ws') {
+ const extracted =
+ extractEarlyDataFromPath(transportPath);
+ transportPath = extracted.path;
+ if (httpupgrade) {
+ httpUpgradeEd = extracted.ed;
+ } else {
+ pathEarlyData = extracted.ed;
+ }
+ }
+ _.set(proxy, proxy.network + '-opts.path', transportPath);
+ }
+ if (params['host']) {
+ _.set(
+ proxy,
+ proxy.network + '-opts.headers.Host',
+ decodeURIComponent(params['host']),
+ );
+ }
+ if (httpupgrade) {
+ httpUpgradeEd =
+ httpUpgradeEd ||
+ (isNumericEarlyData(params.ed) ? `${params.ed}` : '');
+ _.set(
+ proxy,
+ proxy.network + '-opts.v2ray-http-upgrade',
+ true,
+ );
+ if (httpUpgradeEd !== '') {
+ _.set(
+ proxy,
+ proxy.network +
+ '-opts.v2ray-http-upgrade-fast-open',
+ true,
+ );
+ _.set(
+ proxy,
+ proxy.network + '-opts._v2ray-http-upgrade-ed',
+ httpUpgradeEd,
+ );
+ }
+ } else if (proxy.network === 'ws' && pathEarlyData !== '') {
+ _.set(
+ proxy,
+ proxy.network + '-opts.max-early-data',
+ parseEarlyDataSize(pathEarlyData),
+ );
+ _.set(
+ proxy,
+ proxy.network + '-opts.early-data-header-name',
+ 'Sec-WebSocket-Protocol',
+ );
+ }
+ }
+ if (['reality'].includes(params.security)) {
+ const opts = {};
+ if (params.pbk) {
+ opts['public-key'] = params.pbk;
+ }
+ if (params.sid) {
+ opts['short-id'] = params.sid;
+ }
+ if (params.spx) {
+ opts['_spider-x'] = params.spx;
+ }
+ if (params.mode) {
+ proxy._mode = params.mode;
+ }
+ if (params.extra) {
+ proxy._extra = params.extra;
+ }
+ if (Object.keys(opts).length > 0) {
+ _.set(proxy, params.security + '-opts', opts);
+ }
+ }
+ }
+
+ proxy.udp = !!params['udp'];
+
+ const serverAndPort = serverAndPortArray[1];
+ const portIdx = serverAndPort.lastIndexOf(':');
+ proxy.server = serverAndPort.substring(0, portIdx);
+ proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
+ /\d+/,
+ )?.[0];
+ let userInfo = userInfoStr.match(/(^.*?):(.*$)/);
+ proxy.cipher = userInfo?.[1];
+ proxy.password = userInfo?.[2];
+ // if (!proxy.cipher || !proxy.password) {
+ // userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/);
+ // proxy.cipher = userInfo?.[1];
+ // proxy.password = userInfo?.[2];
+ // }
+
+ // handle obfs
+ const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
+ const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
+ const gostMatch = content.match(/[?&]gost=([^&]+)/);
+
+ if (pluginMatch) {
+ const pluginInfo = (
+ 'plugin=' + decodeURIComponent(pluginMatch[1])
+ ).split(';');
+ const params = {};
+ for (const item of pluginInfo) {
+ const separatorIndex = item.indexOf('=');
+ if (separatorIndex === -1) {
+ if (item) params[item] = true; // some options like "tls" will not have value
+ continue;
+ }
+ const key = item.slice(0, separatorIndex);
+ const val = item.slice(separatorIndex + 1).replace(/\\=/g, '=');
+ if (key) params[key] = val || true;
+ }
+ switch (params.plugin) {
+ case 'obfs-local':
+ case 'simple-obfs':
+ proxy.plugin = 'obfs';
+ proxy['plugin-opts'] = {
+ mode: params.obfs,
+ host: getIfNotBlank(params['obfs-host']),
+ };
+ break;
+ case 'v2ray-plugin':
+ proxy.plugin = 'v2ray-plugin';
+ proxy['plugin-opts'] = {
+ mode:
+ getIfNotBlank(params['obfs']) ||
+ getIfNotBlank(params['mode']) ||
+ 'websocket',
+ host:
+ getIfNotBlank(params['obfs-host']) ||
+ getIfNotBlank(params['host']),
+ path: getIfNotBlank(params.path),
+ tls: getIfPresent(params.tls),
+ sni: getIfPresent(params.sni),
+ 'skip-cert-verify': ['1', 'true', 1, true].includes(
+ params['skip-cert-verify'],
+ ),
+ mux: /^\d+$/.test(params.mux)
+ ? parseInt(params.mux, 10)
+ : undefined,
+ };
+ break;
+ case 'shadow-tls': {
+ proxy.plugin = 'shadow-tls';
+ const version = getIfNotBlank(params['version']);
+ proxy['plugin-opts'] = {
+ host: getIfNotBlank(params['host']),
+ password: getIfNotBlank(params['password']),
+ version: version ? parseInt(version, 10) : undefined,
+ };
+ break;
+ }
+ default:
+ throw new Error(
+ `Unsupported plugin option: ${params.plugin}`,
+ );
+ }
+ }
+ // Shadowrocket
+ if (shadowTlsMatch) {
+ const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));
+ const version = getIfNotBlank(params['version']);
+ const address = getIfNotBlank(params['address']);
+ const port = getIfNotBlank(params['port']);
+ proxy.plugin = 'shadow-tls';
+ proxy['plugin-opts'] = {
+ host: getIfNotBlank(params['host']),
+ password: getIfNotBlank(params['password']),
+ version: version ? parseInt(version, 10) : undefined,
+ };
+ if (address) {
+ proxy.server = address;
+ }
+ if (port) {
+ proxy.port = parseInt(port, 10);
+ }
+ }
+ if (gostMatch) {
+ const params = JSON.parse(
+ Base64.decode(decodeURIComponent(gostMatch[1])),
+ );
+ const address = getIfNotBlank(params['address']);
+ const port = getIfNotBlank(params['port']);
+ const route = getIfNotBlank(params['route']);
+ const normalizedRoute = route?.trim().toLowerCase();
+ const isWebsocketRoute = ['ws', 'wss', 'websocket'].includes(
+ normalizedRoute,
+ );
+ proxy.plugin = 'gost-plugin';
+ proxy['plugin-opts'] = {
+ mode: isWebsocketRoute ? 'websocket' : route,
+ host: getIfNotBlank(params['host']),
+ path: getIfNotBlank(params['path']),
+ };
+ if (normalizedRoute === 'wss') {
+ proxy['plugin-opts'].tls = true;
+ }
+ if (address) {
+ proxy.server = address;
+ }
+ if (port) {
+ proxy.port = parseInt(port, 10);
+ }
+ }
+ if (/(&|\?)uot=(1|true)/i.test(query)) {
+ proxy['udp-over-tcp'] = true;
+ }
+ if (/(&|\?)tfo=(1|true)/i.test(query)) {
+ proxy.tfo = true;
+ }
+ proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+// Parse URI SSR format, such as ssr://xxx
+function URI_SSR() {
+ const name = 'URI SSR Parser';
+ const test = (line) => {
+ return /^ssr:\/\//.test(line);
+ };
+ const parse = (line) => {
+ line = Base64.decode(line.split('ssr://')[1]);
+
+ // handle IPV6 & IPV4 format
+ let splitIdx = line.indexOf(':origin');
+ if (splitIdx === -1) {
+ splitIdx = line.indexOf(':auth_');
+ }
+ const serverAndPort = line.substring(0, splitIdx);
+ const server = serverAndPort.substring(
+ 0,
+ serverAndPort.lastIndexOf(':'),
+ );
+ const port = serverAndPort.substring(
+ serverAndPort.lastIndexOf(':') + 1,
+ );
+
+ let params = line
+ .substring(splitIdx + 1)
+ .split('/?')[0]
+ .split(':');
+ let proxy = {
+ type: 'ssr',
+ server,
+ port,
+ protocol: params[0],
+ cipher: params[1],
+ obfs: params[2],
+ password: Base64.decode(params[3]),
+ };
+ // get other params
+ const other_params = {};
+ line = line.split('/?')[1].split('&');
+ if (line.length > 1) {
+ for (const item of line) {
+ let [key, val] = item.split('=');
+ val = val.trim();
+ if (val.length > 0 && val !== '(null)') {
+ other_params[key] = val;
+ }
+ }
+ }
+ proxy = {
+ ...proxy,
+ name: other_params.remarks
+ ? Base64.decode(other_params.remarks)
+ : proxy.server,
+ 'protocol-param': getIfNotBlank(
+ Base64.decode(other_params.protoparam || '').replace(/\s/g, ''),
+ ),
+ 'obfs-param': getIfNotBlank(
+ Base64.decode(other_params.obfsparam || '').replace(/\s/g, ''),
+ ),
+ };
+ return proxy;
+ };
+
+ return { name, test, parse };
+}
+
+// V2rayN URI VMess format
+// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
+
+// Quantumult VMess format
+function URI_VMess() {
+ const name = 'URI VMess Parser';
+ const test = (line) => {
+ return /^vmess:\/\//.test(line);
+ };
+ const parse = (line) => {
+ let { content: lineWithoutFragment, fragment: fragmentName } =
+ splitURIFragment(line.split('vmess://')[1]);
+ let content = Base64.decode(lineWithoutFragment.replace(/\?.*?$/, ''));
+ if (/=\s*vmess/.test(content)) {
+ // Quantumult VMess URI format
+ const partitions = content.split(',').map((p) => p.trim());
+ // get keyword params
+ const params = {};
+ for (const part of partitions) {
+ if (part.indexOf('=') !== -1) {
+ const [key, val] = part.split('=');
+ params[key.trim()] = val.trim();
+ }
+ }
+
+ const proxy = {
+ name: partitions[0].split('=')[0].trim(),
+ type: 'vmess',
+ server: partitions[1],
+ port: partitions[2],
+ cipher: getIfNotBlank(partitions[3], 'auto'),
+ uuid: partitions[4].match(/^"(.*)"$/)[1],
+ tls: params.obfs === 'wss',
+ udp: getIfPresent(params['udp-relay']),
+ tfo: getIfPresent(params['fast-open']),
+ 'skip-cert-verify': isPresent(params['tls-verification'])
+ ? !params['tls-verification']
+ : undefined,
+ };
+
+ // handle ws headers
+ if (isPresent(params.obfs)) {
+ if (params.obfs === 'ws' || params.obfs === 'wss') {
+ proxy.network = 'ws';
+ proxy['ws-opts'].path = (
+ getIfNotBlank(params['obfs-path']) || '"/"'
+ ).match(/^"(.*)"$/)[1];
+ let obfs_host = params['obfs-header'];
+ if (obfs_host && obfs_host.indexOf('Host') !== -1) {
+ obfs_host = obfs_host.match(
+ /Host:\s*([a-zA-Z0-9-.]*)/,
+ )[1];
+ }
+ if (isNotBlank(obfs_host)) {
+ proxy['ws-opts'].headers = {
+ Host: obfs_host,
+ };
+ }
+ } else {
+ throw new Error(`Unsupported obfs: ${params.obfs}`);
+ }
+ }
+ if (isNotBlank(fragmentName)) {
+ proxy.name = fragmentName;
+ }
+ return proxy;
+ } else {
+ let params = {};
+
+ try {
+ // V2rayN URI format
+ params = JSON.parse(content);
+ } catch (e) {
+ // Shadowrocket URI format
+ // eslint-disable-next-line no-unused-vars
+ let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(
+ lineWithoutFragment,
+ );
+ content = Base64.decode(base64Line);
+
+ for (const addon of qs.split('&')) {
+ const [key, valueRaw] = addon.split('=');
+ let value = valueRaw;
+ value = decodeURIComponent(valueRaw);
+ if (value.indexOf(',') === -1) {
+ params[key] = value;
+ } else {
+ params[key] = value.split(',');
+ }
+ }
+ // eslint-disable-next-line no-unused-vars
+ let [___, cipher, uuid, server, port] =
+ /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
+
+ params.scy = cipher;
+ params.id = uuid;
+ params.port = port;
+ params.add = server;
+ }
+ const server = params.add;
+ const port = parseInt(getIfPresent(params.port), 10);
+ const proxy = {
+ name:
+ params.ps ??
+ params.remarks ??
+ params.remark ??
+ `VMess ${server}:${port}`,
+ type: 'vmess',
+ server,
+ port,
+ // https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link
+ // https://github.com/XTLS/Xray-core/issues/91
+ cipher: [
+ 'auto',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ 'none',
+ ].includes(params.scy)
+ ? params.scy
+ : 'auto',
+ uuid: params.id,
+ alterId: parseInt(
+ getIfPresent(params.aid ?? params.alterId, 0),
+ 10,
+ ),
+ tls: ['tls', true, 1, '1'].includes(params.tls),
+ 'skip-cert-verify': isPresent(params.verify_cert)
+ ? !params.verify_cert
+ : undefined,
+ };
+ if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
+ params.allowInsecure,
+ );
+ }
+ // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
+ if (proxy.tls) {
+ if (params.sni && params.sni !== '') {
+ proxy.sni = params.sni;
+ } else if (params.peer && params.peer !== '') {
+ proxy.sni = params.peer;
+ }
+ }
+ let httpupgrade = false;
+ // handle obfs
+ if (params.net === 'ws' || params.obfs === 'websocket') {
+ proxy.network = 'ws';
+ } else if (
+ ['http'].includes(params.net) ||
+ ['http'].includes(params.obfs) ||
+ ['http'].includes(params.type)
+ ) {
+ proxy.network = 'http';
+ } else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
+ proxy.network = params.net;
+ } else if (
+ params.net === 'httpupgrade' ||
+ proxy.network === 'httpupgrade'
+ ) {
+ proxy.network = 'ws';
+ httpupgrade = true;
+ } else if (params.net === 'h2' || proxy.network === 'h2') {
+ proxy.network = 'h2';
+ }
+ // 暂不支持 tcp + host + path
+ // else if (params.net === 'tcp' || proxy.network === 'tcp') {
+ // proxy.network = 'tcp';
+ // }
+ if (proxy.network) {
+ let transportHost = params.host ?? params.obfsParam;
+ try {
+ const parsedObfs = JSON.parse(transportHost);
+ const parsedHost = parsedObfs?.Host;
+ if (parsedHost) {
+ transportHost = parsedHost;
+ }
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+ let transportPath = params.path;
+ let httpUpgradeEd = '';
+ let pathEarlyData = '';
+ if (proxy.network === 'ws' && transportPath) {
+ const extracted = extractEarlyDataFromPath(transportPath);
+ transportPath = extracted.path;
+ if (httpupgrade) {
+ httpUpgradeEd = extracted.ed;
+ } else {
+ pathEarlyData = extracted.ed;
+ }
+ }
+
+ // 补上默认 path
+ if (['ws'].includes(proxy.network)) {
+ transportPath = transportPath || '/';
+ }
+
+ if (proxy.network === 'http') {
+ if (transportHost) {
+ // 1)http(tcp)->host中间逗号(,)隔开
+ transportHost = transportHost
+ .split(',')
+ .map((i) => i.trim());
+ transportHost = Array.isArray(transportHost)
+ ? transportHost[0]
+ : transportHost;
+ }
+ if (transportPath) {
+ transportPath = Array.isArray(transportPath)
+ ? transportPath[0]
+ : transportPath;
+ } else {
+ transportPath = '/';
+ }
+ }
+ // 传输层应该有配置, 暂时不考虑兼容不给配置的节点
+ if (
+ transportPath ||
+ transportHost ||
+ ['kcp', 'quic'].includes(proxy.network)
+ ) {
+ if (['grpc'].includes(proxy.network)) {
+ proxy[`${proxy.network}-opts`] = {
+ 'grpc-service-name': getIfNotBlank(transportPath),
+ '_grpc-type': getIfNotBlank(params.type),
+ '_grpc-authority': getIfNotBlank(params.authority),
+ };
+ } else if (['kcp', 'quic'].includes(proxy.network)) {
+ proxy[`${proxy.network}-opts`] = {
+ [`_${proxy.network}-type`]: getIfNotBlank(
+ params.type,
+ ),
+ [`_${proxy.network}-host`]: getIfNotBlank(
+ getIfNotBlank(transportHost),
+ ),
+ [`_${proxy.network}-path`]:
+ getIfNotBlank(transportPath),
+ };
+ } else {
+ const opts = {
+ path: getIfNotBlank(transportPath),
+ headers: { Host: getIfNotBlank(transportHost) },
+ };
+ if (httpupgrade) {
+ opts['v2ray-http-upgrade'] = true;
+ httpUpgradeEd =
+ httpUpgradeEd ||
+ (isNumericEarlyData(params.ed)
+ ? `${params.ed}`
+ : '');
+ if (httpUpgradeEd !== '') {
+ opts['v2ray-http-upgrade-fast-open'] = true;
+ opts['_v2ray-http-upgrade-ed'] = httpUpgradeEd;
+ }
+ } else if (
+ proxy.network === 'ws' &&
+ pathEarlyData !== ''
+ ) {
+ opts['max-early-data'] =
+ parseEarlyDataSize(pathEarlyData);
+ opts['early-data-header-name'] =
+ 'Sec-WebSocket-Protocol';
+ }
+ proxy[`${proxy.network}-opts`] = opts;
+ }
+ } else {
+ delete proxy.network;
+ }
+ }
+
+ proxy['client-fingerprint'] = params.fp;
+ proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
+ // 然而 wiki 和 app 实测中都没有字段表示这个
+ // proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
+ if (isNotBlank(fragmentName)) {
+ proxy.name = fragmentName;
+ }
+
+ return proxy;
+ }
+ };
+ return { name, test, parse };
+}
+
+function URI_VLESS() {
+ const name = 'URI VLESS Parser';
+ const test = (line) => {
+ return /^vless:\/\//.test(line);
+ };
+ const parse = (line) => {
+ const mapXmuxToReuseSettings = (xmux) => {
+ if (!isPlainObject(xmux)) {
+ return undefined;
+ }
+
+ const reuseSettings = {};
+ const xmuxFieldMap = {
+ maxConnections: 'max-connections',
+ maxConcurrency: 'max-concurrency',
+ cMaxReuseTimes: 'c-max-reuse-times',
+ hMaxRequestTimes: 'h-max-request-times',
+ hMaxReusableSecs: 'h-max-reusable-secs',
+ };
+
+ for (const [sourceKey, targetKey] of Object.entries(xmuxFieldMap)) {
+ const normalizedValue = normalizeXhttpNonNegativeRange(
+ xmux[sourceKey],
+ );
+ if (normalizedValue != null) {
+ reuseSettings[targetKey] =
+ typeof normalizedValue === 'number'
+ ? `${normalizedValue}`
+ : normalizedValue;
+ }
+ }
+
+ const hKeepAlivePeriod = normalizeXhttpIntegerValue(
+ xmux.hKeepAlivePeriod,
+ );
+ if (hKeepAlivePeriod != null) {
+ reuseSettings['h-keep-alive-period'] = hKeepAlivePeriod;
+ }
+
+ return Object.keys(reuseSettings).length > 0
+ ? reuseSettings
+ : undefined;
+ };
+
+ const toStringHeaderMap = (headers) => {
+ if (!isPlainObject(headers)) {
+ return undefined;
+ }
+
+ const parsedHeaders = {};
+ for (const [key, value] of Object.entries(headers)) {
+ if (typeof value === 'string' && value !== '') {
+ parsedHeaders[key] = value;
+ }
+ }
+
+ return Object.keys(parsedHeaders).length > 0
+ ? parsedHeaders
+ : undefined;
+ };
+
+ const cloneUnsupportedXhttpValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(cloneUnsupportedXhttpValue);
+ }
+
+ if (isPlainObject(value)) {
+ const clonedValue = {};
+ for (const [key, entryValue] of Object.entries(value)) {
+ clonedValue[key] = cloneUnsupportedXhttpValue(entryValue);
+ }
+ return clonedValue;
+ }
+
+ return value;
+ };
+
+ const compactUnsupportedXhttpValue = (value) => {
+ if (Array.isArray(value)) {
+ return value
+ .map(compactUnsupportedXhttpValue)
+ .filter((entryValue) => entryValue !== undefined);
+ }
+
+ if (!isPlainObject(value)) {
+ return value;
+ }
+
+ const compactedValue = {};
+ for (const [key, entryValue] of Object.entries(value)) {
+ const compactedEntryValue =
+ compactUnsupportedXhttpValue(entryValue);
+ if (compactedEntryValue !== undefined) {
+ compactedValue[key] = compactedEntryValue;
+ }
+ }
+
+ return Object.keys(compactedValue).length > 0
+ ? compactedValue
+ : undefined;
+ };
+
+ const setUnsupportedXhttpField = (target, key, value) => {
+ const normalizedValue = compactUnsupportedXhttpValue(
+ cloneUnsupportedXhttpValue(value),
+ );
+ if (normalizedValue !== undefined) {
+ target[key] = normalizedValue;
+ }
+ };
+
+ const collectUnsupportedXhttpHeaders = (headers) => {
+ if (headers == null) {
+ return undefined;
+ }
+
+ if (!isPlainObject(headers)) {
+ return cloneUnsupportedXhttpValue(headers);
+ }
+
+ const unsupportedHeaders = {};
+ for (const [key, value] of Object.entries(headers)) {
+ if (typeof value === 'string' && value !== '') {
+ continue;
+ }
+
+ setUnsupportedXhttpField(unsupportedHeaders, key, value);
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedHeaders);
+ };
+
+ const isSupportedXmuxFieldValue = (key, value) => {
+ if (
+ [
+ 'maxConnections',
+ 'maxConcurrency',
+ 'cMaxReuseTimes',
+ 'hMaxRequestTimes',
+ 'hMaxReusableSecs',
+ ].includes(key)
+ ) {
+ return normalizeXhttpNonNegativeRange(value) != null;
+ }
+
+ if (key === 'hKeepAlivePeriod') {
+ return normalizeXhttpIntegerValue(value) != null;
+ }
+
+ return false;
+ };
+
+ const collectUnsupportedXmux = (xmux) => {
+ if (xmux == null) {
+ return undefined;
+ }
+
+ if (!isPlainObject(xmux)) {
+ return cloneUnsupportedXhttpValue(xmux);
+ }
+
+ const unsupportedXmux = {};
+ for (const [key, value] of Object.entries(xmux)) {
+ if (isSupportedXmuxFieldValue(key, value)) {
+ continue;
+ }
+
+ setUnsupportedXhttpField(unsupportedXmux, key, value);
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedXmux);
+ };
+
+ const collectUnsupportedXhttpExtra = (extra) => {
+ if (extra == null) {
+ return undefined;
+ }
+
+ if (!isPlainObject(extra)) {
+ return cloneUnsupportedXhttpValue(extra);
+ }
+
+ const unsupportedExtra = {};
+ for (const [key, value] of Object.entries(extra)) {
+ switch (key) {
+ case 'headers': {
+ const unsupportedHeaders =
+ collectUnsupportedXhttpHeaders(value);
+ if (unsupportedHeaders !== undefined) {
+ unsupportedExtra.headers = unsupportedHeaders;
+ }
+ break;
+ }
+ case 'noGRPCHeader':
+ case 'xPaddingObfsMode':
+ if (value !== true) {
+ setUnsupportedXhttpField(
+ unsupportedExtra,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'xPaddingBytes':
+ case 'xPaddingKey':
+ case 'xPaddingHeader':
+ case 'xPaddingPlacement':
+ case 'xPaddingMethod':
+ case 'uplinkHTTPMethod':
+ case 'sessionPlacement':
+ case 'sessionKey':
+ case 'seqPlacement':
+ case 'seqKey':
+ case 'uplinkDataPlacement':
+ case 'uplinkDataKey':
+ if (!isNotBlank(value)) {
+ setUnsupportedXhttpField(
+ unsupportedExtra,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'uplinkChunkSize':
+ if (normalizeXhttpNonNegativeRange(value) == null) {
+ setUnsupportedXhttpField(
+ unsupportedExtra,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'scMaxEachPostBytes':
+ if (normalizeXhttpScalarUpperBound(value) == null) {
+ setUnsupportedXhttpField(
+ unsupportedExtra,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'scMinPostsIntervalMs':
+ if (normalizeXhttpPositiveRange(value) == null) {
+ setUnsupportedXhttpField(
+ unsupportedExtra,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'xmux': {
+ const unsupportedXmux = collectUnsupportedXmux(value);
+ if (unsupportedXmux !== undefined) {
+ unsupportedExtra.xmux = unsupportedXmux;
+ }
+ break;
+ }
+ default:
+ setUnsupportedXhttpField(unsupportedExtra, key, value);
+ break;
+ }
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedExtra);
+ };
+
+ const collectUnsupportedNestedXhttpSettings = (xhttpSettings) => {
+ if (xhttpSettings == null) {
+ return undefined;
+ }
+
+ if (!isPlainObject(xhttpSettings)) {
+ return cloneUnsupportedXhttpValue(xhttpSettings);
+ }
+
+ const unsupportedXhttpSettings = {};
+ if (
+ Object.prototype.hasOwnProperty.call(xhttpSettings, 'path') &&
+ !isNotBlank(xhttpSettings.path)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedXhttpSettings,
+ 'path',
+ xhttpSettings.path,
+ );
+ }
+ if (
+ Object.prototype.hasOwnProperty.call(xhttpSettings, 'host') &&
+ !isNotBlank(xhttpSettings.host)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedXhttpSettings,
+ 'host',
+ xhttpSettings.host,
+ );
+ }
+
+ if (
+ Object.prototype.hasOwnProperty.call(xhttpSettings, 'mode') &&
+ !isNotBlank(xhttpSettings.mode)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedXhttpSettings,
+ 'mode',
+ xhttpSettings.mode,
+ );
+ }
+
+ const inlineExtra = {};
+ for (const [key, value] of Object.entries(xhttpSettings)) {
+ if (['path', 'host', 'mode', 'extra'].includes(key)) {
+ continue;
+ }
+ inlineExtra[key] = value;
+ }
+
+ const unsupportedInlineExtra =
+ collectUnsupportedXhttpExtra(inlineExtra);
+ if (isPlainObject(unsupportedInlineExtra)) {
+ Object.assign(unsupportedXhttpSettings, unsupportedInlineExtra);
+ }
+
+ if (Object.prototype.hasOwnProperty.call(xhttpSettings, 'extra')) {
+ const unsupportedExtra = collectUnsupportedXhttpExtra(
+ xhttpSettings.extra,
+ );
+ if (unsupportedExtra !== undefined) {
+ unsupportedXhttpSettings.extra = unsupportedExtra;
+ }
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedXhttpSettings);
+ };
+
+ const collectUnsupportedDownloadSettings = (downloadSettings) => {
+ if (downloadSettings == null) {
+ return undefined;
+ }
+
+ if (!isPlainObject(downloadSettings)) {
+ return cloneUnsupportedXhttpValue(downloadSettings);
+ }
+
+ const unsupportedDownloadSettings = {};
+ for (const [key, value] of Object.entries(downloadSettings)) {
+ switch (key) {
+ case 'address':
+ if (!isNotBlank(value)) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'port':
+ if (
+ normalizeXhttpIntegerValue(value, {
+ allowNegative: false,
+ }) == null
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ }
+ break;
+ case 'security': {
+ const normalizedSecurity =
+ typeof value === 'string'
+ ? value.toLowerCase()
+ : '';
+ if (!['tls', 'reality'].includes(normalizedSecurity)) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ }
+ break;
+ }
+ case 'tlsSettings': {
+ if (!isPlainObject(value)) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ break;
+ }
+
+ const unsupportedTlsSettings = {};
+ const hasSupportedEchConfigList =
+ isSupportedXrayEchConfigList(value.echConfigList);
+ for (const [tlsKey, tlsValue] of Object.entries(
+ value,
+ )) {
+ switch (tlsKey) {
+ case 'serverName':
+ case 'fingerprint':
+ if (!isNotBlank(tlsValue)) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ case 'echConfigList':
+ if (
+ !isSupportedXrayEchConfigList(tlsValue)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ case 'echForceQuery':
+ if (
+ !hasSupportedEchConfigList ||
+ !isSupportedXrayEchForceQuery(tlsValue)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ case 'echSockopt':
+ if (
+ !hasSupportedEchConfigList ||
+ !isPlainObject(tlsValue)
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ case 'alpn':
+ if (
+ !(
+ Array.isArray(tlsValue) &&
+ tlsValue.length > 0 &&
+ tlsValue.every(
+ (item) =>
+ typeof item === 'string' &&
+ item !== '',
+ )
+ )
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ case 'allowInsecure':
+ if (tlsValue !== true) {
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ }
+ break;
+ default:
+ setUnsupportedXhttpField(
+ unsupportedTlsSettings,
+ tlsKey,
+ tlsValue,
+ );
+ break;
+ }
+ }
+
+ const compactedTlsSettings =
+ compactUnsupportedXhttpValue(
+ unsupportedTlsSettings,
+ );
+ if (compactedTlsSettings !== undefined) {
+ unsupportedDownloadSettings.tlsSettings =
+ compactedTlsSettings;
+ }
+ break;
+ }
+ case 'realitySettings': {
+ if (!isPlainObject(value)) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ break;
+ }
+
+ const unsupportedRealitySettings = {};
+ for (const [realityKey, realityValue] of Object.entries(
+ value,
+ )) {
+ switch (realityKey) {
+ case 'publicKey':
+ case 'shortId':
+ case 'serverName':
+ case 'fingerprint':
+ if (!isNotBlank(realityValue)) {
+ setUnsupportedXhttpField(
+ unsupportedRealitySettings,
+ realityKey,
+ realityValue,
+ );
+ }
+ break;
+ default:
+ setUnsupportedXhttpField(
+ unsupportedRealitySettings,
+ realityKey,
+ realityValue,
+ );
+ break;
+ }
+ }
+
+ const compactedRealitySettings =
+ compactUnsupportedXhttpValue(
+ unsupportedRealitySettings,
+ );
+ if (compactedRealitySettings !== undefined) {
+ unsupportedDownloadSettings.realitySettings =
+ compactedRealitySettings;
+ }
+ break;
+ }
+ case 'xhttpSettings': {
+ const unsupportedXhttpSettings =
+ collectUnsupportedNestedXhttpSettings(value);
+ if (unsupportedXhttpSettings !== undefined) {
+ unsupportedDownloadSettings.xhttpSettings =
+ unsupportedXhttpSettings;
+ }
+ break;
+ }
+ case 'network': {
+ const normalizedNetwork =
+ typeof value === 'string'
+ ? value.toLowerCase()
+ : '';
+ if (
+ normalizedNetwork !== 'xhttp' &&
+ normalizedNetwork !== 'splithttp'
+ ) {
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ }
+ break;
+ }
+ default:
+ setUnsupportedXhttpField(
+ unsupportedDownloadSettings,
+ key,
+ value,
+ );
+ break;
+ }
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedDownloadSettings);
+ };
+
+ const collectUnsupportedRootXhttpExtra = (
+ extra,
+ { parsedDownloadSettings } = {},
+ ) => {
+ if (!isPlainObject(extra)) {
+ return undefined;
+ }
+
+ const {
+ downloadSettings: rawDownloadSettings,
+ ...rootInlineExtra
+ } = extra;
+
+ const unsupportedExtra =
+ collectUnsupportedXhttpExtra(rootInlineExtra) || {};
+
+ if (
+ Object.prototype.hasOwnProperty.call(extra, 'downloadSettings')
+ ) {
+ const unsupportedDownloadSettings =
+ collectUnsupportedDownloadSettings(rawDownloadSettings);
+ if (unsupportedDownloadSettings !== undefined) {
+ unsupportedExtra.downloadSettings =
+ unsupportedDownloadSettings;
+ }
+ }
+
+ return compactUnsupportedXhttpValue(unsupportedExtra);
+ };
+
+ const applyXhttpExtraFields = (target, extra) => {
+ if (!isPlainObject(target) || !isPlainObject(extra)) {
+ return;
+ }
+
+ const parsedHeaders = toStringHeaderMap(extra.headers);
+ if (parsedHeaders) {
+ const headers = { ...(target.headers || {}) };
+ for (const [key, value] of Object.entries(parsedHeaders)) {
+ if (/^host$/i.test(key)) {
+ if (
+ !Object.prototype.hasOwnProperty.call(
+ headers,
+ 'Host',
+ ) &&
+ !Object.prototype.hasOwnProperty.call(
+ headers,
+ 'host',
+ )
+ ) {
+ headers.Host = value;
+ }
+ continue;
+ }
+ headers[key] = value;
+ }
+ if (Object.keys(headers).length > 0) {
+ target.headers = headers;
+ }
+ }
+
+ if (extra.noGRPCHeader === true) {
+ target['no-grpc-header'] = true;
+ }
+ if (isNotBlank(extra.xPaddingBytes)) {
+ target['x-padding-bytes'] = extra.xPaddingBytes;
+ }
+ if (extra.xPaddingObfsMode === true) {
+ target['x-padding-obfs-mode'] = true;
+ }
+ if (isNotBlank(extra.xPaddingKey)) {
+ target['x-padding-key'] = extra.xPaddingKey;
+ }
+ if (isNotBlank(extra.xPaddingHeader)) {
+ target['x-padding-header'] = extra.xPaddingHeader;
+ }
+ if (isNotBlank(extra.xPaddingPlacement)) {
+ target['x-padding-placement'] = extra.xPaddingPlacement;
+ }
+ if (isNotBlank(extra.xPaddingMethod)) {
+ target['x-padding-method'] = extra.xPaddingMethod;
+ }
+ if (isNotBlank(extra.uplinkHTTPMethod)) {
+ target['uplink-http-method'] = extra.uplinkHTTPMethod;
+ }
+ if (isNotBlank(extra.sessionPlacement)) {
+ target['session-placement'] = extra.sessionPlacement;
+ }
+ if (isNotBlank(extra.sessionKey)) {
+ target['session-key'] = extra.sessionKey;
+ }
+ if (isNotBlank(extra.seqPlacement)) {
+ target['seq-placement'] = extra.seqPlacement;
+ }
+ if (isNotBlank(extra.seqKey)) {
+ target['seq-key'] = extra.seqKey;
+ }
+ if (isNotBlank(extra.uplinkDataPlacement)) {
+ target['uplink-data-placement'] = extra.uplinkDataPlacement;
+ }
+ if (isNotBlank(extra.uplinkDataKey)) {
+ target['uplink-data-key'] = extra.uplinkDataKey;
+ }
+
+ const uplinkChunkSize = normalizeXhttpNonNegativeRange(
+ extra.uplinkChunkSize,
+ );
+ if (uplinkChunkSize != null) {
+ target['uplink-chunk-size'] = uplinkChunkSize;
+ }
+
+ const scMaxEachPostBytes = normalizeXhttpScalarUpperBound(
+ extra.scMaxEachPostBytes,
+ );
+ if (scMaxEachPostBytes != null) {
+ target['sc-max-each-post-bytes'] = scMaxEachPostBytes;
+ }
+
+ const scMinPostsIntervalMs = normalizeXhttpPositiveRange(
+ extra.scMinPostsIntervalMs,
+ );
+ if (scMinPostsIntervalMs != null) {
+ target['sc-min-posts-interval-ms'] = scMinPostsIntervalMs;
+ }
+
+ const reuseSettings = mapXmuxToReuseSettings(extra.xmux);
+ if (reuseSettings) {
+ target['reuse-settings'] = reuseSettings;
+ }
+ };
+
+ const parseDownloadSettings = (downloadSettings) => {
+ if (!isPlainObject(downloadSettings)) {
+ return undefined;
+ }
+
+ const parsedDownloadSettings = {};
+ const downloadNetwork =
+ typeof downloadSettings.network === 'string'
+ ? downloadSettings.network.toLowerCase()
+ : '';
+ if (
+ downloadNetwork === 'xhttp' ||
+ downloadNetwork === 'splithttp'
+ ) {
+ parsedDownloadSettings.network = 'xhttp';
+ }
+ if (isNotBlank(downloadSettings.address)) {
+ parsedDownloadSettings.server = downloadSettings.address;
+ }
+
+ const parsedPort = normalizeXhttpIntegerValue(
+ downloadSettings.port,
+ {
+ allowNegative: false,
+ },
+ );
+ if (parsedPort != null) {
+ parsedDownloadSettings.port = parsedPort;
+ }
+
+ const downloadSecurity =
+ typeof downloadSettings.security === 'string'
+ ? downloadSettings.security.toLowerCase()
+ : '';
+ if (downloadSecurity === 'tls' || downloadSecurity === 'reality') {
+ parsedDownloadSettings.tls = true;
+ }
+
+ if (isPlainObject(downloadSettings.tlsSettings)) {
+ if (isNotBlank(downloadSettings.tlsSettings.serverName)) {
+ parsedDownloadSettings.servername =
+ downloadSettings.tlsSettings.serverName;
+ }
+ if (isNotBlank(downloadSettings.tlsSettings.fingerprint)) {
+ parsedDownloadSettings['client-fingerprint'] =
+ downloadSettings.tlsSettings.fingerprint;
+ }
+ if (
+ Array.isArray(downloadSettings.tlsSettings.alpn) &&
+ downloadSettings.tlsSettings.alpn.length > 0 &&
+ downloadSettings.tlsSettings.alpn.every(
+ (item) => typeof item === 'string' && item !== '',
+ )
+ ) {
+ parsedDownloadSettings.alpn =
+ downloadSettings.tlsSettings.alpn;
+ }
+ if (downloadSettings.tlsSettings.allowInsecure === true) {
+ parsedDownloadSettings['skip-cert-verify'] = true;
+ }
+ const echOpts = buildMihomoEchOptsFromXrayFields({
+ echConfigList: downloadSettings.tlsSettings.echConfigList,
+ echForceQuery: downloadSettings.tlsSettings.echForceQuery,
+ echSockopt: downloadSettings.tlsSettings.echSockopt,
+ });
+ if (echOpts) {
+ parsedDownloadSettings['ech-opts'] = echOpts;
+ }
+ }
+
+ let realityOpts;
+ if (isPlainObject(downloadSettings.realitySettings)) {
+ realityOpts = {};
+ if (isNotBlank(downloadSettings.realitySettings.publicKey)) {
+ realityOpts['public-key'] =
+ downloadSettings.realitySettings.publicKey;
+ }
+ if (isNotBlank(downloadSettings.realitySettings.shortId)) {
+ realityOpts['short-id'] =
+ downloadSettings.realitySettings.shortId;
+ }
+ if (isNotBlank(downloadSettings.realitySettings.serverName)) {
+ parsedDownloadSettings.servername =
+ downloadSettings.realitySettings.serverName;
+ }
+ if (isNotBlank(downloadSettings.realitySettings.fingerprint)) {
+ parsedDownloadSettings['client-fingerprint'] =
+ downloadSettings.realitySettings.fingerprint;
+ }
+ }
+ if (downloadSecurity === 'reality') {
+ // Keep an explicit empty reality marker so the producer can
+ // route invalid nested Reality configs through the shared
+ // "missing public-key" export rejection path.
+ parsedDownloadSettings['reality-opts'] = realityOpts || {};
+ } else if (realityOpts && Object.keys(realityOpts).length > 0) {
+ parsedDownloadSettings['reality-opts'] = realityOpts;
+ }
+
+ if (isPlainObject(downloadSettings.xhttpSettings)) {
+ if (isNotBlank(downloadSettings.xhttpSettings.path)) {
+ parsedDownloadSettings.path =
+ downloadSettings.xhttpSettings.path;
+ }
+ if (isNotBlank(downloadSettings.xhttpSettings.host)) {
+ parsedDownloadSettings.host =
+ downloadSettings.xhttpSettings.host;
+ }
+ if (isNotBlank(downloadSettings.xhttpSettings.mode)) {
+ parsedDownloadSettings.mode =
+ downloadSettings.xhttpSettings.mode;
+ }
+ applyXhttpExtraFields(
+ parsedDownloadSettings,
+ downloadSettings.xhttpSettings,
+ );
+ if (isPlainObject(downloadSettings.xhttpSettings.extra)) {
+ applyXhttpExtraFields(
+ parsedDownloadSettings,
+ downloadSettings.xhttpSettings.extra,
+ );
+ }
+ }
+
+ return Object.keys(parsedDownloadSettings).length > 0
+ ? parsedDownloadSettings
+ : undefined;
+ };
+
+ line = line.split('vless://')[1];
+ let isShadowrocket;
+ let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
+ if (!parsed) {
+ // eslint-disable-next-line no-unused-vars
+ let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line);
+ line = `${Base64.decode(base64)}${other}`;
+ parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
+ isShadowrocket = true;
+ }
+ // eslint-disable-next-line no-unused-vars
+ let [__, uuid, server, port, ___, addons = '', name] = parsed;
+ if (isShadowrocket) {
+ uuid = uuid.replace(/^.*?:/g, '');
+ }
+
+ port = parseInt(`${port}`, 10);
+ uuid = decodeURIComponent(uuid);
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+
+ const proxy = {
+ type: 'vless',
+ name,
+ server,
+ port,
+ uuid,
+ udp: true,
+ };
+ const params = {};
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ const [key, valueRaw] = addon.split('=');
+ let value = valueRaw;
+ value = decodeURIComponent(valueRaw);
+ params[key] = value;
+ }
+ }
+
+ proxy.name =
+ name ??
+ params.remarks ??
+ params.remark ??
+ `VLESS ${server}:${port}`;
+
+ proxy.tls = params.security && params.security !== 'none';
+ if (params.pbk) {
+ params.security = 'reality';
+ }
+ if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
+ proxy.tls = true;
+ params.security = params.security ?? 'reality';
+ }
+ proxy.sni = params.sni || params.peer;
+ proxy.flow = params.flow;
+ if (!proxy.flow && isShadowrocket && params.xtls) {
+ // "none" is undefined
+ const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][
+ params.xtls
+ ];
+ if (flow) {
+ proxy.flow = flow;
+ }
+ }
+ proxy['client-fingerprint'] = params.fp;
+ proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
+ proxy._echConfigList = getIfPresent(params.ech);
+ const echOpts = buildMihomoEchOptsFromXrayFields({
+ echConfigList: params.ech,
+ });
+ if (echOpts) {
+ proxy['ech-opts'] = echOpts;
+ }
+ proxy['tls-fingerprint'] = getIfPresent(params.pcs);
+ proxy._h2 = /(TRUE)|1/i.test(params.h2);
+
+ switch (`${params.packetEncoding || ''}`.toLowerCase()) {
+ case 'none':
+ break;
+ case 'packet':
+ proxy['packet-addr'] = true;
+ break;
+ default:
+ proxy.xudp = true;
+ break;
+ }
+
+ if (['reality'].includes(params.security)) {
+ const opts = {};
+ if (params.pbk) {
+ opts['public-key'] = params.pbk;
+ }
+ if (params.sid) {
+ opts['short-id'] = params.sid;
+ }
+ if (params.spx) {
+ opts['_spider-x'] = params.spx;
+ }
+ if (Object.keys(opts).length > 0) {
+ // proxy[`${params.security}-opts`] = opts;
+ proxy[`${params.security}-opts`] = opts;
+ }
+ }
+ let httpupgrade = false;
+ proxy.network = params.type || 'tcp';
+ if (proxy.network === 'tcp' && params.headerType === 'http') {
+ proxy.network = 'http';
+ } else if (proxy.network === 'http') {
+ proxy.network = 'h2';
+ } else if (proxy.network === 'httpupgrade') {
+ proxy.network = 'ws';
+ httpupgrade = true;
+ }
+ if (!params.type && isShadowrocket && params.obfs) {
+ proxy.network = params.obfs;
+ if (['none'].includes(proxy.network)) {
+ proxy.network = 'tcp';
+ }
+ }
+ if (['websocket'].includes(proxy.network)) {
+ proxy.network = 'ws';
+ }
+
+ if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
+ const opts = {};
+ let pathEarlyData = '';
+ const host = params.host ?? params.obfsParam;
+ if (host) {
+ if (params.obfsParam) {
+ try {
+ const parsed = JSON.parse(host);
+ opts.headers = parsed;
+ } catch (e) {
+ opts.headers = { Host: host };
+ }
+ } else {
+ opts.headers = { Host: host };
+ }
+ if (['xhttp'].includes(proxy.network) && opts.headers?.Host) {
+ opts.host = opts.headers.Host;
+ delete opts.headers.Host;
+ if (Object.keys(opts.headers).length === 0) {
+ delete opts.headers;
+ }
+ }
+ }
+ if (params.serviceName) {
+ opts[`${proxy.network}-service-name`] = params.serviceName;
+ if (['grpc'].includes(proxy.network) && params.authority) {
+ opts['_grpc-authority'] = params.authority;
+ }
+ } else if (isShadowrocket && params.path) {
+ if (!['ws', 'http', 'h2'].includes(proxy.network)) {
+ opts[`${proxy.network}-service-name`] = params.path;
+ delete params.path;
+ }
+ }
+ if (params.path) {
+ let transportPath = params.path;
+ if (proxy.network === 'ws') {
+ const extracted = extractEarlyDataFromPath(transportPath);
+ transportPath = extracted.path;
+ pathEarlyData = extracted.ed;
+ }
+ opts.path = transportPath;
+ }
+ if (proxy.network === 'http' && params.method) {
+ opts.method = params.method;
+ }
+ // https://github.com/XTLS/Xray-core/issues/91
+ if (['grpc'].includes(proxy.network)) {
+ opts['_grpc-type'] = params.mode || 'gun';
+ }
+ if (httpupgrade) {
+ opts['v2ray-http-upgrade'] = true;
+ }
+ const earlyDataRaw = pathEarlyData || params.ed;
+ if (earlyDataRaw) {
+ const maxEarlyData = parseEarlyDataSize(earlyDataRaw);
+ if (httpupgrade) {
+ opts['v2ray-http-upgrade-fast-open'] = true;
+ opts['_v2ray-http-upgrade-ed'] = `${earlyDataRaw}`;
+ } else if (proxy.network === 'ws') {
+ opts['max-early-data'] = maxEarlyData;
+ opts['early-data-header-name'] =
+ params.eh || 'Sec-WebSocket-Protocol';
+ }
+ }
+ if (params.eh && (proxy.network === 'ws' || httpupgrade)) {
+ opts['early-data-header-name'] = params.eh;
+ }
+ if (Object.keys(opts).length > 0) {
+ proxy[`${proxy.network}-opts`] = opts;
+ }
+ if (proxy.network === 'kcp') {
+ // mKCP 种子。省略时不使用种子,但不可以为空字符串。建议 mKCP 用户使用 seed。
+ if (params.seed) {
+ proxy.seed = params.seed;
+ }
+ // mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none,即不使用伪装头部,但不可以为空字符串。
+ proxy.headerType = params.headerType || 'none';
+ }
+ if (params.extra && !['xhttp'].includes(proxy.network)) {
+ proxy._extra = params.extra;
+ }
+ if (['xhttp'].includes(proxy.network)) {
+ let extra = {};
+ let invalidRawExtra;
+ try {
+ extra = params.extra ? JSON.parse(params.extra) : {};
+ } catch (e) {
+ $.error(
+ `Failed to parse extra field as JSON: ${params.extra}`,
+ );
+ invalidRawExtra = params.extra;
+ }
+ const xhttpOpts = {
+ ...(proxy[`${proxy.network}-opts`] || {}),
+ };
+ if (params.mode) {
+ xhttpOpts.mode = params.mode;
+ }
+ applyXhttpExtraFields(xhttpOpts, extra);
+ const downloadSettings = parseDownloadSettings(
+ extra?.downloadSettings,
+ );
+ if (downloadSettings) {
+ xhttpOpts['download-settings'] = downloadSettings;
+ }
+ if (Object.keys(xhttpOpts).length > 0) {
+ proxy[`${proxy.network}-opts`] = xhttpOpts;
+ }
+ if (invalidRawExtra != null) {
+ // Keep the raw invalid extra string so URI exports can
+ // round-trip it verbatim even though Mihomo cannot model it.
+ proxy._extra = invalidRawExtra;
+ }
+
+ // IMPORTANT: for VLESS xhttp we only keep URI extra fields that
+ // Mihomo does not model structurally in `_extra_unsupported`.
+ // Supported fields must round-trip through the structured node
+ // so later edits are reflected on export, while unsupported
+ // fields still survive VLESS URI -> node -> VLESS URI flows.
+ const unsupportedExtra = collectUnsupportedRootXhttpExtra(
+ extra,
+ {
+ parsedDownloadSettings: downloadSettings,
+ },
+ );
+ if (unsupportedExtra) {
+ proxy._extra_unsupported = unsupportedExtra;
+ }
+ } else if (params.mode) {
+ proxy._mode = params.mode;
+ }
+ }
+ if (params.encryption) {
+ proxy.encryption = params.encryption;
+ }
+ if (params.pqv) {
+ proxy._pqv = params.pqv;
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_AnyTLS() {
+ const name = 'URI AnyTLS Parser';
+ const test = (line) => {
+ return /^anytls:\/\//.test(line);
+ };
+ const parse = (line) => {
+ const parsed = URI_VLESS().parse(line.replace('anytls', 'vless'));
+ // 偷个懒
+ line = line.split(/anytls:\/\//)[1];
+ // eslint-disable-next-line no-unused-vars
+ let [__, password, server, port, addons = '', name] =
+ /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
+ password = decodeURIComponent(password);
+ port = parseInt(`${port}`, 10);
+ if (isNaN(port)) {
+ port = 443;
+ }
+ password = decodeURIComponent(password);
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+ name = name ?? `AnyTLS ${server}:${port}`;
+
+ const proxy = {
+ ...parsed,
+ uuid: undefined,
+ type: 'anytls',
+ name,
+ server,
+ port,
+ password,
+ };
+
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ let [key, value] = addon.split('=');
+ key = key.replace(/_/g, '-');
+ value = decodeURIComponent(value);
+ if (['alpn'].includes(key)) {
+ proxy[key] = value ? value.split(',') : undefined;
+ } else if (['insecure'].includes(key)) {
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
+ } else if (['udp'].includes(key)) {
+ proxy[key] = /(TRUE)|1/i.test(value);
+ } else if (!Object.keys(proxy).includes(key)) {
+ proxy[key] = value;
+ }
+ }
+ }
+ if (['tcp'].includes(proxy.network) && !proxy['reality-opts']) {
+ delete proxy.network;
+ delete proxy.security;
+ }
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_Hysteria2() {
+ const name = 'URI Hysteria2 Parser';
+ const test = (line) => {
+ return /^(hysteria2|hy2):\/\//.test(line);
+ };
+ const parse = (line) => {
+ line = line.split(/(hysteria2|hy2):\/\//)[2];
+ // 端口跳跃有两种写法:
+ // 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
+ // 端口部分支持 端口跳跃 的「多端口地址格式」。
+ // https://hysteria.network/zh/docs/advanced/Port-Hopping
+ // 2. 参数 mport
+ let ports;
+ /* eslint-disable no-unused-vars */
+ let [
+ __,
+ password,
+ server,
+ ___,
+ port,
+ ____,
+ _____,
+ ______,
+ _______,
+ ________,
+ addons = '',
+ name,
+ ] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
+ line,
+ );
+
+ /* eslint-enable no-unused-vars */
+ if (/^\d+$/.test(port)) {
+ port = parseInt(`${port}`, 10);
+ if (isNaN(port)) {
+ port = 443;
+ }
+ } else if (port) {
+ ports = port;
+ port = getRandomPort(ports);
+ } else {
+ port = 443;
+ }
+
+ password = decodeURIComponent(password);
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+ name = name ?? `Hysteria2 ${server}:${port}`;
+
+ const proxy = {
+ type: 'hysteria2',
+ name,
+ server,
+ port,
+ ports,
+ password,
+ };
+
+ const params = {};
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ const [key, valueRaw] = addon.split('=');
+ let value = valueRaw;
+ value = decodeURIComponent(valueRaw);
+ params[key] = value;
+ }
+ }
+
+ proxy.sni = params.sni;
+ if (!proxy.sni && params.peer) {
+ proxy.sni = params.peer;
+ }
+ if (params.obfs && params.obfs !== 'none') {
+ proxy.obfs = params.obfs;
+ }
+ if (params.mport) {
+ proxy.ports = params.mport;
+ }
+ proxy['obfs-password'] = params['obfs-password'];
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
+ proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
+ proxy['tls-fingerprint'] = params.pinSHA256;
+ let hop_interval = params['hop-interval'] || params['hop_interval'];
+
+ if (hop_interval != null) {
+ proxy['hop-interval'] = hop_interval;
+ }
+ let keepalive = params['keepalive'];
+
+ if (/^\d+$/.test(keepalive)) {
+ proxy['keepalive'] = parseInt(`${keepalive}`, 10);
+ }
+ if (params.upmbps) {
+ proxy.up = params.upmbps;
+ }
+ if (params.downmbps) {
+ proxy.down = params.downmbps;
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_Hysteria() {
+ const name = 'URI Hysteria Parser';
+ const test = (line) => {
+ return /^(hysteria|hy):\/\//.test(line);
+ };
+ const parse = (line) => {
+ line = line.split(/(hysteria|hy):\/\//)[2];
+ // eslint-disable-next-line no-unused-vars
+ let [__, server, ___, port, ____, addons = '', name] =
+ /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
+ port = parseInt(`${port}`, 10);
+ if (isNaN(port)) {
+ port = 443;
+ }
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+ name = name ?? `Hysteria ${server}:${port}`;
+
+ const proxy = {
+ type: 'hysteria',
+ name,
+ server,
+ port,
+ };
+ const params = {};
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ let [key, value] = addon.split('=');
+ key = key.replace(/_/, '-');
+ value = decodeURIComponent(value);
+ if (['alpn'].includes(key)) {
+ proxy[key] = value ? value.split(',') : undefined;
+ } else if (['insecure'].includes(key)) {
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
+ } else if (['auth'].includes(key)) {
+ proxy['auth-str'] = value;
+ } else if (['mport'].includes(key)) {
+ proxy['ports'] = value;
+ } else if (['obfsParam'].includes(key)) {
+ proxy['obfs'] = value;
+ } else if (['upmbps'].includes(key)) {
+ proxy['up'] = value;
+ } else if (['downmbps'].includes(key)) {
+ proxy['down'] = value;
+ } else if (['obfs'].includes(key)) {
+ // obfs: Obfuscation mode (optional, empty or "xplus")
+ proxy['_obfs'] = value || '';
+ } else if (['fast-open', 'peer'].includes(key)) {
+ params[key] = value;
+ } else if (!Object.keys(proxy).includes(key)) {
+ proxy[key] = value;
+ }
+ }
+ }
+
+ if (!proxy.sni && params.peer) {
+ proxy.sni = params.peer;
+ }
+ if (!proxy['fast-open'] && params.fastopen) {
+ proxy['fast-open'] = true;
+ }
+ if (!proxy.protocol) {
+ // protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
+ proxy.protocol = 'udp';
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_TUIC() {
+ const name = 'URI TUIC Parser';
+ const test = (line) => {
+ return /^tuic:\/\//.test(line);
+ };
+ const parse = (line) => {
+ line = line.split(/tuic:\/\//)[1];
+ // eslint-disable-next-line no-unused-vars
+ let [__, auth, server, port, addons = '', name] =
+ /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
+ auth = decodeURIComponent(auth);
+ let [uuid, ...passwordParts] = auth.split(':');
+ let password = passwordParts.join(':');
+ port = parseInt(`${port}`, 10);
+ if (isNaN(port)) {
+ port = 443;
+ }
+ password = decodeURIComponent(password);
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+ name = name ?? `TUIC ${server}:${port}`;
+
+ const proxy = {
+ type: 'tuic',
+ name,
+ server,
+ port,
+ password,
+ uuid,
+ };
+
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ let [key, value] = addon.split('=');
+ key = key.replace(/_/g, '-');
+ value = decodeURIComponent(value);
+ if (['alpn'].includes(key)) {
+ proxy[key] = value ? value.split(',') : undefined;
+ } else if (['allow-insecure', 'insecure'].includes(key)) {
+ proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
+ } else if (['fast-open'].includes(key)) {
+ proxy.tfo = true;
+ } else if (['disable-sni', 'reduce-rtt'].includes(key)) {
+ proxy[key] = /(TRUE)|1/i.test(value);
+ } else if (key === 'congestion-control') {
+ proxy['congestion-controller'] = value;
+ delete proxy[key];
+ } else if (!Object.keys(proxy).includes(key)) {
+ proxy[key] = value;
+ }
+ }
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function URI_WireGuard() {
+ const name = 'URI WireGuard Parser';
+ const test = (line) => {
+ return /^(wireguard|wg):\/\//.test(line);
+ };
+ const parse = (line) => {
+ line = line.split(/(wireguard|wg):\/\//)[2];
+ /* eslint-disable no-unused-vars */
+ let [
+ __,
+ ___,
+ privateKey,
+ server,
+ ____,
+ port,
+ _____,
+ addons = '',
+ name,
+ ] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
+ /* eslint-enable no-unused-vars */
+
+ port = parseInt(`${port}`, 10);
+ if (isNaN(port)) {
+ port = 51820;
+ }
+ privateKey = decodeURIComponent(privateKey);
+ if (name != null) {
+ name = decodeURIComponent(name);
+ }
+ name = name ?? `WireGuard ${server}:${port}`;
+ const proxy = {
+ type: 'wireguard',
+ name,
+ server,
+ port,
+ 'private-key': privateKey,
+ udp: true,
+ };
+ for (const addon of addons.split('&')) {
+ if (addon) {
+ const equalIndex = addon.indexOf('=');
+ let key;
+ let value;
+ if (equalIndex === -1) {
+ key = addon;
+ value = '';
+ } else {
+ key = addon.slice(0, equalIndex);
+ value = addon.slice(equalIndex + 1);
+ }
+ key = key.replace(/_/, '-');
+ value = decodeURIComponent(value);
+ if (['reserved'].includes(key)) {
+ const parsed = value
+ .split(',')
+ .map((i) => parseInt(i.trim(), 10))
+ .filter((i) => Number.isInteger(i));
+ if (parsed.length === 3) {
+ proxy[key] = parsed;
+ }
+ } else if (['address', 'ip'].includes(key)) {
+ value.split(',').map((i) => {
+ const parsed = parseWireGuardURIAddressValue(i);
+ if (!parsed) return;
+ if (parsed.family === 'ipv4') {
+ proxy.ip = parsed.address;
+ if (typeof parsed.cidr !== 'undefined') {
+ proxy['ip-cidr'] = parsed.cidr;
+ }
+ } else if (parsed.family === 'ipv6') {
+ proxy.ipv6 = parsed.address;
+ if (typeof parsed.cidr !== 'undefined') {
+ proxy['ipv6-cidr'] = parsed.cidr;
+ }
+ }
+ });
+ } else if (['mtu'].includes(key)) {
+ const parsed = parseInt(value.trim(), 10);
+ if (Number.isInteger(parsed)) {
+ proxy[key] = parsed;
+ }
+ } else if (/publickey/i.test(key)) {
+ proxy['public-key'] = value;
+ } else if (/privatekey/i.test(key)) {
+ proxy['private-key'] = value;
+ } else if (['udp'].includes(key)) {
+ proxy[key] = /(TRUE)|1/i.test(value);
+ } else if (![...Object.keys(proxy), 'flag'].includes(key)) {
+ proxy[key] = value;
+ }
+ }
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+// Trojan URI format
+function URI_Trojan() {
+ const name = 'URI Trojan Parser';
+ const test = (line) => {
+ return /^trojan:\/\//.test(line);
+ };
+
+ const parse = (line) => {
+ const matched = /^(trojan:\/\/.*?@.*?)(:(\d+))?\/?(\?.*?)?$/.exec(line);
+ const port = matched?.[2];
+ if (!port) {
+ line = line.replace(matched[1], `${matched[1]}:443`);
+ }
+ let [newLine, name] = line.split(/#(.+)/, 2);
+ const parser = getTrojanURIParser();
+ const proxy = parser.parse(newLine);
+ if (isNotBlank(name)) {
+ try {
+ proxy.name = decodeURIComponent(name);
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+function Clash_All() {
+ const name = 'Clash Parser';
+ const test = (line) => {
+ let proxy;
+ try {
+ proxy = JSON5.parse(line);
+ } catch (e) {
+ proxy = YAML.parse(line);
+ }
+ return !!proxy?.type;
+ };
+ const parse = (line) => {
+ let proxy;
+ try {
+ proxy = JSON5.parse(line);
+ } catch (e) {
+ proxy = YAML.parse(line);
+ }
+ if (
+ ![
+ 'gost-relay',
+ 'openvpn',
+ 'tailscale',
+ 'trusttunnel',
+ 'h2-connect',
+ 'naive',
+ 'anytls',
+ 'mieru',
+ 'masque',
+ 'sudoku',
+ 'juicity',
+ 'ss',
+ 'ssr',
+ 'vmess',
+ 'socks5',
+ 'http',
+ 'snell',
+ 'trojan',
+ 'tuic',
+ 'vless',
+ 'hysteria',
+ 'hysteria2',
+ 'wireguard',
+ 'ssh',
+ 'direct',
+ ].includes(proxy.type)
+ ) {
+ throw new Error(
+ `Clash does not support proxy with type: ${proxy.type}`,
+ );
+ }
+
+ // handle vmess sni
+ if (['vmess', 'vless'].includes(proxy.type) && proxy.servername) {
+ proxy.sni = proxy.servername;
+ delete proxy.servername;
+ }
+ if (proxy['server-cert-fingerprint']) {
+ proxy['tls-fingerprint'] = proxy['server-cert-fingerprint'];
+ }
+ if (proxy.fingerprint) {
+ proxy['tls-fingerprint'] = proxy.fingerprint;
+ }
+ if (proxy['dialer-proxy']) {
+ proxy['underlying-proxy'] = proxy['dialer-proxy'];
+ }
+
+ if (proxy['benchmark-url']) {
+ proxy['test-url'] = proxy['benchmark-url'];
+ }
+ if (proxy['benchmark-timeout']) {
+ proxy['test-timeout'] = proxy['benchmark-timeout'];
+ }
+
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+function QX_SS() {
+ const name = 'QX SS Parser';
+ const test = (line) => {
+ return (
+ /^shadowsocks\s*=/.test(line.split(',')[0].trim()) &&
+ line.indexOf('ssr-protocol') === -1
+ );
+ };
+ const parse = (line) => {
+ const parser = getQXParser();
+ return parser.parse(line);
+ };
+ return { name, test, parse };
+}
+
+function QX_SSR() {
+ const name = 'QX SSR Parser';
+ const test = (line) => {
+ return (
+ /^shadowsocks\s*=/.test(line.split(',')[0].trim()) &&
+ line.indexOf('ssr-protocol') !== -1
+ );
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_VMess() {
+ const name = 'QX VMess Parser';
+ const test = (line) => {
+ return /^vmess\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_VLESS() {
+ const name = 'QX VLESS Parser';
+ const test = (line) => {
+ return /^vless\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_AnyTLS() {
+ const name = 'QX AnyTLS Parser';
+ const test = (line) => {
+ return /^anytls\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_Trojan() {
+ const name = 'QX Trojan Parser';
+ const test = (line) => {
+ return /^trojan\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_Http() {
+ const name = 'QX HTTP Parser';
+ const test = (line) => {
+ return /^http\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function QX_Socks5() {
+ const name = 'QX Socks5 Parser';
+ const test = (line) => {
+ return /^socks5\s*=/.test(line.split(',')[0].trim());
+ };
+ const parse = (line) => getQXParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_SS() {
+ const name = 'Loon SS Parser';
+ const test = (line) => {
+ return (
+ line.split(',')[0].split('=')[1].trim().toLowerCase() ===
+ 'shadowsocks'
+ );
+ };
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_SSR() {
+ const name = 'Loon SSR Parser';
+ const test = (line) => {
+ return (
+ line.split(',')[0].split('=')[1].trim().toLowerCase() ===
+ 'shadowsocksr'
+ );
+ };
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_VMess() {
+ const name = 'Loon VMess Parser';
+ const test = (line) => {
+ // distinguish between surge vmess
+ return (
+ /^.*=\s*vmess/i.test(line.split(',')[0]) &&
+ line.indexOf('username') === -1
+ );
+ };
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_Vless() {
+ const name = 'Loon Vless Parser';
+ const test = (line) => {
+ return /^.*=\s*vless/i.test(line.split(',')[0]);
+ };
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_Trojan() {
+ const name = 'Loon Trojan Parser';
+ const test = (line) => {
+ return /^.*=\s*trojan/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+function Loon_AnyTLS() {
+ const name = 'Loon AnyTLS Parser';
+ const test = (line) => {
+ return /^.*=\s*anytls/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+function Loon_Hysteria2() {
+ const name = 'Loon Hysteria2 Parser';
+ const test = (line) => {
+ return /^.*=\s*Hysteria2/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_Http() {
+ const name = 'Loon HTTP Parser';
+ const test = (line) => {
+ return /^.*=\s*http/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+function Loon_Socks5() {
+ const name = 'Loon SOCKS5 Parser';
+ const test = (line) => {
+ return /^.*=\s*socks5/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => getLoonParser().parse(line);
+ return { name, test, parse };
+}
+
+function Loon_WireGuard() {
+ const name = 'Loon WireGuard Parser';
+ const test = (line) => {
+ return /^.*=\s*wireguard/i.test(line.split(',')[0]);
+ };
+
+ const parse = (line) => {
+ const name = line.match(
+ /(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
+ )?.[1];
+ line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
+ let peers = line.match(
+ /,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
+ )?.[1];
+ let serverPort = peers.match(
+ /(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
+ );
+ let server = serverPort?.[2];
+ let port = parseInt(serverPort?.[3], 10);
+ let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
+ if (mtu) {
+ mtu = parseInt(mtu, 10);
+ }
+ let keepalive = line.match(
+ /(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
+ )?.[2];
+ if (keepalive) {
+ keepalive = parseInt(keepalive, 10);
+ }
+ let reserved = peers.match(
+ /(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
+ )?.[2];
+ if (reserved) {
+ reserved = JSON.parse(reserved);
+ }
+
+ let dns;
+ let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
+ let dnsv6 = line.match(
+ /(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2];
+ if (dnsv4 || dnsv6) {
+ dns = [];
+ if (dnsv4) {
+ dns.push(dnsv4);
+ }
+ if (dnsv6) {
+ dns.push(dnsv6);
+ }
+ }
+ let allowedIps = peers
+ .match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
+ ?.split(',')
+ .map((i) => i.trim());
+ let preSharedKey = peers.match(
+ /(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2];
+ let ip = line.match(
+ /(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2];
+ let ipv6 = line.match(
+ /(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2];
+ let publicKey = peers.match(
+ /(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2];
+ // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
+ const proxy = {
+ type: 'wireguard',
+ name,
+ server,
+ port,
+ ip,
+ ipv6,
+ 'private-key': line.match(
+ /(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
+ )?.[2],
+ 'public-key': publicKey,
+ mtu,
+ keepalive,
+ reserved,
+ 'allowed-ips': allowedIps,
+ 'preshared-key': preSharedKey,
+ dns,
+ udp: true,
+ peers: [
+ {
+ server,
+ port,
+ ip,
+ ipv6,
+ 'public-key': publicKey,
+ 'pre-shared-key': preSharedKey,
+ 'allowed-ips': allowedIps,
+ reserved,
+ },
+ ],
+ };
+
+ proxy;
+ if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
+ proxy['remote-dns-resolve'] = true;
+ }
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+function Surge_Direct() {
+ const name = 'Surge Direct Parser';
+ const test = (line) => {
+ return /^.*=\s*direct/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+function Surge_AnyTLS() {
+ const name = 'Surge AnyTLS Parser';
+ const test = (line) => {
+ return /^.*=\s*anytls/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+function Surge_TrustTunnel() {
+ const name = 'Surge TrustTunnel Parser';
+ const test = (line) => {
+ return /^.*=\s*trust-tunnel/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+function Surge_H2Connect() {
+ const name = 'Surge HTTP/2 CONNECT Parser';
+ const test = (line) => {
+ return /^.*=\s*h2-connect/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+function Surge_SSH() {
+ const name = 'Surge SSH Parser';
+ const test = (line) => {
+ return /^.*=\s*ssh/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+function Surge_SS() {
+ const name = 'Surge SS Parser';
+ const test = (line) => {
+ return /^.*=\s*ss/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_VMess() {
+ const name = 'Surge VMess Parser';
+ const test = (line) => {
+ return (
+ /^.*=\s*vmess/.test(line.split(',')[0]) &&
+ line.indexOf('username') !== -1
+ );
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_Trojan() {
+ const name = 'Surge Trojan Parser';
+ const test = (line) => {
+ return /^.*=\s*trojan/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+const LOON_ONLY_OPTIONS =
+ /(^|,)\s*(fast-open|over-tls|tls-name|ip-mode|tls-cert-sha256|tls-pubkey-sha256)\s*=/i;
+
+function Surge_Http() {
+ const name = 'Surge HTTP Parser';
+ const test = (line) => {
+ return (
+ /^.*=\s*https?/.test(line.split(',')[0]) &&
+ !LOON_ONLY_OPTIONS.test(line)
+ );
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_Socks5() {
+ const name = 'Surge Socks5 Parser';
+ const test = (line) => {
+ return (
+ /^.*=\s*socks5(-tls)?/.test(line.split(',')[0]) &&
+ !LOON_ONLY_OPTIONS.test(line)
+ );
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_External() {
+ const name = 'Surge External Parser';
+ const test = (line) => {
+ return /^.*=\s*external/.test(line.split(',')[0]);
+ };
+ const parse = (line) => {
+ let parsed = /^\s*(.*?)\s*?=\s*?external\s*?,\s*(.*?)\s*$/.exec(line);
+
+ // eslint-disable-next-line no-unused-vars
+ let [_, name, other] = parsed;
+ line = other;
+
+ // exec = "/usr/bin/ssh" 或 exec = /usr/bin/ssh
+ let exec = /(,|^)\s*?exec\s*?=\s*"(.*?)"\s*?(,|$)/.exec(line)?.[2];
+ if (!exec) {
+ exec = /(,|^)\s*?exec\s*?=\s*(.*?)\s*?(,|$)/.exec(line)?.[2];
+ }
+
+ // local-port = "1080" 或 local-port = 1080
+ let localPort = /(,|^)\s*?local-port\s*?=\s*"(.*?)"\s*?(,|$)/.exec(
+ line,
+ )?.[2];
+ if (!localPort) {
+ localPort = /(,|^)\s*?local-port\s*?=\s*(.*?)\s*?(,|$)/.exec(
+ line,
+ )?.[2];
+ }
+ // args = "-m", args = "rc4-md5"
+ // args = -m, args = rc4-md5
+ const argsRegex = /(,|^)\s*?args\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
+ let argsMatch;
+ const args = [];
+ while ((argsMatch = argsRegex.exec(line)) !== null) {
+ if (argsMatch[3] != null) {
+ args.push(argsMatch[3]);
+ } else if (argsMatch[4] != null) {
+ args.push(argsMatch[4]);
+ }
+ }
+ // addresses = "[ipv6]",,addresses = "ipv6", addresses = "ipv4"
+ // addresses = [ipv6], addresses = ipv6, addresses = ipv4
+ const addressesRegex =
+ /(,|^)\s*?addresses\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
+ let addressesMatch;
+ const addresses = [];
+ while ((addressesMatch = addressesRegex.exec(line)) !== null) {
+ let ip;
+ if (addressesMatch[3] != null) {
+ ip = addressesMatch[3];
+ } else if (addressesMatch[4] != null) {
+ ip = addressesMatch[4];
+ }
+ if (ip != null) {
+ ip = `${ip}`.trim().replace(/^\[/, '').replace(/\]$/, '');
+ }
+ if (isIP(ip)) {
+ addresses.push(ip);
+ }
+ }
+
+ const proxy = {
+ type: 'external',
+ name,
+ exec,
+ 'local-port': localPort,
+ args,
+ addresses,
+ };
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+function Surge_Snell() {
+ const name = 'Surge Snell Parser';
+ const test = (line) => {
+ return /^.*=\s*snell/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_Tuic() {
+ const name = 'Surge Tuic Parser';
+ const test = (line) => {
+ return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
+ };
+ const parse = (raw) => {
+ const { port_hopping, line } = surge_port_hopping(raw);
+ const proxy = getSurgeParser().parse(line);
+ proxy['ports'] = port_hopping;
+ return proxy;
+ };
+ return { name, test, parse };
+}
+function Surge_WireGuard() {
+ const name = 'Surge WireGuard Parser';
+ const test = (line) => {
+ return /^.*=\s*wireguard/.test(line.split(',')[0]);
+ };
+ const parse = (line) => getSurgeParser().parse(line);
+ return { name, test, parse };
+}
+
+function Surge_Hysteria2() {
+ const name = 'Surge Hysteria2 Parser';
+ const test = (line) => {
+ return /^.*=\s*hysteria2/.test(line.split(',')[0]);
+ };
+ const parse = (raw) => {
+ const { port_hopping, line } = surge_port_hopping(raw);
+ const proxy = getSurgeParser().parse(line);
+ proxy['ports'] = port_hopping;
+ return proxy;
+ };
+ return { name, test, parse };
+}
+
+function isIP(ip) {
+ return isIPv4(ip) || isIPv6(ip);
+}
+
+export default [
+ URI_PROXY(),
+ URI_SOCKS(),
+ URI_SS(),
+ URI_SSR(),
+ URI_VMess(),
+ URI_VLESS(),
+ URI_TUIC(),
+ URI_WireGuard(),
+ URI_Hysteria(),
+ URI_Hysteria2(),
+ URI_Trojan(),
+ URI_AnyTLS(),
+ Clash_All(),
+ Surge_Direct(),
+ Surge_AnyTLS(),
+ Surge_TrustTunnel(),
+ Surge_H2Connect(),
+ Surge_SSH(),
+ Surge_SS(),
+ Surge_VMess(),
+ Surge_Trojan(),
+ Surge_Http(),
+ Surge_Snell(),
+ Surge_Tuic(),
+ Surge_WireGuard(),
+ Surge_Hysteria2(),
+ Surge_Socks5(),
+ Surge_External(),
+ Loon_SS(),
+ Loon_SSR(),
+ Loon_VMess(),
+ Loon_Vless(),
+ Loon_Hysteria2(),
+ Loon_Trojan(),
+ Loon_AnyTLS(),
+ Loon_Http(),
+ Loon_Socks5(),
+ Loon_WireGuard(),
+ QX_SS(),
+ QX_SSR(),
+ QX_VMess(),
+ QX_VLESS(),
+ QX_AnyTLS(),
+ QX_Trojan(),
+ QX_Http(),
+ QX_Socks5(),
+];
diff --git a/backend/src/core/proxy-utils/parsers/peggy/loon.js b/backend/src/core/proxy-utils/parsers/peggy/loon.js
new file mode 100644
index 0000000000..fee43e4a8a
--- /dev/null
+++ b/backend/src/core/proxy-utils/parsers/peggy/loon.js
@@ -0,0 +1,224 @@
+import peggy from 'peggy';
+const grammars = String.raw`
+// global initializer
+{{
+ function $set(obj, path, value) {
+ if (Object(obj) !== obj) return obj;
+ if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+ path
+ .slice(0, -1)
+ .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+ path[path.length - 1]
+ ] = value;
+ return obj;
+ }
+}}
+
+// per-parser initializer
+{
+ const proxy = {};
+ const obfs = {};
+ const transport = {};
+ const $ = {};
+
+ function handleTransport() {
+ if (transport.type === "tcp") { /* do nothing */ }
+ else if (transport.type === "ws") {
+ proxy.network = "ws";
+ $set(proxy, "ws-opts.path", transport.path);
+ $set(proxy, "ws-opts.headers.Host", transport.host);
+ } else if (transport.type === "http") {
+ proxy.network = "http";
+ $set(proxy, "http-opts.path", transport.path);
+ $set(proxy, "http-opts.headers.Host", transport.host);
+ }
+ }
+}
+
+start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2/anytls) {
+ return proxy;
+}
+
+shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{
+ proxy.type = "ssr";
+ // handle ssr obfs
+ proxy.obfs = obfs.type;
+}
+shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/udp_over_tcp/others)* {
+ proxy.type = "ss";
+ // handle ss obfs
+ if (obfs.type == "http" || obfs.type === "tls") {
+ proxy.plugin = "obfs";
+ $set(proxy, "plugin-opts.mode", obfs.type);
+ $set(proxy, "plugin-opts.host", obfs.host);
+ $set(proxy, "plugin-opts.path", obfs.path);
+ }
+}
+vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
+ proxy.type = "vmess";
+ proxy.cipher = proxy.cipher || "none";
+ proxy.alterId = proxy.alterId || 0;
+ handleTransport();
+}
+vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
+ proxy.type = "vless";
+ handleTransport();
+}
+trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
+ proxy.type = "trojan";
+ handleTransport();
+}
+anytls = tag equals "anytls"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/idle_session_check_interval/idle_session_timeout/min_idle_session/max_stream_count/others)* {
+ proxy.type = "anytls";
+ handleTransport();
+}
+hysteria2 = tag equals "hysteria2"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/server_ports/hop_interval/salamander_password/ecn/ip_mode/block_quic/others)* {
+ proxy.type = "hysteria2";
+}
+https = tag equals "https"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
+ proxy.type = "http";
+ proxy.tls = true;
+}
+http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
+ proxy.type = "http";
+}
+socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
+ proxy.type = "socks5";
+}
+
+address = comma server:server comma port:port {
+ proxy.server = server;
+ proxy.port = port;
+}
+
+server = ip/domain
+
+ip = & {
+ const start = peg$currPos;
+ let j = start;
+ while (j < input.length) {
+ if (input[j] === ",") break;
+ j++;
+ }
+ peg$currPos = j;
+ $.ip = input.substring(start, j).trim();
+ return true;
+} { return $.ip; }
+
+domain = match:[0-9a-zA-z-_.]+ {
+ const domain = match.join("");
+ if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+ return domain;
+ }
+ throw new Error("Invalid domain: " + domain);
+}
+
+port = digits:[0-9]+ {
+ const port = parseInt(digits.join(""), 10);
+ if (port >= 0 && port <= 65535) {
+ return port;
+ }
+ throw new Error("Invalid port number: " + port);
+}
+
+method = comma cipher:cipher {
+ proxy.cipher = cipher;
+}
+cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
+
+username = & {
+ let j = peg$currPos;
+ let start, end;
+ let first = true;
+ while (j < input.length) {
+ if (input[j] === ',') {
+ if (first) {
+ start = j + 1;
+ first = false;
+ } else {
+ end = j;
+ break;
+ }
+ }
+ j++;
+ }
+ const match = input.substring(start, end);
+ if (match.indexOf("=") === -1) {
+ $.username = match;
+ peg$currPos = end;
+ return true;
+ }
+} { proxy.username = $.username; }
+password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
+uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
+
+obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
+obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
+
+obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
+
+obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
+obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; }
+
+obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); }
+obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
+uri = $[^,]+
+
+transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; }
+transport_host = comma "host" equals match:[^,]+ { transport.host = match.join("").replace(/^"(.*)"$/, '$1'); }
+transport_path = comma "path" equals path:uri { transport.path = path; }
+
+ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; }
+ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
+
+vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
+
+udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
+shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
+shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
+shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
+
+over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
+tls_name = comma sni:("tls-name") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); }
+sni = comma "sni" equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); }
+tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
+tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+
+flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+
+fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
+udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
+ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
+
+ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
+download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
+server_ports = comma "server-ports" equals '"' match:$[^"]+ '"' { proxy.ports = match.trim().replace(/\s*-\s*/g, "-").replace(/\s*,\s*/g, ","); }
+hop_interval = comma "hop-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match, 10); }
+salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
+
+block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
+
+idle_session_check_interval = comma "idle-session-check-interval" equals match:$[0-9]+ { proxy["idle-session-check-interval"] = parseInt(match.trim()); }
+idle_session_timeout = comma "idle-session-timeout" equals match:$[0-9]+ { proxy["idle-session-timeout"] = parseInt(match.trim()); }
+min_idle_session = comma "min-idle-session" equals match:$[0-9]+ { proxy["min-idle-session"] = parseInt(match.trim()); }
+max_stream_count = comma "max-stream-count" equals match:$[0-9]+ { proxy["max-stream-count"] = parseInt(match.trim()); }
+
+udp_over_tcp = comma "udp-over-tcp" equals flag:bool { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; }
+
+tag = match:[^=,]* { proxy.name = match.join("").trim(); }
+comma = _ "," _
+equals = _ "=" _
+_ = [ \r\t]*
+bool = b:("true"/"false") { return b === "true" }
+others = comma [^=,]+ equals [^=,]+
+`;
+let parser;
+export default function getParser() {
+ if (!parser) {
+ parser = peggy.generate(grammars);
+ }
+ return parser;
+}
diff --git a/backend/src/core/proxy-utils/parsers/peggy/qx.js b/backend/src/core/proxy-utils/parsers/peggy/qx.js
new file mode 100644
index 0000000000..812413f1bc
--- /dev/null
+++ b/backend/src/core/proxy-utils/parsers/peggy/qx.js
@@ -0,0 +1,261 @@
+import peggy from 'peggy';
+const grammars = String.raw`
+// global initializer
+{{
+ function $set(obj, path, value) {
+ if (Object(obj) !== obj) return obj;
+ if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+ path
+ .slice(0, -1)
+ .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+ path[path.length - 1]
+ ] = value;
+ return obj;
+ }
+}}
+
+// per-parse initializer
+{
+ const proxy = {};
+ const obfs = {};
+ const $ = {};
+
+ function setQxHttpObfs(type) {
+ // Preserve the original QX http-obfs token for round-trip output,
+ // including the upstream "vemss-http" typo that appears in QX
+ // examples.
+ proxy._qx_obfs_http = type;
+ obfs.type = "http";
+ return type;
+ }
+
+ function handleObfs() {
+ if (obfs.type === "ws" || obfs.type === "wss") {
+ proxy.network = "ws";
+ if (obfs.type === 'wss') {
+ proxy.tls = true;
+ }
+ $set(proxy, "ws-opts.path", obfs.path);
+ $set(proxy, "ws-opts.headers.Host", obfs.host);
+ } else if (obfs.type === "over-tls") {
+ proxy.tls = true;
+ // Some QX share links use obfs-host as the TLS server name for
+ // plain over-tls TCP nodes instead of the explicit tls-host field.
+ // Accept it as a compatibility alias, but do not override tls-host.
+ if (obfs.host && !proxy.sni) {
+ proxy.sni = obfs.host;
+ }
+ } else if (obfs.type === "http") {
+ proxy.network = "http";
+ $set(proxy, "http-opts.path", obfs.path);
+ $set(proxy, "http-opts.headers.Host", obfs.host);
+ }
+ }
+}
+
+start = (trojan/shadowsocks/vmess/vless/anytls/http/socks5) {
+ return proxy
+}
+
+trojan = "trojan" equals address
+ (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
+ proxy.type = "trojan";
+ handleObfs();
+}
+
+shadowsocks = "shadowsocks" equals address
+ (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
+ if (proxy.protocol || proxy.type === "ssr") {
+ proxy.type = "ssr";
+ if (!proxy.protocol) {
+ proxy.protocol = "origin";
+ }
+ // handle ssr obfs
+ if (obfs.host) proxy["obfs-param"] = obfs.host;
+ if (obfs.type) proxy.obfs = obfs.type;
+ } else {
+ proxy.type = "ss";
+ // handle ss obfs
+ if (obfs.type == "http" || obfs.type === "tls") {
+ proxy.plugin = "obfs";
+ $set(proxy, "plugin-opts", {
+ mode: obfs.type
+ });
+ } else if (obfs.type === "ws" || obfs.type === "wss") {
+ proxy.plugin = "v2ray-plugin";
+ $set(proxy, "plugin-opts.mode", "websocket");
+ if (obfs.type === "wss") {
+ $set(proxy, "plugin-opts.tls", true);
+ }
+ } else if (obfs.type === 'over-tls') {
+ proxy.tls = true;
+ if (obfs.host) {
+ proxy.sni = obfs.host;
+ }
+ }
+ if (obfs.type && obfs.type !== 'over-tls') {
+ $set(proxy, "plugin-opts.host", obfs.host);
+ $set(proxy, "plugin-opts.path", obfs.path);
+ }
+ }
+}
+
+vmess = "vmess" equals address
+ (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs_vmess/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
+ proxy.type = "vmess";
+ proxy.cipher = proxy.cipher || "none";
+ if (proxy.aead === false) {
+ proxy.alterId = 1;
+ } else {
+ proxy.alterId = 0;
+ }
+ handleObfs();
+}
+
+vless = "vless" equals address
+ (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs_vless/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/vless_flow/others)* {
+ proxy.type = "vless";
+ proxy.cipher = proxy.cipher || "none";
+ handleObfs();
+}
+
+anytls = "anytls" equals address
+ (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/udp_relay/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
+ proxy.type = "anytls";
+ proxy.tls = true;
+}
+
+http = "http" equals address
+ (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{
+ proxy.type = "http";
+}
+
+socks5 = "socks5" equals address
+ (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
+ proxy.type = "socks5";
+}
+
+address = server:server ":" port:port {
+ proxy.server = server;
+ proxy.port = port;
+}
+server = ip/domain
+
+domain = match:[0-9a-zA-z-_.]+ {
+ const domain = match.join("");
+ if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+ return domain;
+ }
+}
+
+ip = & {
+ const start = peg$currPos;
+ let end;
+ let j = start;
+ while (j < input.length) {
+ if (input[j] === ",") break;
+ if (input[j] === ":") end = j;
+ j++;
+ }
+ peg$currPos = end || j;
+ $.ip = input.substring(start, end).trim();
+ return true;
+} { return $.ip; }
+
+port = digits:[0-9]+ {
+ const port = parseInt(digits.join(""), 10);
+ if (port >= 0 && port <= 65535) {
+ return port;
+ }
+}
+
+username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
+password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
+uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
+
+method = comma "method" equals cipher:cipher {
+ proxy.cipher = cipher;
+};
+cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
+aead = comma "aead" equals flag:bool { proxy.aead = flag; }
+
+udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
+udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); }
+udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } }
+
+fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
+
+over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
+tls_host = comma sni:("tls-host") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); }
+tls_verification = comma "tls-verification" equals flag:bool {
+ proxy["skip-cert-verify"] = !flag;
+}
+tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
+tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
+tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
+tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
+ proxy["tls-no-session-ticket"] = flag;
+}
+tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
+ proxy["tls-no-session-reuse"] = flag;
+}
+
+obfs_ss = comma "obfs" equals (
+ type:("tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
+ / type:("http"/"vmess-http"/"vemss-http"/"shadowsocks-http") {
+ // QX accepts multiple http-obfs spellings for ss/vmess/vless; keep
+ // the original token so QX output can round-trip it unchanged.
+ return setQxHttpObfs(type);
+ }
+)
+obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; }
+obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
+obfs_vmess = comma "obfs" equals (
+ type:("wss"/"ws"/"over-tls") { obfs.type = type; return type; }
+ / type:("http"/"vmess-http"/"vemss-http"/"shadowsocks-http") {
+ // QX accepts multiple http-obfs spellings for ss/vmess/vless; keep
+ // the original token so QX output can round-trip it unchanged.
+ return setQxHttpObfs(type);
+ }
+);
+obfs_vless = comma "obfs" equals (
+ type:("wss"/"ws"/"over-tls") { obfs.type = type; return type; }
+ / type:("http"/"vmess-http"/"vemss-http"/"shadowsocks-http") {
+ // QX accepts multiple http-obfs spellings for ss/vmess/vless; keep
+ // the original token so QX output can round-trip it unchanged.
+ return setQxHttpObfs(type);
+ }
+);
+
+obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); }
+obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
+
+ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
+ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
+
+reality_base64_pubkey = comma "reality-base64-pubkey" equals param:$[^=,]+ {
+ $set(proxy, "reality-opts.public-key", param);
+ }
+reality_hex_shortid = comma "reality-hex-shortid" equals param:$[^=,]+ {
+ $set(proxy, "reality-opts.short-id", param);
+}
+
+vless_flow = comma "vless-flow" equals param:$[^=,]+ { proxy["flow"] = param; }
+server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
+
+uri = $[^,]+
+
+tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
+others = comma [^=,]+ equals [^=,]+
+comma = _ "," _
+equals = _ "=" _
+_ = [ \r\t]*
+bool = b:("true"/"false") { return b === "true" }
+`;
+let parser;
+export default function getParser() {
+ if (!parser) {
+ parser = peggy.generate(grammars);
+ }
+ return parser;
+}
diff --git a/backend/src/core/proxy-utils/parsers/peggy/surge.js b/backend/src/core/proxy-utils/parsers/peggy/surge.js
new file mode 100644
index 0000000000..aa391be5b9
--- /dev/null
+++ b/backend/src/core/proxy-utils/parsers/peggy/surge.js
@@ -0,0 +1,568 @@
+import peggy from 'peggy';
+const grammars = String.raw`
+// global initializer
+{{
+ function $set(obj, path, value) {
+ if (Object(obj) !== obj) return obj;
+ if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+ path
+ .slice(0, -1)
+ .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+ path[path.length - 1]
+ ] = value;
+ return obj;
+ }
+}}
+
+// per-parser initializer
+{
+ const proxy = {};
+ const obfs = {};
+ const $ = {};
+
+ function handleWebsocket() {
+ if (obfs.type === "ws") {
+ proxy.network = "ws";
+ $set(proxy, "ws-opts.path", obfs.path);
+ $set(proxy, "ws-opts.headers", obfs['ws-headers']);
+ if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
+ proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
+ }
+ }
+ }
+ function handleShadowTLS() {
+ if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
+ proxy['shadow-tls-version'] = 2;
+ }
+ }
+ function stripQuotes(value) {
+ const trimmed = value.trim();
+ const quote = trimmed[0];
+ if (
+ (quote === '"' || quote === "'") &&
+ trimmed[trimmed.length - 1] === quote
+ ) {
+ return trimmed.slice(1, -1).replace(/\\(["'\\])/g, "$1");
+ }
+
+ return trimmed.replace(/\\(["'\\])/g, "$1");
+ }
+ function isEscaped(text, index) {
+ let count = 0;
+ let cursor = index - 1;
+
+ while (cursor >= 0 && text[cursor] === "\\") {
+ count++;
+ cursor--;
+ }
+
+ return count % 2 === 1;
+ }
+ function readQuotedHeaderKey(text, start) {
+ const quote = text[start];
+ let index = start + 1;
+ let hasKey = false;
+
+ while (index < text.length) {
+ const char = text[index];
+ if (char === "\\" && index + 1 < text.length) {
+ hasKey = true;
+ index += 2;
+ continue;
+ }
+ if (char === quote) {
+ return hasKey ? index + 1 : -1;
+ }
+
+ hasKey = true;
+ index++;
+ }
+
+ return -1;
+ }
+ function startsWithQuotedHeaderKey(text) {
+ const trimmed = text.trim();
+ if (trimmed[0] !== '"' && trimmed[0] !== "'") return false;
+
+ const index = readQuotedHeaderKey(trimmed, 0);
+ if (index === -1) return false;
+
+ let cursor = index;
+ while (cursor < trimmed.length && /\s/.test(trimmed[cursor])) cursor++;
+ return trimmed[cursor] === ":";
+ }
+ function stripOuterHeadersQuotes(headers) {
+ const trimmed = headers.trim();
+ const quote = trimmed[0];
+
+ if (
+ (quote === '"' || quote === "'") &&
+ trimmed[trimmed.length - 1] === quote &&
+ !startsWithQuotedHeaderKey(trimmed)
+ ) {
+ return trimmed.slice(1, -1);
+ }
+
+ return trimmed;
+ }
+ function isHeaderKeyStart(text, start) {
+ let index = start;
+ while (index < text.length && /\s/.test(text[index])) index++;
+
+ if (text[index] === '"' || text[index] === "'") {
+ index = readQuotedHeaderKey(text, index);
+ if (index === -1) return false;
+ } else {
+ const keyStart = index;
+ while (
+ index < text.length &&
+ /[!#$%&'*+\-.^_|~0-9A-Za-z]/.test(text[index])
+ )
+ index++;
+ if (index === keyStart) return false;
+ }
+
+ while (index < text.length && /\s/.test(text[index])) index++;
+ return text[index] === ":";
+ }
+ function isHeaderValueQuoteEnd(text, index) {
+ let cursor = index + 1;
+ while (cursor < text.length && /\s/.test(text[cursor])) cursor++;
+
+ return (
+ cursor >= text.length ||
+ text[cursor] === "," ||
+ (text[cursor] === ";" && isHeaderKeyStart(text, cursor + 1))
+ );
+ }
+ function findHeaderSeparator(pair) {
+ let quote = "";
+
+ for (let index = 0; index < pair.length; index++) {
+ const char = pair[index];
+
+ if (quote) {
+ if (char === "\\" && index + 1 < pair.length) {
+ index++;
+ continue;
+ }
+ if (char === quote) {
+ quote = "";
+ }
+ continue;
+ }
+
+ if (char === '"' || char === "'") {
+ quote = char;
+ continue;
+ }
+
+ if (char === ":") {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+ function readUnquotedHeadersEnd(text, start) {
+ let index = start;
+ let quote = "";
+ let quoteRole = "";
+ let seenSeparator = false;
+
+ while (index < text.length) {
+ const char = text[index];
+
+ if (quote) {
+ if (char === "\\" && index + 1 < text.length) {
+ index += 2;
+ continue;
+ }
+ if (char === quote) {
+ if (
+ quoteRole === "key" ||
+ isHeaderValueQuoteEnd(text, index)
+ ) {
+ quote = "";
+ quoteRole = "";
+ }
+ }
+ index++;
+ continue;
+ }
+
+ if (char === '"' || char === "'") {
+ quote = char;
+ quoteRole = seenSeparator ? "value" : "key";
+ index++;
+ continue;
+ }
+
+ if (char === ":" && !seenSeparator) {
+ seenSeparator = true;
+ index++;
+ continue;
+ }
+
+ if (char === ";" && isHeaderKeyStart(text, index + 1)) {
+ seenSeparator = false;
+ index++;
+ continue;
+ }
+
+ if (char === ",") break;
+ index++;
+ }
+
+ return index;
+ }
+ function readQuotedHeadersEnd(text, start) {
+ const quote = text[start];
+ let index = start + 1;
+
+ while (index < text.length) {
+ if (text[index] === quote && !isEscaped(text, index)) {
+ let cursor = index + 1;
+ while (cursor < text.length && /\s/.test(text[cursor])) cursor++;
+ if (cursor >= text.length || text[cursor] === ",") {
+ return index + 1;
+ }
+ }
+ index++;
+ }
+
+ return text.length;
+ }
+ function readHeadersEnd(text, start) {
+ let index = start;
+ while (index < text.length && /\s/.test(text[index])) index++;
+
+ if (
+ (text[index] === '"' || text[index] === "'") &&
+ !startsWithQuotedHeaderKey(text.slice(index))
+ ) {
+ return readQuotedHeadersEnd(text, index);
+ }
+
+ return readUnquotedHeadersEnd(text, start);
+ }
+ function splitHeaders(headers) {
+ const result = [];
+ let start = 0;
+ let quote = "";
+ let quoteRole = "";
+ let seenSeparator = false;
+
+ for (let index = 0; index < headers.length; index++) {
+ const char = headers[index];
+
+ if (quote) {
+ if (char === "\\" && index + 1 < headers.length) {
+ index++;
+ continue;
+ }
+ if (char === quote) {
+ if (
+ quoteRole === "key" ||
+ isHeaderValueQuoteEnd(headers, index)
+ ) {
+ quote = "";
+ quoteRole = "";
+ }
+ }
+ continue;
+ }
+
+ if (char === '"' || char === "'") {
+ quote = char;
+ quoteRole = seenSeparator ? "value" : "key";
+ continue;
+ }
+
+ if (char === ":" && !seenSeparator) {
+ seenSeparator = true;
+ continue;
+ }
+
+ if (char === ";" && isHeaderKeyStart(headers, index + 1)) {
+ result.push(headers.slice(start, index));
+ start = index + 1;
+ seenSeparator = false;
+ }
+ }
+
+ result.push(headers.slice(start));
+ return result;
+ }
+ function parseHeaders(headers) {
+ const result = {};
+ splitHeaders(stripOuterHeadersQuotes(headers)).forEach((pair) => {
+ const index = findHeaderSeparator(pair);
+ if (index === -1) return;
+
+ const key = stripQuotes(pair.slice(0, index));
+ const value = stripQuotes(pair.slice(index + 1));
+
+ if (key) {
+ result[key] = value;
+ }
+ });
+ return result;
+ }
+}
+
+start = (anytls/shadowsocks/vmess/trojan/h2_connect/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) {
+ return proxy;
+}
+
+shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
+ proxy.type = "ss";
+ // handle obfs
+ if (obfs.type == "http" || obfs.type === "tls") {
+ proxy.plugin = "obfs";
+ $set(proxy, "plugin-opts.mode", obfs.type);
+ $set(proxy, "plugin-opts.host", obfs.host);
+ $set(proxy, "plugin-opts.path", obfs.path);
+ }
+ handleShadowTLS();
+}
+vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "vmess";
+ proxy.cipher = proxy.cipher || "none";
+ // Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
+ if (proxy.aead) {
+ proxy.alterId = 0;
+ } else {
+ proxy.alterId = 1;
+ }
+ handleWebsocket();
+ handleShadowTLS();
+}
+trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "trojan";
+ handleWebsocket();
+ handleShadowTLS();
+}
+https = tag equals "https" address (username password)? (usernamek passwordk)? (headers/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "http";
+ proxy.tls = true;
+ handleShadowTLS();
+}
+h2_connect = tag equals "h2-connect" address (username password)? (usernamek passwordk)? (headers/max_streams/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "h2-connect";
+ proxy.tls = true;
+ handleShadowTLS();
+}
+http = tag equals "http" address (username password)? (usernamek passwordk)? (headers/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "http";
+ handleShadowTLS();
+}
+ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "ssh";
+ handleShadowTLS();
+}
+snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "snell";
+ // handle obfs
+ if (obfs.type == "http" || obfs.type === "tls") {
+ $set(proxy, "obfs-opts.mode", obfs.type);
+ $set(proxy, "obfs-opts.host", obfs.host);
+ $set(proxy, "obfs-opts.path", obfs.path);
+ }
+ handleShadowTLS();
+}
+tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
+ proxy.type = "tuic";
+ handleShadowTLS();
+}
+tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
+ proxy.type = "tuic";
+ proxy.version = 5;
+ handleShadowTLS();
+}
+wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "wireguard-surge";
+ handleShadowTLS();
+}
+hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/client_cert/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/others)* {
+ proxy.type = "hysteria2";
+ handleShadowTLS();
+}
+socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "socks5";
+ handleShadowTLS();
+}
+socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
+ proxy.type = "socks5";
+ proxy.tls = true;
+ handleShadowTLS();
+}
+anytls = tag equals "anytls" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/block_quic/others)* {
+ proxy.type = "anytls";
+ proxy.tls = true;
+}
+trust_tunnel = tag equals "trust-tunnel" address (usernamek/passwordk/headers/max_streams/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/block_quic/others)* {
+ proxy.type = "trusttunnel";
+ proxy.tls = true;
+}
+
+direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
+ proxy.type = "direct";
+}
+address = comma server:server comma port:port {
+ proxy.server = server;
+ proxy.port = port;
+}
+
+server = ip/domain
+
+ip = & {
+ const start = peg$currPos;
+ let j = start;
+ while (j < input.length) {
+ if (input[j] === ",") break;
+ j++;
+ }
+ peg$currPos = j;
+ $.ip = input.substring(start, j).trim();
+ return true;
+} { return $.ip; }
+
+domain = match:[0-9a-zA-z-_.]+ {
+ const domain = match.join("");
+ if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+ return domain;
+ }
+}
+
+port = digits:[0-9]+ {
+ const port = parseInt(digits.join(""), 10);
+ if (port >= 0 && port <= 65535) {
+ return port;
+ }
+}
+
+port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
+
+username = & {
+ let j = peg$currPos;
+ let start, end;
+ let first = true;
+ while (j < input.length) {
+ if (input[j] === ',') {
+ if (first) {
+ start = j + 1;
+ first = false;
+ } else {
+ end = j;
+ break;
+ }
+ }
+ j++;
+ }
+ const match = input.substring(start, end);
+ if (match.indexOf("=") === -1) {
+ $.username = match;
+ peg$currPos = end;
+ return true;
+ }
+} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+
+tls = comma "tls" equals flag:bool { proxy.tls = flag; }
+sni = comma "sni" equals match:[^,]+ {
+ const sni = match.join("").replace(/^"(.*)"$/, '$1');
+ if (sni === "off") {
+ proxy["disable-sni"] = true;
+ } else {
+ proxy.sni = sni;
+ }
+}
+tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
+tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
+client_cert = comma "client-cert" equals match:[^,]+ { proxy["keystore-client-cert"] = stripQuotes(match.join("")); }
+
+snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
+snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
+
+usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
+vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
+
+method = comma "encrypt-method" equals cipher:cipher {
+ proxy.cipher = cipher;
+}
+cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
+
+ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
+ws_headers = comma "ws-headers" equals headers:$[^,]+ {
+ const pairs = headers.split("|");
+ const result = {};
+ pairs.forEach(pair => {
+ const [key, value] = pair.trim().split(":");
+ result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
+ })
+ obfs["ws-headers"] = result;
+}
+ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+headers = comma "headers" equals & {
+ const start = peg$currPos;
+ const index = readHeadersEnd(input, start);
+
+ $.headers = input.substring(start, index);
+ peg$currPos = index;
+ return $.headers.trim().length > 0;
+} { proxy.headers = parseHeaders($.headers); }
+
+obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
+obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); };
+obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
+uri = $[^,]+
+
+udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
+fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
+reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
+ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
+tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
+ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
+section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
+no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
+underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
+download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
+test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
+test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
+test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
+max_streams = comma "max-streams" equals match:quoted_integer { proxy["max-streams"] = match; }
+quoted_integer = '"' match:$[0-9]+ '"' { return parseInt(match.trim()); } / "'" match:$[0-9]+ "'" { return parseInt(match.trim()); } / match:$[0-9]+ { return parseInt(match.trim()); }
+tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
+interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
+allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
+hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
+idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
+private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = stripQuotes(match.join("")); }
+server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
+block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
+udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
+shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
+shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
+shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
+token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
+alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
+uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
+salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; }
+
+tag = match:[^=,]* { proxy.name = match.join("").trim(); }
+comma = _ "," _
+equals = _ "=" _
+_ = [ \r\t]*
+bool = b:("true"/"false") { return b === "true" }
+others = comma [^=,]+ equals [^=,]+
+`;
+let parser;
+export default function getParser() {
+ if (!parser) {
+ parser = peggy.generate(grammars);
+ }
+ return parser;
+}
diff --git a/backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js b/backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
new file mode 100644
index 0000000000..fe735d76d8
--- /dev/null
+++ b/backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
@@ -0,0 +1,275 @@
+import peggy from 'peggy';
+const grammars = String.raw`
+// global initializer
+{{
+ function $set(obj, path, value) {
+ if (Object(obj) !== obj) return obj;
+ if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
+ path
+ .slice(0, -1)
+ .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
+ path[path.length - 1]
+ ] = value;
+ return obj;
+ }
+
+ function toBool(str) {
+ if (typeof str === 'undefined' || str === null) return undefined;
+ return /(TRUE)|1/i.test(str);
+ }
+
+ function decodeQueryComponent(value) {
+ try {
+ return decodeURIComponent(String(value).replace(/\+/g, '%20'));
+ } catch (e) {
+ return value;
+ }
+ }
+
+ function splitQueryPart(part) {
+ const separatorIndex = part.indexOf('=');
+ if (separatorIndex === -1) {
+ return {
+ key: decodeQueryComponent(part),
+ value: '',
+ };
+ }
+
+ return {
+ key: decodeQueryComponent(part.slice(0, separatorIndex)),
+ value: decodeQueryComponent(part.slice(separatorIndex + 1)),
+ };
+ }
+
+ function getPathQueryParam(path, paramName) {
+ const queryIndex = path.indexOf('?');
+ if (queryIndex === -1) return '';
+
+ const query = path.slice(queryIndex + 1);
+ for (const part of query.split('&')) {
+ if (part === '') continue;
+
+ const parsed = splitQueryPart(part);
+ if (parsed.key === paramName && parsed.value !== '') {
+ return parsed.value;
+ }
+ }
+
+ return '';
+ }
+
+ function extractPathQueryParam(path, paramName) {
+ const queryIndex = path.indexOf('?');
+ if (queryIndex === -1) {
+ return {
+ path,
+ value: '',
+ };
+ }
+
+ const basePath = path.slice(0, queryIndex);
+ const query = path.slice(queryIndex + 1);
+ const keptParts = [];
+ let value = '';
+
+ for (const part of query.split('&')) {
+ if (part === '') continue;
+
+ const parsed = splitQueryPart(part);
+ if (parsed.key === paramName) {
+ if (value === '' && parsed.value !== '') {
+ value = parsed.value;
+ }
+ continue;
+ }
+
+ keptParts.push(part);
+ }
+
+ return {
+ path: keptParts.length > 0 ? basePath + '?' + keptParts.join('&') : basePath,
+ value,
+ };
+ }
+
+ function parseEarlyDataSize(value) {
+ if (value == null || !/^\d+$/.test(String(value))) return null;
+
+ const parsed = parseInt(String(value), 10);
+ return Number.isSafeInteger(parsed) ? parsed : null;
+ }
+
+ function isNumericEarlyData(value) {
+ return parseEarlyDataSize(value) != null;
+ }
+}}
+
+{
+ const proxy = {};
+ const obfs = {};
+ const $ = {};
+ const params = {};
+}
+
+start = (trojan) {
+ return proxy
+}
+
+trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
+ proxy.type = "trojan";
+ proxy.password = password;
+ proxy.server = server;
+ proxy.port = port;
+ proxy.name = name;
+
+ // name may be empty
+ if (!proxy.name) {
+ proxy.name = server + ":" + port;
+ }
+};
+
+password = match:$[^@]+ {
+ return decodeURIComponent(match);
+};
+
+server = ip/domain;
+
+domain = match:[0-9a-zA-z-_.]+ {
+ const domain = match.join("");
+ if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
+ return domain;
+ }
+}
+
+ip = & {
+ const start = peg$currPos;
+ let end;
+ let j = start;
+ while (j < input.length) {
+ if (input[j] === ",") break;
+ if (input[j] === ":") end = j;
+ j++;
+ }
+ peg$currPos = end || j;
+ $.ip = input.substring(start, end).trim();
+ return true;
+} { return $.ip; }
+
+port = digits:[0-9]+ {
+ const port = parseInt(digits.join(""), 10);
+ if (port >= 0 && port <= 65535) {
+ return port;
+ } else {
+ throw new Error("Invalid port: " + port);
+ }
+}
+
+params = "?" head:param tail:("&"@param)* {
+ for (const [key, value] of Object.entries(params)) {
+ params[key] = decodeURIComponent(value);
+ }
+ proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
+ proxy.sni = params["sni"] || params["peer"];
+ proxy['client-fingerprint'] = params.fp;
+ proxy['tls-fingerprint'] = params.pcs;
+ proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
+
+ if (toBool(params["ws"])) {
+ proxy.network = "ws";
+ $set(proxy, "ws-opts.path", params["wspath"]);
+ }
+
+ if (params["type"]) {
+ let httpupgrade
+ let httpUpgradeEd = ''
+ let pathEarlyData = ''
+ proxy.network = params["type"]
+ if(proxy.network === 'httpupgrade') {
+ proxy.network = 'ws'
+ httpupgrade = true
+ }
+ if (['grpc'].includes(proxy.network)) {
+ proxy[proxy.network + '-opts'] = {
+ 'grpc-service-name': params["serviceName"],
+ '_grpc-type': params["mode"],
+ '_grpc-authority': params["authority"],
+ };
+ } else {
+ if (params["path"]) {
+ let transportPath = params["path"]
+ if (proxy.network === 'ws') {
+ const pathEd = getPathQueryParam(transportPath, 'ed')
+ if (isNumericEarlyData(pathEd)) {
+ transportPath = extractPathQueryParam(transportPath, 'ed').path
+ if (httpupgrade) {
+ httpUpgradeEd = pathEd
+ } else {
+ pathEarlyData = pathEd
+ }
+ }
+ }
+ $set(proxy, proxy.network+"-opts.path", transportPath);
+ }
+ if (params["host"]) {
+ $set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
+ }
+ if (httpupgrade) {
+ httpUpgradeEd = httpUpgradeEd || (isNumericEarlyData(params.ed) ? String(params.ed) : '')
+ $set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
+ if (httpUpgradeEd !== '') {
+ $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
+ $set(proxy, proxy.network+"-opts._v2ray-http-upgrade-ed", httpUpgradeEd);
+ }
+ } else if (proxy.network === 'ws' && pathEarlyData !== '') {
+ $set(proxy, proxy.network+"-opts.max-early-data", parseEarlyDataSize(pathEarlyData));
+ $set(proxy, proxy.network+"-opts.early-data-header-name", 'Sec-WebSocket-Protocol');
+ }
+ }
+ if (['reality'].includes(params.security)) {
+ const opts = {};
+ if (params.pbk) {
+ opts['public-key'] = params.pbk;
+ }
+ if (params.sid) {
+ opts['short-id'] = params.sid;
+ }
+ if (params.spx) {
+ opts['_spider-x'] = params.spx;
+ }
+ if (params.mode) {
+ proxy._mode = params.mode;
+ }
+ if (params.extra) {
+ proxy._extra = params.extra;
+ }
+ if (Object.keys(opts).length > 0) {
+ $set(proxy, params.security+"-opts", opts);
+ }
+ }
+ }
+
+ proxy.udp = toBool(params["udp"]);
+ proxy.tfo = toBool(params["tfo"]);
+}
+
+param = kv/single;
+
+kv = key:$[a-z]i+ "=" value:$[^]i* {
+ params[key] = value;
+}
+
+single = key:$[a-z]i+ {
+ params[key] = true;
+};
+
+name = "#" + match:$.* {
+ return decodeURIComponent(match);
+}
+`;
+let parser;
+export default function getParser() {
+ if (!parser) {
+ parser = peggy.generate(grammars);
+ }
+ return parser;
+}
diff --git a/backend/src/core/proxy-utils/preprocessors/index.js b/backend/src/core/proxy-utils/preprocessors/index.js
new file mode 100644
index 0000000000..235fb7b78d
--- /dev/null
+++ b/backend/src/core/proxy-utils/preprocessors/index.js
@@ -0,0 +1,200 @@
+import { safeLoad } from '@/utils/yaml';
+import { Base64 } from 'js-base64';
+import $ from '@/core/app';
+
+export function normalizeClashYaml(raw) {
+ if (
+ typeof raw !== 'string' ||
+ !raw.includes('proxies:') ||
+ !raw.includes('short-id:')
+ ) {
+ return raw;
+ }
+
+ try {
+ const content = safeLoad(raw);
+ if (!Array.isArray(content.proxies) || content.proxies.length === 0)
+ return raw;
+ } catch (e) {
+ return raw;
+ }
+ // 防止 VLESS 节点 reality-opts 里的 short-id 被 YAML 标量推断成数字
+ // 例如 08 / 0088 在部分内核重新解析时会触发 invalid REALITY short ID
+ return raw.replace(/short-id:([ \t]*[^#\n,}]*)/g, (matched, value) => {
+ const afterTrim = value.trim();
+
+ if (!afterTrim || afterTrim === '') {
+ return 'short-id: ""';
+ }
+
+ if (/^(['"]).*\1$/.test(afterTrim)) {
+ return `short-id: ${afterTrim}`;
+ } else if (['null'].includes(afterTrim)) {
+ return `short-id: ${afterTrim}`;
+ } else {
+ return `short-id: "${afterTrim}"`;
+ }
+ });
+}
+
+function HTML() {
+ const name = 'HTML';
+ const test = (raw) => /^/.test(raw);
+ // simply discard HTML
+ const parse = () => '';
+ return { name, test, parse };
+}
+
+function Base64Encoded() {
+ const name = 'Base64 Pre-processor';
+
+ const keys = [
+ 'dm1lc3M', // vmess
+ 'c3NyOi8v', // ssr://
+ 'c29ja3M6Ly', // socks://
+ 'dHJvamFu', // trojan
+ 'c3M6Ly', // ss:/
+ 'c3NkOi8v', // ssd://
+ 'c2hhZG93', // shadow
+ 'aHR0c', // htt
+ 'dmxlc3M=', // vless
+ 'aHlzdGVyaWEy', // hysteria2
+ 'aHkyOi8v', // hy2://
+ 'd2lyZWd1YXJkOi8v', // wireguard://
+ 'd2c6Ly8=', // wg://
+ 'dHVpYzovLw==', // tuic://
+ ];
+
+ const test = function (raw) {
+ return (
+ !/^\w+:\/\/\w+/im.test(raw) &&
+ keys.some((k) => raw.indexOf(k) !== -1)
+ );
+ };
+ const parse = function (raw) {
+ const decoded = Base64.decode(raw);
+ if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
+ $.error(
+ `Base64 Pre-processor error: decoded line does not start with protocol`,
+ );
+ return raw;
+ }
+
+ return decoded;
+ };
+ return { name, test, parse };
+}
+
+function fallbackBase64Encoded() {
+ const name = 'Fallback Base64 Pre-processor';
+
+ const test = function (raw) {
+ return true;
+ };
+ const parse = function (raw) {
+ const decoded = Base64.decode(raw);
+ if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
+ $.error(
+ `Fallback Base64 Pre-processor error: decoded line does not start with protocol`,
+ );
+ return raw;
+ }
+
+ return decoded;
+ };
+ return { name, test, parse };
+}
+
+function Clash() {
+ const name = 'Clash Pre-processor';
+ const test = function (raw) {
+ if (!/proxies/.test(raw)) return false;
+ const content = safeLoad(raw);
+ return content.proxies && Array.isArray(content.proxies);
+ };
+ const parse = function (raw, includeProxies) {
+ // Clash YAML format
+
+ const afterReplace = normalizeClashYaml(raw);
+
+ const { proxies } = safeLoad(afterReplace);
+ return (
+ (includeProxies ? 'proxies:\n' : '') +
+ proxies
+ .map((p) => {
+ return `${includeProxies ? ' - ' : ''}${JSON.stringify(
+ p,
+ )}\n`;
+ })
+ .join('')
+ );
+ };
+ return { name, test, parse };
+}
+
+function SSD() {
+ const name = 'SSD Pre-processor';
+ const test = function (raw) {
+ return raw.indexOf('ssd://') === 0;
+ };
+ const parse = function (raw) {
+ // preprocessing for SSD subscription format
+ const output = [];
+ let ssdinfo = JSON.parse(Base64.decode(raw.split('ssd://')[1]));
+ let port = ssdinfo.port;
+ let method = ssdinfo.encryption;
+ let password = ssdinfo.password;
+ // servers config
+ let servers = ssdinfo.servers;
+ for (let i = 0; i < servers.length; i++) {
+ let server = servers[i];
+ method = server.encryption ? server.encryption : method;
+ password = server.password ? server.password : password;
+ let userinfo = Base64.encode(method + ':' + password);
+ let hostname = server.server;
+ port = server.port ? server.port : port;
+ let tag = server.remarks ? server.remarks : i;
+ let plugin = server.plugin_options
+ ? '/?plugin=' +
+ encodeURIComponent(
+ server.plugin + ';' + server.plugin_options,
+ )
+ : '';
+ output[i] =
+ 'ss://' +
+ userinfo +
+ '@' +
+ hostname +
+ ':' +
+ port +
+ plugin +
+ '#' +
+ tag;
+ }
+ return output.join('\n');
+ };
+ return { name, test, parse };
+}
+
+function FullConfig() {
+ const name = 'Full Config Preprocessor';
+ const test = function (raw) {
+ return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
+ };
+ const parse = function (raw) {
+ const match = raw.match(
+ /^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
+ )?.[1];
+ return match || raw;
+ };
+ return { name, test, parse };
+}
+
+export default [
+ HTML(),
+ Clash(),
+ Base64Encoded(),
+ SSD(),
+ FullConfig(),
+ fallbackBase64Encoded(),
+];
diff --git a/backend/src/core/proxy-utils/processors/index.js b/backend/src/core/proxy-utils/processors/index.js
new file mode 100644
index 0000000000..723f255db9
--- /dev/null
+++ b/backend/src/core/proxy-utils/processors/index.js
@@ -0,0 +1,1406 @@
+import resourceCache from '@/utils/resource-cache';
+import scriptResourceCache from '@/utils/script-resource-cache';
+import { isIPv4, isIPv6, ipAddress, isPlainObject } from '@/utils';
+import { FULL } from '@/utils/logical';
+import { getFlag, removeFlag } from '@/utils/geo';
+import { doh } from '@/utils/dns';
+import lodash from 'lodash';
+import $ from '@/core/app';
+import { hex_md5 } from '@/vendor/md5';
+import { ProxyUtils } from '@/core/proxy-utils';
+import { produceArtifact } from '@/restful/sync';
+import { SETTINGS_KEY } from '@/constants';
+import YAML from '@/utils/yaml';
+
+import env from '@/utils/env';
+import {
+ getFlowField,
+ getFlowHeaders,
+ parseFlowHeaders,
+ validCheck,
+ flowTransfer,
+ getRmainingDays,
+ normalizeFlowHeader,
+} from '@/utils/flow';
+
+export const RESPONSE_TRANSFORMER = 'Response Transformer';
+
+export function isResponseTransformerType(type) {
+ return type === RESPONSE_TRANSFORMER;
+}
+
+function trimWrap(str) {
+ if (str.startsWith('<') && str.endsWith('>')) {
+ return str.slice(1, -1);
+ }
+ return str;
+}
+function deepMerge(target, _other) {
+ const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
+ for (const key in other) {
+ // Only recurse into JSON-like patch objects. YAML can surface non-plain
+ // objects (for example timestamps), and those should be assigned as
+ // values instead of being treated as nested config maps.
+ if (isPlainObject(other[key])) {
+ if (key.endsWith('!')) {
+ const k = trimWrap(key.slice(0, -1));
+ target[k] = other[key];
+ } else {
+ const k = trimWrap(key);
+ if (!target[k]) Object.assign(target, { [k]: {} });
+ deepMerge(target[k], other[k]);
+ }
+ } else if (Array.isArray(other[key])) {
+ if (key.startsWith('+')) {
+ const k = trimWrap(key.slice(1));
+ if (!target[k]) Object.assign(target, { [k]: [] });
+ target[k] = [...other[key], ...target[k]];
+ } else if (key.endsWith('+')) {
+ const k = trimWrap(key.slice(0, -1));
+ if (!target[k]) Object.assign(target, { [k]: [] });
+ target[k] = [...target[k], ...other[key]];
+ } else {
+ const k = trimWrap(key);
+ Object.assign(target, { [k]: other[key] });
+ }
+ } else {
+ Object.assign(target, { [key]: other[key] });
+ }
+ }
+ return target;
+}
+/**
+ The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
+ {
+ operator: "AND",
+ child: [
+ {
+ attr: "name",
+ proposition: "CONTAINS",
+ value: "🇨🇳"
+ },
+ {
+ attr: "port",
+ proposition: "IN",
+ value: [80, 443]
+ }
+ ]
+}
+ */
+
+function ConditionalFilter({ rule }) {
+ return {
+ name: 'Conditional Filter',
+ func: (proxies) => {
+ return proxies.map((proxy) => isMatch(rule, proxy));
+ },
+ };
+}
+
+function isMatch(rule, proxy) {
+ // leaf node
+ if (!rule.operator) {
+ switch (rule.proposition) {
+ case 'IN':
+ return rule.value.indexOf(proxy[rule.attr]) !== -1;
+ case 'CONTAINS':
+ if (typeof proxy[rule.attr] !== 'string') return false;
+ return proxy[rule.attr].indexOf(rule.value) !== -1;
+ case 'EQUALS':
+ return proxy[rule.attr] === rule.value;
+ case 'EXISTS':
+ return (
+ proxy[rule.attr] !== null ||
+ typeof proxy[rule.attr] !== 'undefined'
+ );
+ default:
+ throw new Error(`Unknown proposition: ${rule.proposition}`);
+ }
+ }
+
+ // operator nodes
+ switch (rule.operator) {
+ case 'AND':
+ return rule.child.every((child) => isMatch(child, proxy));
+ case 'OR':
+ return rule.child.some((child) => isMatch(child, proxy));
+ case 'NOT':
+ return !isMatch(rule.child, proxy);
+ default:
+ throw new Error(`Unknown operator: ${rule.operator}`);
+ }
+}
+
+function QuickSettingOperator(args) {
+ return {
+ name: 'Quick Setting Operator',
+ func: (proxies) => {
+ if (get(args.useless)) {
+ const filter = UselessFilter();
+ const selected = filter.func(proxies);
+ proxies = proxies.filter(
+ (p, i) => selected[i] && p.port > 0 && p.port <= 65535,
+ );
+ }
+
+ return proxies.map((proxy) => {
+ proxy.udp = get(args.udp, proxy.udp);
+ proxy.tfo = get(args.tfo, proxy.tfo);
+ proxy['fast-open'] = get(args.tfo, proxy['fast-open']);
+ proxy['skip-cert-verify'] = get(
+ args.scert,
+ proxy['skip-cert-verify'],
+ );
+ if (proxy.type === 'vmess') {
+ proxy.aead = get(args['vmess aead'], proxy.aead);
+ }
+ return proxy;
+ });
+ },
+ };
+
+ function get(value, defaultValue) {
+ switch (value) {
+ case 'ENABLED':
+ return true;
+ case 'DISABLED':
+ return false;
+ default:
+ return defaultValue;
+ }
+ }
+}
+
+// add or remove flag for proxies
+function FlagOperator({ mode, tw }) {
+ return {
+ name: 'Flag Operator',
+ func: (proxies) => {
+ return proxies.map((proxy) => {
+ if (mode === 'remove') {
+ // no flag
+ proxy.name = removeFlag(proxy.name);
+ } else {
+ // get flag
+ const newFlag = getFlag(proxy.name);
+ // remove old flag
+ proxy.name = removeFlag(proxy.name);
+ proxy.name = newFlag + ' ' + proxy.name;
+ if (tw == 'ws') {
+ proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');
+ } else if (tw == 'tw') {
+ // 不变
+ } else {
+ proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
+ }
+ }
+ return proxy;
+ });
+ },
+ };
+}
+
+// duplicate handler
+function HandleDuplicateOperator(arg) {
+ const { action, template, link, position, field } = {
+ ...{
+ action: 'rename',
+ template: '0 1 2 3 4 5 6 7 8 9',
+ link: '-',
+ position: 'back',
+ field: ['name'],
+ },
+ ...arg,
+ };
+ return {
+ name: 'Handle Duplicate Operator',
+ func: (proxies) => {
+ if (action === 'delete') {
+ const chosen = {};
+ return proxies.filter((p) => {
+ const key = field
+ .map((f) => lodash.get(p, f, '-'))
+ .join('_');
+ if (chosen[key]) {
+ return false;
+ }
+ chosen[key] = true;
+ return true;
+ });
+ } else if (action === 'rename') {
+ const numbers = template.split(' ');
+ // count occurrences of each name
+ const counter = {};
+ let maxLen = 0;
+ proxies.forEach((p) => {
+ const key = field
+ .map((f) => lodash.get(p, f, '-'))
+ .join('_');
+ if (typeof counter[key] === 'undefined') counter[key] = 1;
+ else counter[key]++;
+ maxLen = Math.max(counter[key].toString().length, maxLen);
+ });
+ const increment = {};
+ return proxies.map((p) => {
+ const key = field
+ .map((f) => lodash.get(p, f, '-'))
+ .join('_');
+ if (counter[key] > 1) {
+ if (typeof increment[key] == 'undefined')
+ increment[key] = 1;
+ let num = '';
+ let cnt = increment[key]++;
+ let numDigits = 0;
+ while (cnt > 0) {
+ num = numbers[cnt % 10] + num;
+ cnt = parseInt(cnt / 10);
+ numDigits++;
+ }
+ // padding
+ while (numDigits++ < maxLen) {
+ num = numbers[0] + num;
+ }
+ if (position === 'front') {
+ p.name = num + link + p.name;
+ } else if (position === 'back') {
+ p.name = p.name + link + num;
+ }
+ }
+ return p;
+ });
+ }
+ },
+ };
+}
+
+// sort proxies according to their names
+function SortOperator(order = 'asc') {
+ return {
+ name: 'Sort Operator',
+ func: (proxies) => {
+ switch (order) {
+ case 'asc':
+ case 'desc':
+ return proxies.sort((a, b) => {
+ let res = a.name > b.name ? 1 : -1;
+ res *= order === 'desc' ? -1 : 1;
+ return res;
+ });
+ case 'random':
+ return shuffle(proxies);
+ default:
+ throw new Error('Unknown sort option: ' + order);
+ }
+ },
+ };
+}
+
+// sort by regex
+function RegexSortOperator(input) {
+ const order = input.order || 'asc';
+ let expressions = input.expressions;
+ if (Array.isArray(input)) {
+ expressions = input;
+ }
+ if (!Array.isArray(expressions)) {
+ expressions = [];
+ }
+ return {
+ name: 'Regex Sort Operator',
+ func: (proxies) => {
+ expressions = expressions.map((expr) => buildRegex(expr));
+ return proxies.sort((a, b) => {
+ const oA = getRegexOrder(expressions, a.name);
+ const oB = getRegexOrder(expressions, b.name);
+ if (oA && !oB) return -1;
+ if (oB && !oA) return 1;
+ if (oA && oB) return oA < oB ? -1 : 1;
+ if (order === 'original') {
+ return 0;
+ } else if (order === 'desc') {
+ return a.name < b.name ? 1 : -1;
+ } else {
+ return a.name < b.name ? -1 : 1;
+ }
+ });
+ },
+ };
+}
+
+function getRegexOrder(expressions, str) {
+ let order = null;
+ for (let i = 0; i < expressions.length; i++) {
+ if (expressions[i].test(str)) {
+ order = i + 1; // plus 1 is important! 0 will be treated as false!!!
+ break;
+ }
+ }
+ return order;
+}
+
+// rename by regex
+// keywords: [{expr: "string format regex", now: "now"}]
+function RegexRenameOperator(regex) {
+ return {
+ name: 'Regex Rename Operator',
+ func: (proxies) => {
+ return proxies.map((proxy) => {
+ for (const { expr, now } of regex) {
+ proxy.name = proxy.name
+ .replace(buildRegex(expr, 'g'), now)
+ .trim();
+ }
+ return proxy;
+ });
+ },
+ };
+}
+
+// delete regex operator
+// regex: ['a', 'b', 'c']
+function RegexDeleteOperator(regex) {
+ const regex_ = regex.map((r) => {
+ return {
+ expr: r,
+ now: '',
+ };
+ });
+ return {
+ name: 'Regex Delete Operator',
+ func: RegexRenameOperator(regex_).func,
+ };
+}
+
+/** Script Operator
+ function operator(proxies) {
+ const {arg1} = $arguments;
+
+ // do something
+ return proxies;
+ }
+
+ WARNING:
+ 1. This function name should be `operator`!
+ 2. Always declare variables before using them!
+ */
+function ScriptOperator(
+ script,
+ targetPlatform,
+ $arguments,
+ source,
+ $options,
+ context,
+) {
+ context.source = source;
+ context.env = env;
+ return {
+ name: 'Script Operator',
+ func: async (proxies) => {
+ let output = proxies;
+ if (output?.$file?.type === 'mihomoProfile') {
+ try {
+ let patch = YAML.safeLoad(script);
+ let config;
+ if (output?.$content) {
+ try {
+ config = YAML.safeLoad(output?.$content);
+ } catch (e) {
+ $.error(e.message ?? e);
+ }
+ }
+ // if (typeof patch !== 'object') patch = {};
+ if (typeof patch !== 'object')
+ throw new Error('patch is not an object');
+ output.$content = ProxyUtils.yaml.safeDump(
+ deepMerge(
+ config ||
+ (output?.$file?.sourceType === 'none'
+ ? {}
+ : {
+ proxies: await produceArtifact({
+ type:
+ output?.$file?.sourceType ||
+ 'collection',
+ name: output?.$file?.sourceName,
+ platform: 'mihomo',
+ produceType: 'internal',
+ produceOpts: {
+ 'delete-underscore-fields': true,
+ },
+ }),
+ }),
+ patch,
+ ),
+ );
+ return output;
+ } catch (e) {
+ // console.log(e);
+ }
+ }
+ await (async function () {
+ const operator = createDynamicFunction(
+ 'operator',
+ script,
+ $arguments,
+ $options,
+ );
+ output = operator(proxies, targetPlatform, context);
+ })();
+ return output;
+ },
+ nodeFunc: async (proxies) => {
+ let output = proxies;
+ await (async function () {
+ const operator = createDynamicFunction(
+ 'operator',
+ `async function operator(input = [], targetPlatform, context) {
+ if (input && (input.$files || input.$content)) {
+ let { $content, $files, $options, $file } = input
+ if($file.type === 'mihomoProfile') {
+ ${script}
+ if(typeof main === 'function') {
+ let config;
+ if ($content) {
+ try {
+ config = ProxyUtils.yaml.safeLoad($content);
+ } catch (e) {
+ console.log(e.message ?? e);
+ }
+ }
+ $content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
+ proxies: await produceArtifact({
+ type: $file.sourceType || 'collection',
+ name: $file.sourceName,
+ platform: 'mihomo',
+ produceType: 'internal',
+ produceOpts: {
+ 'delete-underscore-fields': true
+ }
+ }),
+ })))
+ }
+ } else {
+ ${script}
+ }
+ return { $content, $files, $options, $file }
+ } else {
+ let proxies = input
+ let list = []
+ for await (let $server of proxies) {
+ ${script}
+ list.push($server)
+ }
+ return list
+ }
+ }`,
+ $arguments,
+ $options,
+ );
+ output = operator(proxies, targetPlatform, context);
+ })();
+ return output;
+ },
+ };
+}
+
+function parseIP4P(IP4P) {
+ let server;
+ let port;
+ try {
+ let array = IP4P.split(':');
+
+ port = parseInt(array[2], 16);
+ let ipab = parseInt(array[3], 16);
+ let ipcd = parseInt(array[4], 16);
+ let ipa = ipab >> 8;
+ let ipb = ipab & 0xff;
+ let ipc = ipcd >> 8;
+ let ipd = ipcd & 0xff;
+ server = `${ipa}.${ipb}.${ipc}.${ipd}`;
+ if (port <= 0 || port > 65535) {
+ throw new Error(`Invalid port number: ${port}`);
+ }
+ if (!isIPv4(server)) {
+ throw new Error(`Invalid IP address: ${server}`);
+ }
+ } catch (e) {
+ // throw new Error(`IP4P 解析失败: ${e}`);
+ $.error(`IP4P 解析失败: ${e}`);
+ }
+ return { server, port };
+}
+
+const DEFAULT_RESOLVE_DOMAIN_CONCURRENCY = 15;
+const RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD = 20;
+
+function normalizeResolveDomainConcurrency(concurrency) {
+ if (
+ typeof concurrency === 'undefined' ||
+ concurrency === null ||
+ (typeof concurrency === 'string' && concurrency.trim() === '')
+ ) {
+ return DEFAULT_RESOLVE_DOMAIN_CONCURRENCY;
+ }
+
+ const parsed = Number(concurrency);
+ if (!Number.isInteger(parsed) || parsed < 1) {
+ throw new Error('域名解析并发数应为大于 0 的整数');
+ }
+
+ return parsed;
+}
+
+async function resolveDomainsWithConcurrency(
+ domains,
+ concurrency,
+ resolveDomain,
+) {
+ let nextIndex = 0;
+ const workerCount = Math.min(concurrency, domains.length);
+ const workers = Array.from({ length: workerCount }, async () => {
+ while (nextIndex < domains.length) {
+ const domain = domains[nextIndex];
+ nextIndex += 1;
+ await resolveDomain(domain);
+ }
+ });
+
+ await Promise.all(workers);
+}
+
+function getDomainResolverCacheId(provider, domain, type, url) {
+ switch (provider) {
+ case 'Custom':
+ return hex_md5(`CUSTOM:${url}:${domain}:${type}`);
+ case 'Google':
+ return hex_md5(`GOOGLE:${domain}:${type}`);
+ case 'IP-API':
+ return hex_md5(`IP-API:${domain}`);
+ case 'Cloudflare':
+ return hex_md5(`CLOUDFLARE:${domain}:${type}`);
+ case 'Ali':
+ return hex_md5(`ALI:${domain}:${type}`);
+ case 'Tencent':
+ return hex_md5(`TENCENT:${domain}:${type}`);
+ }
+}
+
+function getCachedDomainResolverResult(provider, domain, type, cache, url) {
+ if (cache === 'disabled') return null;
+ const id = getDomainResolverCacheId(provider, domain, type, url);
+ return id ? resourceCache.get(id) : null;
+}
+
+const DOMAIN_RESOLVERS = {
+ Custom: async function (domain, type, noCache, timeout, edns, url) {
+ const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
+ const res = await doh({
+ url,
+ domain,
+ type: answerType,
+ timeout,
+ edns,
+ });
+
+ const { answers } = res;
+ if (!Array.isArray(answers) || answers.length === 0) {
+ throw new Error('No answers');
+ }
+ const result = answers
+ .filter((i) => i?.type === answerType)
+ .map((i) => i?.data)
+ .filter((i) => i);
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+ Google: async function (domain, type, noCache, timeout, edns) {
+ const id = hex_md5(`GOOGLE:${domain}:${type}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
+ const res = await doh({
+ url: 'https://8.8.4.4/dns-query',
+ domain,
+ type: answerType,
+ timeout,
+ edns,
+ });
+
+ const { answers } = res;
+ if (!Array.isArray(answers) || answers.length === 0) {
+ throw new Error('No answers');
+ }
+ const result = answers
+ .filter((i) => i?.type === answerType)
+ .map((i) => i?.data)
+ .filter((i) => i);
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+ 'IP-API': async function (domain, type, noCache, timeout) {
+ if (['IPv6'].includes(type)) {
+ throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
+ }
+ const id = hex_md5(`IP-API:${domain}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const resp = await $.http.get({
+ url: `http://ip-api.com/json/${encodeURIComponent(
+ domain,
+ )}?lang=zh-CN`,
+ timeout,
+ });
+ const body = JSON.parse(resp.body);
+ if (body['status'] !== 'success') {
+ throw new Error(`Status is ${body['status']}`);
+ }
+ if (!body.query || body.query === 0) {
+ throw new Error('No answers');
+ }
+ const result = [body.query];
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+ Cloudflare: async function (domain, type, noCache, timeout, edns) {
+ const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
+ const res = await doh({
+ url: 'https://1.0.0.1/dns-query',
+ domain,
+ type: answerType,
+ timeout,
+ edns,
+ });
+
+ const { answers } = res;
+ if (!Array.isArray(answers) || answers.length === 0) {
+ throw new Error('No answers');
+ }
+ const result = answers
+ .filter((i) => i?.type === answerType)
+ .map((i) => i?.data)
+ .filter((i) => i);
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+ Ali: async function (domain, type, noCache, timeout, edns) {
+ const id = hex_md5(`ALI:${domain}:${type}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const resp = await $.http.get({
+ url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/${
+ isIPv4(edns) ? 24 : 56
+ }&name=${encodeURIComponent(domain)}&type=${
+ type === 'IPv6' ? 'AAAA' : 'A'
+ }&short=1`,
+ headers: {
+ accept: 'application/dns-json',
+ },
+ timeout,
+ });
+ const answers = JSON.parse(resp.body);
+ if (!Array.isArray(answers) || answers.length === 0) {
+ throw new Error('No answers');
+ }
+ const result = answers;
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+ Tencent: async function (domain, type, noCache, timeout, edns) {
+ const id = hex_md5(`TENCENT:${domain}:${type}`);
+ const cached = resourceCache.get(id);
+ if (!noCache && cached) return cached;
+ const resp = await $.http.get({
+ url: `http://119.28.28.28/d?ip=${edns}&type=${
+ type === 'IPv6' ? 'AAAA' : 'A'
+ }&dn=${encodeURIComponent(domain)}`,
+ headers: {
+ accept: 'application/dns-json',
+ },
+ timeout,
+ });
+ const answers = resp.body.split(';').map((i) => i.split(',')[0]);
+ if (answers.length === 0 || String(answers) === '0') {
+ throw new Error('No answers');
+ }
+ const result = answers;
+ if (result.length === 0) {
+ throw new Error('No answers');
+ }
+ resourceCache.set(id, result);
+ return result;
+ },
+};
+
+function ResolveDomainOperator({
+ provider,
+ type: _type,
+ filter,
+ cache,
+ url,
+ timeout,
+ edns: _edns,
+ concurrency: _concurrency,
+}) {
+ if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
+ throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
+ }
+ const { defaultTimeout } = $.read(SETTINGS_KEY) || {};
+ const requestTimeout = timeout || defaultTimeout || 8000;
+ let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
+
+ const resolver = DOMAIN_RESOLVERS[provider];
+ if (!resolver) {
+ throw new Error(`找不到域名解析服务提供方: ${provider}`);
+ }
+ let edns = _edns || '223.6.6.6';
+ if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
+ const concurrency = normalizeResolveDomainConcurrency(_concurrency);
+ if (concurrency > RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD) {
+ $.warn(
+ `域名解析并发数 ${concurrency} 超过建议值 ${RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD}, 可能导致代理 App TCP 连接数激增`,
+ );
+ }
+ $.info(
+ `Domain Resolver: [${_type}] ${provider} ${edns || ''} ${
+ url || ''
+ } concurrency=${concurrency}`,
+ );
+ return {
+ name: 'Resolve Domain Operator',
+ func: async (proxies) => {
+ proxies.forEach((p, i) => {
+ if (!p['_no-resolve'] && p['no-resolve']) {
+ proxies[i]['_no-resolve'] = p['no-resolve'];
+ }
+ });
+ const results = {};
+ const domains = [
+ ...new Set(
+ proxies
+ .filter((p) => !isIP(p.server) && !p['_no-resolve'])
+ .map((c) => c.server),
+ ),
+ ];
+ const domainsToResolve = [];
+ domains.forEach((domain) => {
+ const cached = getCachedDomainResolverResult(
+ provider,
+ domain,
+ type,
+ cache,
+ url,
+ );
+ if (cached) {
+ results[domain] = cached;
+ $.info(
+ `Using cached resolved domain: ${domain} ➟ ${cached}`,
+ );
+ } else {
+ domainsToResolve.push(domain);
+ }
+ });
+ await resolveDomainsWithConcurrency(
+ domainsToResolve,
+ concurrency,
+ async (domain) => {
+ try {
+ const ip = await resolver(
+ domain,
+ type,
+ cache === 'disabled',
+ requestTimeout,
+ edns,
+ url,
+ );
+ results[domain] = ip;
+ $.info(
+ `Successfully resolved domain: ${domain} ➟ ${ip}`,
+ );
+ } catch (err) {
+ $.error(
+ `Failed to resolve domain: ${domain} with resolver [${provider}]: ${err}`,
+ );
+ }
+ },
+ );
+ proxies.forEach((p) => {
+ if (!p['_no-resolve']) {
+ if (results[p.server]) {
+ p._resolved_ips = results[p.server];
+ let ip = Array.isArray(results[p.server])
+ ? results[p.server][
+ Math.floor(
+ Math.random() * results[p.server].length,
+ )
+ ]
+ : results[p.server];
+ if (type === 'IPv6' && isIPv6(ip)) {
+ try {
+ ip = new ipAddress.Address6(ip).correctForm();
+ } catch (e) {
+ $.error(
+ `Failed to parse IPv6 address: ${ip}: ${e}`,
+ );
+ }
+ if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {
+ p._IP4P = ip;
+ const { server, port } = parseIP4P(ip);
+ if (server && port) {
+ p._domain = p.server;
+ p.server = server;
+ p.port = port;
+ p.resolved = true;
+ p._IPv4 = p.server;
+ if (!isIP(p._IP)) {
+ p._IP = p.server;
+ }
+ } else if (!p.resolved) {
+ p.resolved = false;
+ }
+ } else {
+ p._domain = p.server;
+ p.server = ip;
+ p.resolved = true;
+ p[`_${type}`] = p.server;
+ if (!isIP(p._IP)) {
+ p._IP = p.server;
+ }
+ }
+ } else {
+ p._domain = p.server;
+ p.server = ip;
+ p.resolved = true;
+ p[`_${type}`] = p.server;
+ if (!isIP(p._IP)) {
+ p._IP = p.server;
+ }
+ }
+ } else if (!p.resolved) {
+ p.resolved = false;
+ }
+ }
+ });
+
+ return proxies.filter((p) => {
+ if (filter === 'removeFailed') {
+ return isIP(p.server) || p['_no-resolve'] || p.resolved;
+ } else if (filter === 'IPOnly') {
+ return isIP(p.server);
+ } else if (filter === 'IPv4Only') {
+ return isIPv4(p.server);
+ } else if (filter === 'IPv6Only') {
+ return isIPv6(p.server);
+ } else {
+ return true;
+ }
+ });
+ },
+ };
+}
+
+function isIP(ip) {
+ return isIPv4(ip) || isIPv6(ip);
+}
+
+ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
+
+function isAscii(str) {
+ // eslint-disable-next-line no-control-regex
+ var pattern = /^[\x00-\x7F]+$/; // ASCII 范围的 Unicode 编码
+ return pattern.test(str);
+}
+
+/**************************** Filters ***************************************/
+// filter useless proxies
+function UselessFilter() {
+ return {
+ name: 'Useless Filter',
+ func: (proxies) => {
+ return proxies.map((proxy) => {
+ if (proxy.cipher && !isAscii(proxy.cipher)) {
+ return false;
+ } else if (proxy.password && !isAscii(proxy.password)) {
+ return false;
+ } else {
+ if (proxy.network) {
+ let transportHosts =
+ proxy[`${proxy.network}-opts`]?.headers?.Host ||
+ proxy[`${proxy.network}-opts`]?.headers?.host;
+ transportHosts = Array.isArray(transportHosts)
+ ? transportHosts
+ : [transportHosts];
+ if (
+ transportHosts.some(
+ (host) => host && !isAscii(host),
+ )
+ ) {
+ return false;
+ }
+ }
+ return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test(
+ proxy.name,
+ );
+ }
+ });
+ },
+ };
+}
+
+// filter by regions
+function RegionFilter(input) {
+ let regions = input?.value || input;
+ if (!Array.isArray(regions)) {
+ regions = [];
+ }
+ const keep = input?.keep ?? true;
+ const REGION_MAP = {
+ HK: '🇭🇰',
+ TW: '🇹🇼',
+ US: '🇺🇸',
+ SG: '🇸🇬',
+ JP: '🇯🇵',
+ UK: '🇬🇧',
+ DE: '🇩🇪',
+ KR: '🇰🇷',
+ };
+ return {
+ name: 'Region Filter',
+ func: (proxies) => {
+ // this would be high memory usage
+ return proxies.map((proxy) => {
+ const flag = getFlag(proxy.name);
+ const selected = regions.some((r) => REGION_MAP[r] === flag);
+ return keep ? selected : !selected;
+ });
+ },
+ };
+}
+
+// filter by regex
+function RegexFilter({ regex = [], keep = true }) {
+ return {
+ name: 'Regex Filter',
+ func: (proxies) => {
+ return proxies.map((proxy) => {
+ const selected = regex.some((r) => {
+ return buildRegex(r).test(proxy.name);
+ });
+ return keep ? selected : !selected;
+ });
+ },
+ };
+}
+
+function buildRegex(str, ...options) {
+ options = options.join('');
+ if (str.startsWith('(?i)')) {
+ str = str.substring(4);
+ return new RegExp(str, 'i' + options);
+ } else {
+ return new RegExp(str, options);
+ }
+}
+
+// filter by proxy types
+function TypeFilter(input) {
+ let types = input?.value || input;
+ if (!Array.isArray(types)) {
+ types = [];
+ }
+ const keep = input?.keep ?? true;
+ return {
+ name: 'Type Filter',
+ func: (proxies) => {
+ return proxies.map((proxy) => {
+ const selected = types.some((t) => proxy.type === t);
+ return keep ? selected : !selected;
+ });
+ },
+ };
+}
+
+/**
+ Script Example
+
+ function filter(proxies) {
+ return proxies.map(p => {
+ return p.name.indexOf('🇭🇰') !== -1;
+ });
+ }
+
+ WARNING:
+ 1. This function name should be `filter`!
+ 2. Always declare variables before using them!
+ */
+function ScriptFilter(
+ script,
+ targetPlatform,
+ $arguments,
+ source,
+ $options,
+ context,
+) {
+ context.source = source;
+ context.env = env;
+ return {
+ name: 'Script Filter',
+ func: async (proxies) => {
+ let output = FULL(proxies.length, true);
+ await (async function () {
+ const filter = createDynamicFunction(
+ 'filter',
+ script,
+ $arguments,
+ $options,
+ );
+ output = filter(proxies, targetPlatform, context);
+ })();
+ return output;
+ },
+ nodeFunc: async (proxies) => {
+ let output = FULL(proxies.length, true);
+ await (async function () {
+ const filter = createDynamicFunction(
+ 'filter',
+ `async function filter(input = [], targetPlatform, context) {
+ let proxies = input
+ let list = []
+ const fn = async ($server) => {
+ ${script}
+ }
+ for await (let $server of proxies) {
+ list.push(await fn($server))
+ }
+ return list
+ }`,
+ $arguments,
+ $options,
+ );
+ output = filter(proxies, targetPlatform, context);
+ })();
+ return output;
+ },
+ };
+}
+
+function ResponseTransformer(
+ script,
+ targetPlatform,
+ $arguments,
+ source,
+ $options,
+ context,
+) {
+ context.source = source;
+ context.env = env;
+ return {
+ name: RESPONSE_TRANSFORMER,
+ func: async (res) => {
+ let output = res;
+ await (async function () {
+ const transformFunction = createDynamicFunction(
+ 'transformFunction',
+ script,
+ $arguments,
+ $options,
+ );
+ output = transformFunction(res, context);
+ })();
+ return output;
+ },
+ shortcutFunc: async (res) => {
+ let output = res;
+ await (async function () {
+ const transformFunction = createDynamicFunction(
+ 'transformFunction',
+ `async function transformFunction(res = {}, context) {
+ let $res = res
+ ${script}
+ return $res
+ }`,
+ $arguments,
+ $options,
+ );
+ output = transformFunction(res, context);
+ })();
+ return output;
+ },
+ };
+}
+
+export default {
+ 'Useless Filter': UselessFilter,
+ 'Region Filter': RegionFilter,
+ 'Regex Filter': RegexFilter,
+ 'Type Filter': TypeFilter,
+ 'Script Filter': ScriptFilter,
+ 'Conditional Filter': ConditionalFilter,
+
+ 'Quick Setting Operator': QuickSettingOperator,
+ 'Flag Operator': FlagOperator,
+ 'Sort Operator': SortOperator,
+ 'Regex Sort Operator': RegexSortOperator,
+ 'Regex Rename Operator': RegexRenameOperator,
+ 'Regex Delete Operator': RegexDeleteOperator,
+ 'Script Operator': ScriptOperator,
+ [RESPONSE_TRANSFORMER]: ResponseTransformer,
+ 'Handle Duplicate Operator': HandleDuplicateOperator,
+ 'Resolve Domain Operator': ResolveDomainOperator,
+};
+
+export async function ApplyResponseTransformer(transformer, res) {
+ let output = res;
+ try {
+ const output_ = await transformer.func(output);
+ if (output_) output = output_;
+ } catch (err) {
+ let funcErr = '';
+ const funcErrMsg = `${err.message ?? err}`;
+ if (!funcErrMsg.includes('$res is not defined')) {
+ $.error(
+ `Cannot apply ${transformer.name}(function transformFunction)! Reason: ${err}`,
+ );
+ funcErr = `执行 function transformFunction 失败 ${funcErrMsg}; `;
+ }
+ try {
+ const output_ = await transformer.shortcutFunc(output);
+ if (output_) output = output_;
+ } catch (shortcutErr) {
+ $.error(
+ `Cannot apply ${transformer.name}(shortcut script)! Reason: ${shortcutErr}`,
+ );
+ let shortcutErrText = '';
+ const shortcutErrMsg = `${shortcutErr.message ?? shortcutErr}`;
+ if (funcErr && shortcutErrMsg === funcErrMsg) {
+ shortcutErrText = '';
+ funcErr = `执行失败 ${funcErrMsg}`;
+ } else {
+ shortcutErrText = `执行快捷脚本失败 ${shortcutErrMsg}`;
+ }
+ throw new Error(`响应修改 ${funcErr}${shortcutErrText}`);
+ }
+ }
+ return output;
+}
+
+async function ApplyFilter(filter, objs) {
+ // select proxies
+ let selected = FULL(objs.length, true);
+ try {
+ selected = await filter.func(objs);
+ } catch (err) {
+ let funcErr = '';
+ let funcErrMsg = `${err.message ?? err}`;
+ if (funcErrMsg.includes('$server is not defined')) {
+ funcErr = '';
+ } else {
+ $.error(
+ `Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
+ );
+ funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
+ }
+ try {
+ selected = await filter.nodeFunc(objs);
+ } catch (err) {
+ $.error(
+ `Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,
+ );
+ let nodeErr = '';
+ let nodeErrMsg = `${err.message ?? err}`;
+ if (funcErr && nodeErrMsg === funcErrMsg) {
+ nodeErr = '';
+ funcErr = `执行失败 ${funcErrMsg}`;
+ } else {
+ nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
+ }
+ throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
+ }
+ }
+ return objs.filter((_, i) => selected[i]);
+}
+
+async function ApplyOperator(operator, objs) {
+ let output = clone(objs);
+ try {
+ const output_ = await operator.func(output);
+ if (output_) output = output_;
+ } catch (err) {
+ let funcErr = '';
+ let funcErrMsg = `${err.message ?? err}`;
+ if (
+ funcErrMsg.includes('$server is not defined') ||
+ funcErrMsg.includes('$content is not defined') ||
+ funcErrMsg.includes('$files is not defined') ||
+ output?.$files ||
+ output?.$content
+ ) {
+ funcErr = '';
+ } else {
+ $.error(
+ `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
+ );
+ funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
+ }
+ try {
+ const output_ = await operator.nodeFunc(output);
+ if (output_) output = output_;
+ } catch (err) {
+ $.error(
+ `Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,
+ );
+ let nodeErr = '';
+ let nodeErrMsg = `${err.message ?? err}`;
+ if (funcErr && nodeErrMsg === funcErrMsg) {
+ nodeErr = '';
+ funcErr = `执行失败 ${funcErrMsg}`;
+ } else {
+ nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
+ }
+ throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
+ }
+ }
+ return output;
+}
+
+export async function ApplyProcessor(processor, objs) {
+ if (processor.name.indexOf('Filter') !== -1) {
+ return ApplyFilter(processor, objs);
+ } else if (processor.name.indexOf('Operator') !== -1) {
+ return ApplyOperator(processor, objs);
+ }
+}
+
+// shuffle array
+function shuffle(array) {
+ let currentIndex = array.length,
+ temporaryValue,
+ randomIndex;
+
+ // While there remain elements to shuffle...
+ while (0 !== currentIndex) {
+ // Pick a remaining element...
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+
+ // And swap it with the current element.
+ temporaryValue = array[currentIndex];
+ array[currentIndex] = array[randomIndex];
+ array[randomIndex] = temporaryValue;
+ }
+
+ return array;
+}
+
+// deep clone object
+function clone(object) {
+ return JSON.parse(JSON.stringify(object));
+}
+
+function createDynamicFunction(name, script, $arguments, $options) {
+ const flowUtils = {
+ getFlowField,
+ getFlowHeaders,
+ parseFlowHeaders,
+ flowTransfer,
+ validCheck,
+ getRmainingDays,
+ normalizeFlowHeader,
+ };
+ if ($.env.isLoon) {
+ return new Function(
+ '$arguments',
+ '$options',
+ '$substore',
+ 'lodash',
+ '$persistentStore',
+ '$httpClient',
+ '$notification',
+ 'ProxyUtils',
+ 'yaml',
+ 'Buffer',
+ 'b64d',
+ 'b64e',
+ 'DOMAIN_RESOLVERS',
+ 'scriptResourceCache',
+ 'flowUtils',
+ 'produceArtifact',
+ 'require',
+ `${script}\n return ${name}`,
+ )(
+ $arguments,
+ $options,
+ $,
+ lodash,
+ // eslint-disable-next-line no-undef
+ $persistentStore,
+ // eslint-disable-next-line no-undef
+ $httpClient,
+ // eslint-disable-next-line no-undef
+ $notification,
+ ProxyUtils,
+ ProxyUtils.yaml,
+ ProxyUtils.Buffer,
+ ProxyUtils.Base64.decode,
+ ProxyUtils.Base64.encode,
+ DOMAIN_RESOLVERS,
+ scriptResourceCache,
+ flowUtils,
+ produceArtifact,
+ eval(`typeof require !== "undefined"`) ? require : undefined,
+ );
+ } else {
+ return new Function(
+ '$arguments',
+ '$options',
+ '$substore',
+ 'lodash',
+ 'ProxyUtils',
+ 'yaml',
+ 'Buffer',
+ 'b64d',
+ 'b64e',
+ 'DOMAIN_RESOLVERS',
+ 'scriptResourceCache',
+ 'flowUtils',
+ 'produceArtifact',
+ 'require',
+ `${script}\n return ${name}`,
+ )(
+ $arguments,
+ $options,
+ $,
+ lodash,
+ ProxyUtils,
+ ProxyUtils.yaml,
+ ProxyUtils.Buffer,
+ ProxyUtils.Base64.decode,
+ ProxyUtils.Base64.encode,
+ DOMAIN_RESOLVERS,
+ scriptResourceCache,
+ flowUtils,
+ produceArtifact,
+ eval(`typeof require !== "undefined"`) ? require : undefined,
+ );
+ }
+}
diff --git a/backend/src/core/proxy-utils/producers/clash.js b/backend/src/core/proxy-utils/producers/clash.js
new file mode 100644
index 0000000000..8af2fb359c
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/clash.js
@@ -0,0 +1,218 @@
+import {
+ isPresent,
+ produceProxyListOutput,
+} from '@/core/proxy-utils/producers/utils';
+import {
+ deleteHttpUpgradeEarlyDataMetadata,
+ normalizeWebSocketEarlyDataPath,
+} from '../transport-path';
+import $ from '@/core/app';
+
+export default function Clash_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ // VLESS XTLS is not supported by Clash
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
+ // github.com/Dreamacro/clash/pull/2891/files
+ // filter unsupported proxies
+ // https://clash.wiki/configuration/outbound.html#shadowsocks
+ const list = proxies
+ .filter((proxy) => {
+ if (opts['include-unsupported-proxy']) return true;
+ if (
+ ![
+ 'ss',
+ 'ssr',
+ 'vmess',
+ 'vless',
+ 'socks5',
+ 'http',
+ 'snell',
+ 'trojan',
+ 'wireguard',
+ ].includes(proxy.type) ||
+ (proxy.type === 'ss' &&
+ ![
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'rc4-md5',
+ 'chacha20-ietf',
+ 'xchacha20',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ ].includes(proxy.cipher)) ||
+ (proxy.type === 'snell' && proxy.version >= 4) ||
+ (proxy.type === 'vless' &&
+ (typeof proxy.flow !== 'undefined' ||
+ proxy['reality-opts']))
+ ) {
+ return false;
+ } else if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ return false;
+ } else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
+ $.error(
+ `Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`,
+ );
+ return false;
+ }
+ return true;
+ })
+ .map((proxy) => {
+ if (proxy.type === 'vmess') {
+ // handle vmess aead
+ if (isPresent(proxy, 'aead')) {
+ if (proxy.aead) {
+ proxy.alterId = 0;
+ }
+ delete proxy.aead;
+ }
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ // https://dreamacro.github.io/clash/configuration/outbound.html#vmess
+ if (
+ isPresent(proxy, 'cipher') &&
+ ![
+ 'auto',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ 'none',
+ ].includes(proxy.cipher)
+ ) {
+ proxy.cipher = 'auto';
+ }
+ } else if (proxy.type === 'wireguard') {
+ proxy.keepalive =
+ proxy.keepalive ?? proxy['persistent-keepalive'];
+ proxy['persistent-keepalive'] = proxy.keepalive;
+ proxy['preshared-key'] =
+ proxy['preshared-key'] ?? proxy['pre-shared-key'];
+ proxy['pre-shared-key'] = proxy['preshared-key'];
+ } else if (proxy.type === 'snell' && proxy.version < 3) {
+ delete proxy.udp;
+ } else if (proxy.type === 'vless') {
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ }
+
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'http'
+ ) {
+ let httpPath = proxy['http-opts']?.path;
+ if (
+ isPresent(proxy, 'http-opts.path') &&
+ !Array.isArray(httpPath)
+ ) {
+ proxy['http-opts'].path = [httpPath];
+ }
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ if (
+ isPresent(proxy, 'http-opts.headers.Host') &&
+ !Array.isArray(httpHost)
+ ) {
+ proxy['http-opts'].headers.Host = [httpHost];
+ }
+ }
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'h2'
+ ) {
+ let path = proxy['h2-opts']?.path;
+ if (
+ isPresent(proxy, 'h2-opts.path') &&
+ Array.isArray(path)
+ ) {
+ proxy['h2-opts'].path = path[0];
+ }
+ let host = proxy['h2-opts']?.headers?.host;
+ if (
+ isPresent(proxy, 'h2-opts.headers.Host') &&
+ !Array.isArray(host)
+ ) {
+ proxy['h2-opts'].headers.host = [host];
+ }
+ }
+ if (['ws'].includes(proxy.network)) {
+ const networkOptsKey = `${proxy.network}-opts`;
+ proxy[networkOptsKey] = proxy[networkOptsKey] || {};
+ if (!proxy[networkOptsKey].path) {
+ proxy[networkOptsKey].path = '/';
+ }
+ normalizeWebSocketEarlyDataPath(proxy[networkOptsKey]);
+ }
+
+ if (proxy['plugin-opts']?.tls) {
+ if (isPresent(proxy, 'skip-cert-verify')) {
+ proxy['plugin-opts']['skip-cert-verify'] =
+ proxy['plugin-opts']['skip-cert-verify'] ||
+ proxy['skip-cert-verify'];
+ }
+ }
+ if (
+ [
+ 'trojan',
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'anytls',
+ 'trusttunnel',
+ 'naive',
+ ].includes(proxy.type)
+ ) {
+ delete proxy.tls;
+ }
+
+ if (proxy['tls-fingerprint']) {
+ proxy.fingerprint = proxy['tls-fingerprint'];
+ }
+ delete proxy['tls-fingerprint'];
+
+ if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
+ delete proxy.tls;
+ }
+
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+ delete proxy['ip-cidr'];
+ delete proxy['ipv6-cidr'];
+ if (type !== 'internal') {
+ for (const key in proxy) {
+ if (proxy[key] == null || /^_/i.test(key)) {
+ delete proxy[key];
+ }
+ }
+ deleteHttpUpgradeEarlyDataMetadata(
+ proxy[`${proxy.network}-opts`],
+ );
+ }
+ if (
+ ['grpc'].includes(proxy.network) &&
+ proxy[`${proxy.network}-opts`]
+ ) {
+ delete proxy[`${proxy.network}-opts`]['_grpc-type'];
+ delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
+ }
+ return proxy;
+ });
+ return produceProxyListOutput(list, type, opts);
+ };
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/producers/clashmeta.js b/backend/src/core/proxy-utils/producers/clashmeta.js
new file mode 100644
index 0000000000..7e0b46df97
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/clashmeta.js
@@ -0,0 +1,444 @@
+import {
+ getWireGuardAddressWithCIDR,
+ isPresent,
+ normalizePluginMuxBooleanValue,
+ produceProxyListOutput,
+ supportsShadowsocksV2rayPluginMode,
+} from '@/core/proxy-utils/producers/utils';
+import { isNotBlank, isPlainObject } from '@/utils';
+import {
+ deleteHttpUpgradeEarlyDataMetadata,
+ normalizeWebSocketEarlyDataPath,
+} from '../transport-path';
+import { ECH_DNS_FIELD } from '../ech-utils';
+import $ from '@/core/app';
+
+const ipVersions = {
+ dual: 'dual',
+ 'v4-only': 'ipv4',
+ 'v6-only': 'ipv6',
+ 'prefer-v4': 'ipv4-prefer',
+ 'prefer-v6': 'ipv6-prefer',
+};
+
+function warnMihomoUnsupportedEchDns(proxy, echOpts, echOptsPath) {
+ if (!isPlainObject(echOpts) || !isNotBlank(echOpts[ECH_DNS_FIELD])) {
+ return;
+ }
+
+ const queryServerName = isNotBlank(echOpts['query-server-name'])
+ ? echOpts['query-server-name']
+ : '这里是 query-server-name';
+ $.warn(
+ `mihomo 不支持在 ech-opts 中配置 ECH DNS. 如需跟节点 ECH 配置一致, 请在 mihomo 配置文件里设置 dns["nameserver-policy"]["${queryServerName}"] = ["${echOpts[ECH_DNS_FIELD]}"].`,
+ );
+}
+
+function warnMihomoUnsupportedEchDnsFields(proxy, type) {
+ if (type === 'internal') {
+ return;
+ }
+
+ warnMihomoUnsupportedEchDns(proxy, proxy['ech-opts'], 'ech-opts');
+ warnMihomoUnsupportedEchDns(
+ proxy,
+ proxy['xhttp-opts']?.['download-settings']?.['ech-opts'],
+ 'xhttp-opts.download-settings.ech-opts',
+ );
+}
+
+export default function ClashMeta_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ const list = proxies
+ .filter((proxy) => {
+ if (opts['include-unsupported-proxy']) return true;
+
+ if (proxy.type === 'h2-connect') {
+ $.error(
+ `Mihomo does not support HTTP/2 CONNECT proxy type. Proxy ${proxy.name} has been filtered.`,
+ );
+ return false;
+ }
+ if (hasRootHeaders(proxy) && proxy.type === 'trusttunnel') {
+ $.error(
+ `Mihomo does not support headers for TrustTunnel proxy ${proxy.name}. Proxy has been filtered.`,
+ );
+ return false;
+ }
+ if (!supportsShadowsocksV2rayPluginMode(proxy, ['websocket'])) {
+ return false;
+ } else if (
+ proxy.type === 'snell' &&
+ !isSupportedMihomoVersion(proxy.version, [1, 2, 3, 4, 5])
+ ) {
+ return false;
+ } else if (
+ hasMihomoShadowTls(proxy) &&
+ (proxy.type !== 'ss' ||
+ !isSupportedMihomoVersion(
+ getMihomoShadowTlsVersion(proxy),
+ [1, 2, 3],
+ ))
+ ) {
+ return false;
+ } else if (['juicity', 'naive'].includes(proxy.type)) {
+ return false;
+ } else if (
+ ['ss'].includes(proxy.type) &&
+ ![
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'aes-128-ccm',
+ 'aes-192-ccm',
+ 'aes-256-ccm',
+ 'aes-128-gcm-siv',
+ 'aes-256-gcm-siv',
+ 'chacha20-ietf',
+ 'chacha20',
+ 'xchacha20',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ 'chacha8-ietf-poly1305',
+ 'xchacha8-ietf-poly1305',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ '2022-blake3-chacha20-poly1305',
+ 'lea-128-gcm',
+ 'lea-192-gcm',
+ 'lea-256-gcm',
+ 'rabbit128-poly1305',
+ 'aegis-128l',
+ 'aegis-256',
+ 'aez-384',
+ 'deoxys-ii-256-128',
+ 'rc4-md5',
+ 'none',
+ ].includes(proxy.cipher)
+ ) {
+ // https://wiki.metacubex.one/config/proxies/ss/#cipher
+ return false;
+ } else if (
+ ['anytls'].includes(proxy.type) &&
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) &&
+ proxy['reality-opts']))
+ ) {
+ return false;
+ } else if (
+ !['vless'].includes(proxy.type) &&
+ ['xhttp'].includes(proxy.network)
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .map((proxy) => {
+ warnMihomoUnsupportedEchDnsFields(proxy, type);
+
+ if (proxy['reality-opts'] && !proxy['client-fingerprint']) {
+ proxy['client-fingerprint'] = 'chrome';
+ }
+ if (proxy.type === 'vmess') {
+ // handle vmess aead
+ if (isPresent(proxy, 'aead')) {
+ if (proxy.aead) {
+ proxy.alterId = 0;
+ }
+ delete proxy.aead;
+ }
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
+ // https://stash.wiki/proxy-protocols/proxy-types#vmess
+ if (
+ isPresent(proxy, 'cipher') &&
+ ![
+ 'auto',
+ 'none',
+ 'zero',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ ].includes(proxy.cipher)
+ ) {
+ proxy.cipher = 'auto';
+ }
+ } else if (proxy.type === 'tuic') {
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ // else {
+ // proxy.alpn = ['h3'];
+ // }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
+ if (
+ (!proxy.token || proxy.token.length === 0) &&
+ !isPresent(proxy, 'version')
+ ) {
+ proxy.version = 5;
+ }
+ } else if (proxy.type === 'hysteria') {
+ // auth_str 将会在未来某个时候删除 但是有的机场不规范
+ if (
+ isPresent(proxy, 'auth_str') &&
+ !isPresent(proxy, 'auth-str')
+ ) {
+ proxy['auth-str'] = proxy['auth_str'];
+ }
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ }
+ } else if (proxy.type === 'wireguard') {
+ proxy.keepalive =
+ proxy.keepalive ?? proxy['persistent-keepalive'];
+ proxy['persistent-keepalive'] = proxy.keepalive;
+ proxy['preshared-key'] =
+ proxy['preshared-key'] ?? proxy['pre-shared-key'];
+ proxy['pre-shared-key'] = proxy['preshared-key'];
+ proxy.ip = getWireGuardAddressWithCIDR(proxy, 'ipv4');
+ proxy.ipv6 = getWireGuardAddressWithCIDR(proxy, 'ipv6');
+ } else if (proxy.type === 'snell' && proxy.version < 3) {
+ delete proxy.udp;
+ } else if (proxy.type === 'vless') {
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ // Mihomo 的运行时校验(`adapter/outbound/vless.go`)
+ // 会先把 flow 截断到 16 个字符,并且只接受
+ // `xtls-rprx-vision`。`xtls-rprx-direct`、
+ // `xtls-rprx-unknown` 等值在 Mihomo 加载时会报错。
+ //
+ // 另外,xhttp 使用 `alpn: [h3]` 时必须启用 TLS,
+ // 且不能启用 Reality;这条限制同时作用于根 xhttp
+ // 配置,以及继承根配置默认值后的嵌套
+ // `download-settings`。
+
+ // 1. mihomo 似乎不支持上行/下行有一个为 tls 一个不为 tls. 但是这跟转换无关了
+ // 2. 下行有 tls 且上行有 reality-opts 且下行无 reality-opts →
+ // 补 reality-opts: { public-key: '' },阻断 reality 继承
+ if (
+ proxy.network === 'xhttp' &&
+ proxy['xhttp-opts']?.['download-settings']
+ ) {
+ const ds = proxy['xhttp-opts']['download-settings'];
+ if (
+ proxy.tls &&
+ ds.tls &&
+ proxy['reality-opts'] &&
+ !ds['reality-opts']
+ ) {
+ ds['reality-opts'] = { 'public-key': '' };
+ }
+ }
+ } else if (proxy.type === 'ss') {
+ if (
+ isPresent(proxy, 'shadow-tls-password') &&
+ !isPresent(proxy, 'plugin')
+ ) {
+ proxy.plugin = 'shadow-tls';
+ proxy['plugin-opts'] = {
+ host: proxy['shadow-tls-sni'],
+ password: proxy['shadow-tls-password'],
+ version: proxy['shadow-tls-version'],
+ };
+ delete proxy['shadow-tls-password'];
+ delete proxy['shadow-tls-sni'];
+ delete proxy['shadow-tls-version'];
+ }
+ }
+
+ if (isPresent(proxy, 'plugin-opts.mux')) {
+ proxy['plugin-opts'].mux = normalizePluginMuxBooleanValue(
+ proxy['plugin-opts'].mux,
+ );
+ }
+
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'http'
+ ) {
+ let httpPath = proxy['http-opts']?.path;
+ if (
+ isPresent(proxy, 'http-opts.path') &&
+ !Array.isArray(httpPath)
+ ) {
+ proxy['http-opts'].path = [httpPath];
+ }
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ if (
+ isPresent(proxy, 'http-opts.headers.Host') &&
+ !Array.isArray(httpHost)
+ ) {
+ proxy['http-opts'].headers.Host = [httpHost];
+ }
+ }
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'h2'
+ ) {
+ let path = proxy['h2-opts']?.path;
+ if (
+ isPresent(proxy, 'h2-opts.path') &&
+ Array.isArray(path)
+ ) {
+ proxy['h2-opts'].path = path[0];
+ }
+ let host = proxy['h2-opts']?.headers?.host;
+ if (
+ isPresent(proxy, 'h2-opts.headers.Host') &&
+ !Array.isArray(host)
+ ) {
+ proxy['h2-opts'].headers.host = [host];
+ }
+ }
+ if (['ws'].includes(proxy.network)) {
+ const networkOptsKey = `${proxy.network}-opts`;
+ proxy[networkOptsKey] = proxy[networkOptsKey] || {};
+ if (!proxy[networkOptsKey].path) {
+ proxy[networkOptsKey].path = '/';
+ }
+ normalizeWebSocketEarlyDataPath(proxy[networkOptsKey]);
+ }
+
+ if (proxy['plugin-opts']?.tls) {
+ if (isPresent(proxy, 'skip-cert-verify')) {
+ proxy['plugin-opts']['skip-cert-verify'] =
+ proxy['plugin-opts']['skip-cert-verify'] ||
+ proxy['skip-cert-verify'];
+ }
+ }
+ if (
+ [
+ 'trojan',
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'anytls',
+ 'trusttunnel',
+ 'naive',
+ ].includes(proxy.type)
+ ) {
+ delete proxy.tls;
+ }
+
+ if (proxy['tls-fingerprint']) {
+ proxy.fingerprint = proxy['tls-fingerprint'];
+ }
+ delete proxy['tls-fingerprint'];
+
+ if (proxy['underlying-proxy']) {
+ proxy['dialer-proxy'] = proxy['underlying-proxy'];
+ }
+ delete proxy['underlying-proxy'];
+
+ if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
+ delete proxy.tls;
+ }
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+ delete proxy['ip-cidr'];
+ delete proxy['ipv6-cidr'];
+ if (type !== 'internal' || opts['delete-underscore-fields']) {
+ for (const key in proxy) {
+ if (proxy[key] == null || /^_/i.test(key)) {
+ delete proxy[key];
+ }
+ }
+ deleteHttpUpgradeEarlyDataMetadata(
+ proxy[`${proxy.network}-opts`],
+ );
+ }
+ if (
+ ['grpc'].includes(proxy.network) &&
+ proxy[`${proxy.network}-opts`]
+ ) {
+ delete proxy[`${proxy.network}-opts`]['_grpc-type'];
+ delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
+ }
+
+ if (proxy['ip-version']) {
+ proxy['ip-version'] =
+ ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ }
+ return proxy;
+ });
+
+ return produceProxyListOutput(list, type, opts);
+ };
+ return { type, produce };
+}
+
+function hasRootHeaders(proxy) {
+ return (
+ proxy?.headers &&
+ typeof proxy.headers === 'object' &&
+ Object.keys(proxy.headers).length > 0
+ );
+}
+
+function isSupportedMihomoVersion(version, supportedVersions) {
+ if (version == null) {
+ return true;
+ }
+
+ const normalized =
+ typeof version === 'string' ? version.trim() : `${version}`;
+ if (!normalized) {
+ return false;
+ }
+
+ const parsed = Number(normalized);
+ return Number.isInteger(parsed) && supportedVersions.includes(parsed);
+}
+
+function hasMihomoShadowTls(proxy) {
+ return (
+ proxy?.plugin === 'shadow-tls' ||
+ isPresent(proxy, 'shadow-tls-password') ||
+ isPresent(proxy, 'shadow-tls-sni') ||
+ isPresent(proxy, 'shadow-tls-version')
+ );
+}
+
+function getMihomoShadowTlsVersion(proxy) {
+ if (isPresent(proxy, 'shadow-tls-version')) {
+ return proxy['shadow-tls-version'];
+ }
+
+ if (proxy?.plugin === 'shadow-tls') {
+ return proxy?.['plugin-opts']?.version;
+ }
+
+ return undefined;
+}
diff --git a/backend/src/core/proxy-utils/producers/egern.js b/backend/src/core/proxy-utils/producers/egern.js
new file mode 100644
index 0000000000..487f404bb8
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/egern.js
@@ -0,0 +1,613 @@
+import $ from '@/core/app';
+import {
+ getWireGuardAddressWithCIDR,
+ isPresent,
+ produceProxyListOutput,
+} from './utils';
+
+export default function Egern_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ // https://egernapp.com/zh-CN/docs/configuration/proxies
+ const list = proxies
+ .filter((proxy) => {
+ if (
+ ![
+ 'http',
+ 'https',
+ 'socks5',
+ 'ss',
+ 'trojan',
+ 'hysteria2',
+ 'vless',
+ 'vmess',
+ 'tuic',
+ 'wireguard',
+ 'anytls',
+ ].includes(proxy.type) ||
+ (proxy.type === 'ss' &&
+ ((proxy.plugin === 'obfs' &&
+ !['http', 'tls'].includes(
+ proxy['plugin-opts']?.mode,
+ )) ||
+ ![
+ 'chacha20-ietf-poly1305',
+ 'chacha20-poly1305',
+ 'aes-256-gcm',
+ 'aes-128-gcm',
+ 'none',
+ 'tbale',
+ 'rc4',
+ 'rc4-md5',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'bf-cfb',
+ 'camellia-128-cfb',
+ 'camellia-192-cfb',
+ 'camellia-256-cfb',
+ 'cast5-cfb',
+ 'des-cfb',
+ 'idea-cfb',
+ 'rc2-cfb',
+ 'seed-cfb',
+ 'salsa20',
+ 'chacha20',
+ 'chacha20-ietf',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher))) ||
+ (proxy.type === 'vmess' &&
+ !['http', 'ws', 'tcp'].includes(proxy.network) &&
+ proxy.network) ||
+ (proxy.type === 'trojan' &&
+ !['http', 'ws', 'tcp'].includes(proxy.network) &&
+ proxy.network) ||
+ (proxy.type === 'vless' &&
+ ((!['http', 'ws', 'tcp'].includes(proxy.network) &&
+ proxy.network) ||
+ (typeof proxy.flow !== 'undefined' &&
+ !['xtls-rprx-vision', ''].includes(
+ proxy.flow,
+ )))) ||
+ (proxy.type === 'tuic' &&
+ proxy.token &&
+ proxy.token.length !== 0)
+ ) {
+ return false;
+ } else if (
+ ['anytls'].includes(proxy.type) &&
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) &&
+ proxy['reality-opts']))
+ ) {
+ return false;
+ } else if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .map((proxy) => {
+ const sourceProxy = proxy;
+
+ try {
+ const original = { ...proxy };
+ let flow;
+ if (proxy.tls && !proxy.sni) {
+ proxy.sni = proxy.server;
+ }
+ const prev_hop =
+ proxy.prev_hop ||
+ proxy['underlying-proxy'] ||
+ proxy['dialer-proxy'] ||
+ proxy.detour;
+
+ if (proxy.type === 'http') {
+ proxy = {
+ type: proxy.tls ? 'https' : 'http',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ username: proxy.username,
+ password: proxy.password,
+ ...(hasHeaders(proxy)
+ ? {
+ headers: proxy.headers,
+ }
+ : {}),
+ tfo: proxy.tfo || proxy['fast-open'],
+ next_hop: proxy.next_hop,
+ ...(proxy.tls
+ ? {
+ sni: proxy.sni,
+ skip_tls_verify:
+ proxy['skip-cert-verify'],
+ }
+ : {}),
+ };
+ } else if (proxy.type === 'socks5') {
+ proxy = {
+ type: 'socks5',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ username: proxy.username,
+ password: proxy.password,
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ };
+ } else if (proxy.type === 'ss') {
+ proxy = {
+ type: 'shadowsocks',
+ name: proxy.name,
+ method:
+ proxy.cipher === 'chacha20-ietf-poly1305'
+ ? 'chacha20-poly1305'
+ : proxy.cipher,
+ server: proxy.server,
+ port: proxy.port,
+ password: proxy.password,
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ };
+ if (isPresent(original, 'plugin')) {
+ if (original.plugin === 'obfs') {
+ proxy.obfs = original['plugin-opts'].mode;
+ proxy.obfs_host = original['plugin-opts'].host;
+ proxy.obfs_uri = original['plugin-opts'].path;
+ } else if (
+ !['shadow-tls'].includes(original.plugin)
+ ) {
+ throw new Error(
+ `plugin ${original.plugin} is not supported`,
+ );
+ }
+ }
+ } else if (proxy.type === 'hysteria2') {
+ proxy = {
+ type: 'hysteria2',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ auth: proxy.password,
+ ...(isPresent(proxy, 'up')
+ ? {
+ bandwidth: parseInt(
+ `${proxy.up}`.match(/\d+/)?.[0] || 0,
+ 10,
+ ),
+ }
+ : {}),
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ sni: proxy.sni,
+ skip_tls_verify: proxy['skip-cert-verify'],
+ port_hopping: proxy.ports,
+ port_hopping_interval: proxy['hop-interval'],
+ };
+ if (
+ original['obfs-password'] &&
+ original.obfs == 'salamander'
+ ) {
+ proxy.obfs = 'salamander';
+ proxy.obfs_password = original['obfs-password'];
+ }
+ } else if (proxy.type === 'tuic') {
+ proxy = {
+ type: 'tuic',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ uuid: proxy.uuid,
+ password: proxy.password,
+ next_hop: proxy.next_hop,
+ sni: proxy.sni,
+ alpn: Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn || 'h3'],
+ skip_tls_verify: proxy['skip-cert-verify'],
+ port_hopping: proxy.ports,
+ port_hopping_interval: proxy['hop-interval'],
+ };
+ } else if (proxy.type === 'trojan') {
+ if (proxy.network === 'ws') {
+ proxy.websocket = {
+ path: proxy['ws-opts']?.path,
+ host: proxy['ws-opts']?.headers?.Host,
+ };
+ }
+ proxy = {
+ type: 'trojan',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ password: proxy.password,
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ sni: proxy.sni,
+ skip_tls_verify: proxy['skip-cert-verify'],
+ websocket: proxy.websocket,
+ };
+ } else if (proxy.type === 'anytls') {
+ proxy = {
+ type: 'anytls',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ password: proxy.password,
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ sni: proxy.sni,
+ skip_tls_verify: proxy['skip-cert-verify'],
+ };
+ } else if (proxy.type === 'vmess') {
+ // Egern:传输层,支持 ws/wss/http1/http2/tls,不配置则为 tcp
+ let security = proxy.cipher;
+ if (
+ security &&
+ ![
+ 'auto',
+ 'none',
+ 'zero',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ ].includes(security)
+ ) {
+ security = 'auto';
+ }
+ if (proxy.network === 'ws') {
+ proxy.transport = {
+ [proxy.tls ? 'wss' : 'ws']: {
+ path: proxy['ws-opts']?.path,
+ headers: {
+ Host: proxy['ws-opts']?.headers?.Host,
+ },
+ sni: proxy.tls ? proxy.sni : undefined,
+ skip_tls_verify: proxy.tls
+ ? proxy['skip-cert-verify']
+ : undefined,
+ },
+ };
+ } else if (proxy.network === 'http') {
+ proxy.transport = {
+ http1: {
+ method: proxy['http-opts']?.method,
+ path: Array.isArray(
+ proxy['http-opts']?.path,
+ )
+ ? proxy['http-opts']?.path[0]
+ : proxy['http-opts']?.path,
+ headers: {
+ Host: Array.isArray(
+ proxy['http-opts']?.headers?.Host,
+ )
+ ? proxy['http-opts']?.headers
+ ?.Host[0]
+ : proxy['http-opts']?.headers?.Host,
+ },
+ skip_tls_verify: proxy['skip-cert-verify'],
+ },
+ };
+ } else if (proxy.network === 'h2') {
+ proxy.transport = {
+ http2: {
+ method: proxy['h2-opts']?.method,
+ path: Array.isArray(proxy['h2-opts']?.path)
+ ? proxy['h2-opts']?.path[0]
+ : proxy['h2-opts']?.path,
+ headers: {
+ Host: Array.isArray(
+ proxy['h2-opts']?.headers?.Host,
+ )
+ ? proxy['h2-opts']?.headers?.Host[0]
+ : proxy['h2-opts']?.headers?.Host,
+ },
+ skip_tls_verify: proxy['skip-cert-verify'],
+ },
+ };
+ } else if (
+ (proxy.network === 'tcp' || !proxy.network) &&
+ proxy.tls
+ ) {
+ proxy.transport = {
+ tls: {
+ sni: proxy.tls ? proxy.sni : undefined,
+ skip_tls_verify: proxy.tls
+ ? proxy['skip-cert-verify']
+ : undefined,
+ },
+ };
+ }
+ let legacy;
+ if (isPresent(proxy, 'aead') && !proxy.aead) {
+ legacy = true;
+ } else if (proxy.alterId !== 0) {
+ legacy = true;
+ }
+ proxy = {
+ type: 'vmess',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ user_id: proxy.uuid,
+ security,
+ tfo: proxy.tfo || proxy['fast-open'],
+ legacy,
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ transport: proxy.transport,
+ };
+ } else if (proxy.type === 'vless') {
+ if (proxy.encryption && proxy.encryption !== 'none')
+ throw new Error(
+ `VLESS encryption is not supported`,
+ );
+ if (proxy.network === 'ws') {
+ proxy.transport = {
+ [proxy.tls ? 'wss' : 'ws']: {
+ path: proxy['ws-opts']?.path,
+ headers: {
+ Host: proxy['ws-opts']?.headers?.Host,
+ },
+ sni: proxy.tls ? proxy.sni : undefined,
+ skip_tls_verify: proxy.tls
+ ? proxy['skip-cert-verify']
+ : undefined,
+ },
+ };
+ } else if (proxy.network === 'http') {
+ proxy.transport = {
+ http: {
+ method: proxy['http-opts']?.method,
+ path: Array.isArray(
+ proxy['http-opts']?.path,
+ )
+ ? proxy['http-opts']?.path[0]
+ : proxy['http-opts']?.path,
+ headers: {
+ Host: Array.isArray(
+ proxy['http-opts']?.headers?.Host,
+ )
+ ? proxy['http-opts']?.headers
+ ?.Host[0]
+ : proxy['http-opts']?.headers?.Host,
+ },
+ skip_tls_verify: proxy['skip-cert-verify'],
+ },
+ };
+ } else if (proxy.network === 'tcp' || !proxy.network) {
+ let reality;
+ if (
+ proxy['reality-opts']?.['short-id'] ||
+ proxy['reality-opts']?.['public-key']
+ ) {
+ reality = {
+ short_id: proxy['reality-opts']['short-id'],
+ public_key:
+ proxy['reality-opts']['public-key'],
+ };
+ }
+ proxy.transport = {
+ [proxy.tls ? 'tls' : 'tcp']: {
+ sni: proxy.tls ? proxy.sni : undefined,
+ skip_tls_verify: proxy.tls
+ ? proxy['skip-cert-verify']
+ : undefined,
+ reality,
+ },
+ };
+ flow = proxy.flow;
+ if (flow === '') flow = undefined;
+ }
+ proxy = {
+ type: 'vless',
+ name: proxy.name,
+ server: proxy.server,
+ port: proxy.port,
+ user_id: proxy.uuid,
+ security: proxy.cipher,
+ tfo: proxy.tfo || proxy['fast-open'],
+ udp_relay:
+ proxy.udp || proxy.udp_relay || proxy.udp_relay,
+ next_hop: proxy.next_hop,
+ transport: proxy.transport,
+ flow,
+ };
+ } else if (proxy.type === 'wireguard') {
+ if (
+ Array.isArray(proxy.peers) &&
+ proxy.peers.length > 0
+ ) {
+ proxy.server = proxy.peers[0].server;
+ proxy.port = proxy.peers[0].port;
+ proxy.ip = proxy.peers[0].ip;
+ proxy.ipv6 = proxy.peers[0].ipv6;
+ proxy['public-key'] = proxy.peers[0]['public-key'];
+ proxy['preshared-key'] =
+ proxy.peers[0]['pre-shared-key'];
+ proxy['allowed-ips'] =
+ proxy.peers[0]['allowed-ips'];
+ proxy.reserved = proxy.peers[0].reserved;
+ }
+ proxy = {
+ type: 'wireguard',
+ name: proxy.name,
+ local_ipv4: getWireGuardAddressWithCIDR(
+ proxy,
+ 'ipv4',
+ ),
+ local_ipv6: getWireGuardAddressWithCIDR(
+ proxy,
+ 'ipv6',
+ ),
+ server: proxy.server,
+ port: proxy.port,
+ private_key: proxy['private-key'],
+ peer_public_key: proxy['public-key'],
+ preshared_key: proxy['preshared-key'],
+ reserved: proxy.reserved
+ ? Array.isArray(proxy.reserved)
+ ? proxy.reserved
+ : proxy.reserved
+ .split(/\s*\/\s*/)
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0)
+ : undefined,
+ dns_servers: proxy.dns
+ ? Array.isArray(proxy.dns)
+ ? proxy.dns
+ : proxy.dns
+ .split(/\s*,\s*/)
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0)
+ : undefined,
+ mtu: proxy.mtu,
+ keepalive: proxy.keepalive,
+ };
+ }
+ if (
+ [
+ 'http',
+ 'https',
+ 'socks5',
+ 'ss',
+ 'trojan',
+ 'vless',
+ 'vmess',
+ 'anytls',
+ ].includes(original.type)
+ ) {
+ if (isPresent(original, 'shadow-tls-password')) {
+ if (original['shadow-tls-version'] != 3)
+ throw new Error(
+ `shadow-tls version ${original['shadow-tls-version']} is not supported`,
+ );
+ proxy.shadow_tls = {
+ password: original['shadow-tls-password'],
+ sni: original['shadow-tls-sni'],
+ };
+ } else if (
+ ['shadow-tls'].includes(original.plugin) &&
+ original['plugin-opts']
+ ) {
+ if (original['plugin-opts'].version != 3)
+ throw new Error(
+ `shadow-tls version ${original['plugin-opts'].version} is not supported`,
+ );
+ proxy.shadow_tls = {
+ password: original['plugin-opts'].password,
+ sni: original['plugin-opts'].host,
+ };
+ }
+ }
+ if (
+ [
+ 'socks5',
+ 'ss',
+ 'trojan',
+ 'vless',
+ 'vmess',
+ 'wireguard',
+ 'tuic',
+ 'hysteria2',
+ 'anytls',
+ ].includes(original.type)
+ ) {
+ if (
+ ['on', 'true', true, '1', 1].includes(
+ original['block-quic'],
+ )
+ ) {
+ proxy.block_quic = true;
+ } else if (
+ ['off', 'false', false, '0', 0].includes(
+ original['block-quic'],
+ )
+ ) {
+ proxy.block_quic = false;
+ }
+ }
+ if (
+ ['ss'].includes(original.type) &&
+ proxy.shadow_tls &&
+ original['udp-port'] > 0 &&
+ original['udp-port'] <= 65535
+ ) {
+ proxy['udp_port'] = original['udp-port'];
+ }
+
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+
+ if (proxy.transport) {
+ for (const key in proxy.transport) {
+ if (
+ Object.keys(proxy.transport[key]).length ===
+ 0 ||
+ Object.values(proxy.transport[key]).every(
+ (value) => value == null,
+ )
+ ) {
+ delete proxy.transport[key];
+ }
+ }
+ if (Object.keys(proxy.transport).length === 0) {
+ delete proxy.transport;
+ }
+ }
+
+ if (type !== 'internal') {
+ for (const key in proxy) {
+ if (proxy[key] == null || /^_/i.test(key)) {
+ delete proxy[key];
+ }
+ }
+ }
+ return {
+ [proxy.type]: {
+ ...proxy,
+ type: undefined,
+ prev_hop,
+ },
+ };
+ } catch (err) {
+ $.error(
+ `Cannot produce proxy: ${proxy.name}\nReason: ${err}`,
+ );
+ return null;
+ }
+ })
+ .filter(Boolean);
+ return produceProxyListOutput(list, type, opts);
+ };
+ return { type, produce };
+}
+
+function hasHeaders(proxy) {
+ return (
+ proxy?.headers &&
+ typeof proxy.headers === 'object' &&
+ Object.keys(proxy.headers).length > 0
+ );
+}
diff --git a/backend/src/core/proxy-utils/producers/index.js b/backend/src/core/proxy-utils/producers/index.js
new file mode 100644
index 0000000000..2915a77572
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/index.js
@@ -0,0 +1,56 @@
+import Surge_Producer from './surge';
+import SurgeMac_Producer from './surgemac';
+import Clash_Producer from './clash';
+import ClashMeta_Producer from './clashmeta';
+import Stash_Producer from './stash';
+import Loon_Producer from './loon';
+import URI_Producer from './uri';
+import V2Ray_Producer from './v2ray';
+import QX_Producer from './qx';
+import Shadowrocket_Producer from './shadowrocket';
+import Surfboard_Producer from './surfboard';
+import singbox_Producer from './sing-box';
+import Egern_Producer from './egern';
+
+function JSON_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type) =>
+ type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
+ return { type, produce };
+}
+
+export default {
+ qx: QX_Producer(),
+ QX: QX_Producer(),
+ QuantumultX: QX_Producer(),
+ surge: Surge_Producer(),
+ Surge: Surge_Producer(),
+ SurgeMac: SurgeMac_Producer(),
+ Loon: Loon_Producer(),
+ Clash: Clash_Producer(),
+ meta: ClashMeta_Producer(),
+ clashmeta: ClashMeta_Producer(),
+ 'clash.meta': ClashMeta_Producer(),
+ 'Clash.Meta': ClashMeta_Producer(),
+ ClashMeta: ClashMeta_Producer(),
+ mihomo: ClashMeta_Producer(),
+ Mihomo: ClashMeta_Producer(),
+ uri: URI_Producer(),
+ URI: URI_Producer(),
+ v2: V2Ray_Producer(),
+ v2ray: V2Ray_Producer(),
+ V2Ray: V2Ray_Producer(),
+ json: JSON_Producer(),
+ JSON: JSON_Producer(),
+ stash: Stash_Producer(),
+ Stash: Stash_Producer(),
+ shadowrocket: Shadowrocket_Producer(),
+ Shadowrocket: Shadowrocket_Producer(),
+ ShadowRocket: Shadowrocket_Producer(),
+ surfboard: Surfboard_Producer(),
+ Surfboard: Surfboard_Producer(),
+ singbox: singbox_Producer(),
+ 'sing-box': singbox_Producer(),
+ egern: Egern_Producer(),
+ Egern: Egern_Producer(),
+};
diff --git a/backend/src/core/proxy-utils/producers/loon.js b/backend/src/core/proxy-utils/producers/loon.js
new file mode 100644
index 0000000000..d9f9fa9020
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/loon.js
@@ -0,0 +1,808 @@
+/* eslint-disable no-case-declarations */
+const targetPlatform = 'Loon';
+import { isPresent, Result } from './utils';
+import { isIPv4, isIPv6 } from '@/utils';
+import $ from '@/core/app';
+
+const ipVersions = {
+ dual: 'dual',
+ ipv4: 'v4-only',
+ ipv6: 'v6-only',
+ 'ipv4-prefer': 'prefer-v4',
+ 'ipv6-prefer': 'prefer-v6',
+};
+
+export default function Loon_Producer() {
+ const produce = (proxy, type, opts = {}) => {
+ if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,
+ );
+ }
+ switch (proxy.type) {
+ case 'ss':
+ return shadowsocks(proxy);
+ case 'ssr':
+ return shadowsocksr(proxy);
+ case 'trojan':
+ return trojan(proxy);
+ case 'vmess':
+ return vmess(proxy, opts['include-unsupported-proxy']);
+ case 'vless':
+ return vless(proxy, opts['include-unsupported-proxy']);
+ case 'http':
+ return http(proxy);
+ case 'socks5':
+ return socks5(proxy);
+ case 'wireguard':
+ return wireguard(proxy);
+ case 'hysteria2':
+ return hysteria2(proxy);
+ }
+ if (proxy.type === 'anytls') {
+ if (
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) && proxy['reality-opts']))
+ ) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support proxy type ${proxy.type} with network or REALITY`,
+ );
+ }
+
+ return anytls(proxy);
+ }
+ throw new Error(
+ `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
+ );
+ };
+ return { produce };
+}
+
+function shadowsocks(proxy) {
+ const result = new Result(proxy);
+ if (
+ ![
+ 'rc4',
+ 'rc4-md5',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'bf-cfb',
+ 'camellia-128-cfb',
+ 'camellia-192-cfb',
+ 'camellia-256-cfb',
+ 'salsa20',
+ 'chacha20',
+ 'chacha20-ietf',
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher)
+ ) {
+ throw new Error(`cipher ${proxy.cipher} is not supported`);
+ }
+ result.append(
+ `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
+ );
+
+ // obfs
+ if (isPresent(proxy, 'plugin')) {
+ if (proxy.plugin === 'obfs') {
+ if (
+ proxy['plugin-opts']?.mode &&
+ proxy.cipher.startsWith('2022-')
+ ) {
+ throw new Error(
+ `${proxy.cipher} ${proxy.plugin} is not supported`,
+ );
+ }
+ result.append(`,obfs-name=${proxy['plugin-opts'].mode}`);
+ result.appendIfPresent(
+ `,obfs-host=${proxy['plugin-opts'].host}`,
+ 'plugin-opts.host',
+ );
+ result.appendIfPresent(
+ `,obfs-uri=${proxy['plugin-opts'].path}`,
+ 'plugin-opts.path',
+ );
+ } else if (!['shadow-tls'].includes(proxy.plugin)) {
+ throw new Error(`plugin ${proxy.plugin} is not supported`);
+ }
+ }
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ // udp-port
+ result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
+ } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
+ const password = proxy['plugin-opts'].password;
+ const host = proxy['plugin-opts'].host;
+ const version = proxy['plugin-opts'].version;
+ if (password) {
+ result.append(`,shadow-tls-password=${password}`);
+ if (host) {
+ result.append(`,shadow-tls-sni=${host}`);
+ }
+ if (version) {
+ if (version < 2) {
+ throw new Error(
+ `shadow-tls version ${version} is not supported`,
+ );
+ }
+ result.append(`,shadow-tls-version=${version}`);
+ }
+ // udp-port
+ result.appendIfPresent(
+ `,udp-port=${proxy['udp-port']}`,
+ 'udp-port',
+ );
+ }
+ }
+
+ // udp over tcp
+ if (proxy['udp-over-tcp']) {
+ if (proxy['udp-over-tcp-version'] === 2) {
+ if (proxy.plugin === 'obfs') {
+ $.error(
+ `Platform ${targetPlatform} shadowsocks udp-over-tcp does not support obfs`,
+ );
+ } else {
+ result.append(`,udp-over-tcp=true`);
+ }
+ } else {
+ $.error(
+ `Platform ${targetPlatform} shadowsocks only supports udp-over-tcp-version 2`,
+ );
+ }
+ }
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+
+function shadowsocksr(proxy) {
+ const result = new Result(proxy);
+ result.append(
+ `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
+ );
+
+ // ssr protocol
+ result.append(`,protocol=${proxy.protocol}`);
+ result.appendIfPresent(
+ `,protocol-param=${proxy['protocol-param']}`,
+ 'protocol-param',
+ );
+
+ // obfs
+ result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
+ result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ // udp-port
+ result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
+ } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
+ const password = proxy['plugin-opts'].password;
+ const host = proxy['plugin-opts'].host;
+ const version = proxy['plugin-opts'].version;
+ if (password) {
+ result.append(`,shadow-tls-password=${password}`);
+ if (host) {
+ result.append(`,shadow-tls-sni=${host}`);
+ }
+ if (version) {
+ if (version < 2) {
+ throw new Error(
+ `shadow-tls version ${version} is not supported`,
+ );
+ }
+ result.append(`,shadow-tls-version=${version}`);
+ }
+ // udp-port
+ result.appendIfPresent(
+ `,udp-port=${proxy['udp-port']}`,
+ 'udp-port',
+ );
+ }
+ }
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+
+function trojan(proxy) {
+ const result = new Result(proxy);
+ result.append(
+ `${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
+ );
+ if (proxy.network === 'tcp') {
+ delete proxy.network;
+ }
+ // transport
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ result.append(`,transport=ws`);
+ result.appendIfPresent(
+ `,path=${proxy['ws-opts']?.path}`,
+ 'ws-opts.path',
+ );
+ result.appendIfPresent(
+ `,host=${proxy['ws-opts']?.headers?.Host}`,
+ 'ws-opts.headers.Host',
+ );
+ } else {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ }
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // sni
+ result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+
+function anytls(proxy) {
+ const result = new Result(proxy);
+ result.append(
+ `${proxy.name}=anytls,${proxy.server},${proxy.port},"${proxy.password}"`,
+ );
+ // 新版删除idle-session-check-interval和min-idle-session 参数,session 改为主动超时机制,由于 anytls-go 不支持一个tcp 并发多个 stream,max-stream-cout 设置大于 1 时会有阻塞,如果有其他支持多路复用的 anytls 服务器实现,可以设置max-stream-cout 大于 1
+ for (const key of [
+ // 'idle-session-check-interval',
+ 'idle-session-timeout',
+ // 'min-idle-session',
+ 'max-stream-count',
+ ]) {
+ // 值为整数 才附加
+ if (isPresent(proxy, key) && Number.isInteger(proxy[key])) {
+ result.append(`,${key}=${proxy[key]}`);
+ }
+ }
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // sni
+ result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+
+function vmess(proxy) {
+ const isReality = !!proxy['reality-opts'];
+
+ const result = new Result(proxy);
+ result.append(
+ `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
+ );
+ if (proxy.network === 'tcp') {
+ delete proxy.network;
+ }
+ // transport
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ result.append(`,transport=ws`);
+ result.appendIfPresent(
+ `,path=${proxy['ws-opts']?.path}`,
+ 'ws-opts.path',
+ );
+ result.appendIfPresent(
+ `,host=${proxy['ws-opts']?.headers?.Host}`,
+ 'ws-opts.headers.Host',
+ );
+ } else if (proxy.network === 'http') {
+ result.append(`,transport=http`);
+ let httpPath = proxy['http-opts']?.path;
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ result.appendIfPresent(
+ `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
+ 'http-opts.path',
+ );
+ result.appendIfPresent(
+ `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
+ 'http-opts.headers.Host',
+ );
+ } else {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ } else {
+ result.append(`,transport=tcp`);
+ }
+
+ // tls
+ result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ if (isReality) {
+ result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,public-key="${proxy['reality-opts']['public-key']}"`,
+ 'reality-opts.public-key',
+ );
+ result.appendIfPresent(
+ `,short-id=${proxy['reality-opts']['short-id']}`,
+ 'reality-opts.short-id',
+ );
+ } else {
+ // sni
+ result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ }
+
+ // AEAD
+ if (isPresent(proxy, 'aead')) {
+ result.append(`,alterId=${proxy.aead ? 0 : 1}`);
+ } else {
+ result.append(`,alterId=${proxy.alterId}`);
+ }
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+ return result.toString();
+}
+
+function vless(proxy) {
+ if (proxy.encryption && proxy.encryption !== 'none')
+ throw new Error(`VLESS encryption is not supported`);
+ let isXtls = false;
+ const isReality = !!proxy['reality-opts'];
+
+ if (typeof proxy.flow !== 'undefined') {
+ if (['xtls-rprx-vision'].includes(proxy.flow)) {
+ isXtls = true;
+ } else {
+ throw new Error(`VLESS flow(${proxy.flow}) is not supported`);
+ }
+ }
+
+ const result = new Result(proxy);
+ result.append(
+ `${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
+ );
+ if (proxy.network === 'tcp') {
+ delete proxy.network;
+ }
+ // transport
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ result.append(`,transport=ws`);
+ result.appendIfPresent(
+ `,path=${proxy['ws-opts']?.path}`,
+ 'ws-opts.path',
+ );
+ result.appendIfPresent(
+ `,host=${proxy['ws-opts']?.headers?.Host}`,
+ 'ws-opts.headers.Host',
+ );
+ } else if (proxy.network === 'http') {
+ result.append(`,transport=http`);
+ let httpPath = proxy['http-opts']?.path;
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ result.appendIfPresent(
+ `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
+ 'http-opts.path',
+ );
+ result.appendIfPresent(
+ `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
+ 'http-opts.headers.Host',
+ );
+ } else {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ } else {
+ result.append(`,transport=tcp`);
+ }
+
+ // tls
+ result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ if (isXtls) {
+ result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
+ }
+ if (isReality) {
+ result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,public-key="${proxy['reality-opts']['public-key']}"`,
+ 'reality-opts.public-key',
+ );
+ result.appendIfPresent(
+ `,short-id=${proxy['reality-opts']['short-id']}`,
+ 'reality-opts.short-id',
+ );
+ } else {
+ // sni
+ result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ }
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+ return result.toString();
+}
+
+function http(proxy) {
+ const result = new Result(proxy);
+ const type = proxy.tls ? 'https' : 'http';
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,${proxy.username}`, 'username');
+ result.appendIfPresent(`,"${proxy.password}"`, 'password');
+
+ // sni
+ result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+function socks5(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,${proxy.username}`, 'username');
+ result.appendIfPresent(`,"${proxy.password}"`, 'password');
+
+ // tls
+ result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
+
+ // sni
+ result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
+
+ // tls verification
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
+
+function wireguard(proxy) {
+ if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
+ proxy.server = proxy.peers[0].server;
+ proxy.port = proxy.peers[0].port;
+ proxy.ip = proxy.peers[0].ip;
+ proxy.ipv6 = proxy.peers[0].ipv6;
+ proxy['public-key'] = proxy.peers[0]['public-key'];
+ proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
+ // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
+ proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
+ proxy.reserved = proxy.peers[0].reserved;
+ }
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=wireguard`);
+
+ result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
+ result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
+
+ result.appendIfPresent(
+ `,private-key="${proxy['private-key']}"`,
+ 'private-key',
+ );
+ result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
+
+ if (proxy.dns) {
+ if (Array.isArray(proxy.dns)) {
+ proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
+ let dns = proxy.dns.find((i) => isIPv4(i));
+ if (!dns) {
+ dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));
+ }
+ proxy.dns = dns;
+ }
+ }
+ result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
+ result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
+ result.appendIfPresent(
+ `,keepalive=${proxy['persistent-keepalive']}`,
+ 'persistent-keepalive',
+ );
+ result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
+ const allowedIps = Array.isArray(proxy['allowed-ips'])
+ ? proxy['allowed-ips'].join(',')
+ : proxy['allowed-ips'];
+ let reserved = Array.isArray(proxy.reserved)
+ ? proxy.reserved.join(',')
+ : proxy.reserved;
+ if (reserved) {
+ reserved = `,reserved=[${reserved}]`;
+ }
+ let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
+ if (presharedKey) {
+ presharedKey = `,preshared-key="${presharedKey}"`;
+ }
+ result.append(
+ `,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
+ allowedIps ?? '0.0.0.0/0,::/0'
+ }",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
+ presharedKey ?? ''
+ }}]`,
+ );
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ return result.toString();
+}
+
+function hysteria2(proxy) {
+ if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
+ throw new Error(`only salamander obfs is supported`);
+ }
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
+
+ result.appendIfPresent(`,"${proxy.password}"`, 'password');
+
+ if (isPresent(proxy, 'ports') && `${proxy.ports}`.trim().length > 0) {
+ result.append(`,server-ports="${proxy.ports}"`);
+ }
+
+ if (
+ isPresent(proxy, 'hop-interval') &&
+ `${proxy['hop-interval']}`.trim().length > 0
+ ) {
+ result.append(`,hop-interval=${proxy['hop-interval']}`);
+ }
+
+ // sni
+ result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
+ result.appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
+ result.append(`,salamander-password=${proxy['obfs-password']}`);
+ }
+
+ // tfo
+ result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // block-quic
+ if (proxy['block-quic'] === 'on') {
+ result.append(',block-quic=true');
+ } else if (proxy['block-quic'] === 'off') {
+ result.append(',block-quic=false');
+ }
+
+ // udp
+ if (proxy.udp) {
+ result.append(`,udp=true`);
+ }
+
+ // download-bandwidth
+ result.appendIfPresent(
+ `,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
+ 'down',
+ );
+
+ result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
+
+ return result.toString();
+}
diff --git a/backend/src/core/proxy-utils/producers/qx.js b/backend/src/core/proxy-utils/producers/qx.js
new file mode 100644
index 0000000000..0b41335f35
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/qx.js
@@ -0,0 +1,713 @@
+import { isPresent, isShadowsocksOverTls, Result } from './utils';
+
+const targetPlatform = 'QX';
+
+export default function QX_Producer() {
+ // eslint-disable-next-line no-unused-vars
+ const produce = (proxy, type, opts = {}) => {
+ if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,
+ );
+ }
+ switch (proxy.type) {
+ case 'ss':
+ return shadowsocks(proxy);
+ case 'ssr':
+ return shadowsocksr(proxy);
+ case 'trojan':
+ return trojan(proxy);
+ case 'vmess':
+ return vmess(proxy);
+ case 'http':
+ return http(proxy);
+ case 'socks5':
+ return socks5(proxy);
+ case 'vless':
+ return vless(proxy);
+ case 'anytls':
+ return anytls(proxy);
+ }
+ throw new Error(
+ `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
+ );
+ };
+ return {
+ produce: (proxy, type, opts = {}) => {
+ let result = produce(proxy, type, opts);
+ if (proxy.flow && proxy.flow !== 'xtls-rprx-vision') {
+ throw new Error(
+ `Platform ${targetPlatform} does not support flow ${proxy.flow}`,
+ );
+ }
+ if (proxy['reality-opts']) {
+ if (proxy['reality-opts']['public-key']) {
+ result = `${result},reality-base64-pubkey=${proxy['reality-opts']['public-key']}`;
+ }
+ if (proxy['reality-opts']['short-id']) {
+ result = `${result},reality-hex-shortid=${proxy['reality-opts']['short-id']}`;
+ }
+ }
+ return result;
+ },
+ };
+}
+
+function getQxHttpObfs(proxy) {
+ // QX accepts multiple http-obfs spellings for ss/vmess/vless. Preserve
+ // the original token for round-trip output, including "vemss-http".
+ return ['http', 'vmess-http', 'vemss-http', 'shadowsocks-http'].includes(
+ proxy._qx_obfs_http,
+ )
+ ? proxy._qx_obfs_http
+ : 'http';
+}
+
+function shadowsocks(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+ const isSSOverTls = isShadowsocksOverTls(proxy);
+ if (!proxy.cipher) {
+ proxy.cipher = 'none';
+ }
+ if (
+ ![
+ 'none',
+ 'rc4-md5',
+ 'rc4-md5-6',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'bf-cfb',
+ 'cast5-cfb',
+ 'des-cfb',
+ 'rc2-cfb',
+ 'salsa20',
+ 'chacha20',
+ 'chacha20-ietf',
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher)
+ ) {
+ throw new Error(`cipher ${proxy.cipher} is not supported`);
+ }
+ append(`shadowsocks=${proxy.server}:${proxy.port}`);
+ append(`,method=${proxy.cipher}`);
+ append(`,password=${proxy.password}`);
+
+ // obfs
+ if (needTls(proxy)) {
+ proxy.tls = true;
+ }
+ if (isSSOverTls) {
+ append(`,obfs=over-tls`);
+ if (isPresent(proxy, 'sni')) {
+ append(`,obfs-host=${proxy.sni}`);
+ } else {
+ appendIfPresent(`,obfs-host=${proxy.servername}`, 'servername');
+ }
+ } else if (isPresent(proxy, 'plugin')) {
+ if (proxy.plugin === 'obfs') {
+ const opts = proxy['plugin-opts'];
+ if (opts.mode === 'http') {
+ // Keep the original QX http-obfs token instead of collapsing
+ // it back to plain "http".
+ append(`,obfs=${getQxHttpObfs(proxy)}`);
+ } else {
+ append(`,obfs=${opts.mode}`);
+ }
+ } else if (
+ proxy.plugin === 'v2ray-plugin' &&
+ proxy['plugin-opts'].mode === 'websocket'
+ ) {
+ const opts = proxy['plugin-opts'];
+ if (opts.tls) append(`,obfs=wss`);
+ else append(`,obfs=ws`);
+ } else {
+ throw new Error(`plugin is not supported`);
+ }
+ appendIfPresent(
+ `,obfs-host=${proxy['plugin-opts'].host}`,
+ 'plugin-opts.host',
+ );
+ appendIfPresent(
+ `,obfs-uri=${proxy['plugin-opts'].path}`,
+ 'plugin-opts.path',
+ );
+ }
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ if (!isSSOverTls) {
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+ }
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // udp over tcp
+ if (proxy['_ssr_python_uot']) {
+ append(`,udp-over-tcp=true`);
+ } else if (proxy['udp-over-tcp']) {
+ if (
+ !proxy['udp-over-tcp-version'] ||
+ proxy['udp-over-tcp-version'] === 1
+ ) {
+ append(`,udp-over-tcp=sp.v1`);
+ } else if (proxy['udp-over-tcp-version'] === 2) {
+ append(`,udp-over-tcp=sp.v2`);
+ }
+ }
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function shadowsocksr(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`shadowsocks=${proxy.server}:${proxy.port}`);
+ append(`,method=${proxy.cipher}`);
+ append(`,password=${proxy.password}`);
+
+ // ssr protocol
+ append(`,ssr-protocol=${proxy.protocol}`);
+ appendIfPresent(
+ `,ssr-protocol-param=${proxy['protocol-param']}`,
+ 'protocol-param',
+ );
+
+ // obfs
+ appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
+ appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param');
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function trojan(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`trojan=${proxy.server}:${proxy.port}`);
+ append(`,password=${proxy.password}`);
+
+ // obfs ws
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ if (needTls(proxy)) append(`,obfs=wss`);
+ else append(`,obfs=ws`);
+ appendIfPresent(
+ `,obfs-uri=${proxy['ws-opts']?.path}`,
+ 'ws-opts.path',
+ );
+ appendIfPresent(
+ `,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
+ 'ws-opts.headers.Host',
+ );
+ } else if (!['tcp'].includes(proxy.network)) {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ }
+
+ // over tls
+ if (proxy.network !== 'ws' && needTls(proxy)) {
+ append(`,over-tls=true`);
+ }
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function vmess(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`vmess=${proxy.server}:${proxy.port}`);
+
+ // cipher
+ let cipher;
+ if (proxy.cipher === 'auto') {
+ cipher = 'chacha20-ietf-poly1305';
+ } else {
+ cipher = proxy.cipher;
+ }
+ append(`,method=${cipher}`);
+
+ append(`,password=${proxy.uuid}`);
+
+ // obfs
+ if (needTls(proxy)) {
+ proxy.tls = true;
+ }
+
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ if (proxy.tls) append(`,obfs=wss`);
+ else append(`,obfs=ws`);
+ } else if (proxy.network === 'http') {
+ append(`,obfs=${getQxHttpObfs(proxy)}`);
+ } else if (['tcp'].includes(proxy.network)) {
+ if (proxy.tls) append(`,obfs=over-tls`);
+ } else if (!['tcp'].includes(proxy.network)) {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ let transportPath = proxy[`${proxy.network}-opts`]?.path;
+ let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
+ appendIfPresent(
+ `,obfs-uri=${
+ Array.isArray(transportPath) ? transportPath[0] : transportPath
+ }`,
+ `${proxy.network}-opts.path`,
+ );
+ appendIfPresent(
+ `,obfs-host=${
+ Array.isArray(transportHost) ? transportHost[0] : transportHost
+ }`,
+ `${proxy.network}-opts.headers.Host`,
+ );
+ } else {
+ // over-tls
+ if (proxy.tls) append(`,obfs=over-tls`);
+ }
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+
+ // AEAD
+ if (isPresent(proxy, 'aead')) {
+ append(`,aead=${proxy.aead}`);
+ } else {
+ append(`,aead=${proxy.alterId === 0}`);
+ }
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+function vless(proxy) {
+ if (proxy.encryption && proxy.encryption !== 'none')
+ throw new Error(`VLESS encryption is not supported`);
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`vless=${proxy.server}:${proxy.port}`);
+
+ // The method field for vless should be none.
+ let cipher = 'none';
+ // if (proxy.cipher === 'auto') {
+ // cipher = 'chacha20-ietf-poly1305';
+ // } else {
+ // cipher = proxy.cipher;
+ // }
+ append(`,method=${cipher}`);
+
+ append(`,password=${proxy.uuid}`);
+
+ // obfs
+ if (needTls(proxy)) {
+ proxy.tls = true;
+ }
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ if (proxy.tls) append(`,obfs=wss`);
+ else append(`,obfs=ws`);
+ } else if (proxy.network === 'http') {
+ append(`,obfs=${getQxHttpObfs(proxy)}`);
+ } else if (['tcp'].includes(proxy.network)) {
+ if (proxy.tls) append(`,obfs=over-tls`);
+ } else if (!['tcp'].includes(proxy.network)) {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ let transportPath = proxy[`${proxy.network}-opts`]?.path;
+ let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
+ appendIfPresent(
+ `,obfs-uri=${
+ Array.isArray(transportPath) ? transportPath[0] : transportPath
+ }`,
+ `${proxy.network}-opts.path`,
+ );
+ appendIfPresent(
+ `,obfs-host=${
+ Array.isArray(transportHost) ? transportHost[0] : transportHost
+ }`,
+ `${proxy.network}-opts.headers.Host`,
+ );
+ } else {
+ // over-tls
+ if (proxy.tls) append(`,obfs=over-tls`);
+ }
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+
+ appendIfPresent(`,vless-flow=${proxy.flow}`, 'flow');
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function anytls(proxy) {
+ const network = proxy.network?.trim().toLowerCase();
+ if (network && network !== 'tcp') {
+ throw new Error(
+ `Platform ${targetPlatform} does not support AnyTLS with transport ${proxy.network}`,
+ );
+ }
+
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`anytls=${proxy.server}:${proxy.port}`);
+ append(`,password=${proxy.password}`);
+
+ proxy.tls = true;
+ append(`,over-tls=true`);
+
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function http(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`http=${proxy.server}:${proxy.port}`);
+ appendIfPresent(`,username=${proxy.username}`, 'username');
+ appendIfPresent(`,password=${proxy.password}`, 'password');
+
+ // tls
+ if (needTls(proxy)) {
+ proxy.tls = true;
+ }
+ appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function socks5(proxy) {
+ const result = new Result(proxy);
+ const append = result.append.bind(result);
+ const appendIfPresent = result.appendIfPresent.bind(result);
+
+ append(`socks5=${proxy.server}:${proxy.port}`);
+ appendIfPresent(`,username=${proxy.username}`, 'username');
+ appendIfPresent(`,password=${proxy.password}`, 'password');
+
+ // tls
+ if (needTls(proxy)) {
+ proxy.tls = true;
+ }
+ appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
+
+ if (needTls(proxy)) {
+ appendIfPresent(
+ `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
+ 'tls-pubkey-sha256',
+ );
+ appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
+ appendIfPresent(
+ `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
+ 'tls-no-session-ticket',
+ );
+ appendIfPresent(
+ `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
+ 'tls-no-session-reuse',
+ );
+ // tls fingerprint
+ appendIfPresent(
+ `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ appendIfPresent(
+ `,tls-verification=${!proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
+ }
+
+ // tfo
+ appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+
+ // udp
+ appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // server_check_url
+ result.appendIfPresent(
+ `,server_check_url=${proxy['test-url']}`,
+ 'test-url',
+ );
+
+ // tag
+ append(`,tag=${proxy.name}`);
+
+ return result.toString();
+}
+
+function needTls(proxy) {
+ return proxy.tls;
+}
diff --git a/backend/src/core/proxy-utils/producers/shadowrocket.js b/backend/src/core/proxy-utils/producers/shadowrocket.js
new file mode 100644
index 0000000000..14d87e43d7
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/shadowrocket.js
@@ -0,0 +1,287 @@
+import {
+ getWireGuardAddressWithCIDR,
+ isPresent,
+ isShadowsocksOverTls,
+ produceProxyListOutput,
+ supportsShadowsocksV2rayPluginMode,
+} from '@/core/proxy-utils/producers/utils';
+import {
+ deleteHttpUpgradeEarlyDataMetadata,
+ normalizeWebSocketEarlyDataPath,
+} from '../transport-path';
+import $ from '@/core/app';
+
+export default function Shadowrocket_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ const list = proxies
+ .filter((proxy) => {
+ if (opts['include-unsupported-proxy']) return true;
+ if (
+ !supportsShadowsocksV2rayPluginMode(proxy, [
+ 'websocket',
+ 'quic',
+ 'http2',
+ 'mkcp',
+ 'grpc',
+ ])
+ ) {
+ return false;
+ } else if (proxy.type === 'snell' && proxy.version >= 4) {
+ return false;
+ } else if (
+ [
+ 'tailscale',
+ 'sudoku',
+ 'naive',
+ 'masque',
+ 'openvpn',
+ 'gost-relay',
+ ].includes(proxy.type)
+ ) {
+ return false;
+ } else if (['xhttp'].includes(proxy.network)) {
+ $.warn(
+ `VLESS XHTTP 结构复杂, Shadowrocket 可能无法完全兼容`,
+ );
+ return true;
+ }
+ return true;
+ })
+ .map((proxy) => {
+ if (proxy.type === 'vmess') {
+ // handle vmess aead
+ if (isPresent(proxy, 'aead')) {
+ if (proxy.aead) {
+ proxy.alterId = 0;
+ }
+ delete proxy.aead;
+ }
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
+ // https://stash.wiki/proxy-protocols/proxy-types#vmess
+ if (
+ isPresent(proxy, 'cipher') &&
+ ![
+ 'auto',
+ 'none',
+ 'zero',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ ].includes(proxy.cipher)
+ ) {
+ proxy.cipher = 'auto';
+ }
+ } else if (proxy.type === 'tuic') {
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ // else {
+ // proxy.alpn = ['h3'];
+ // }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
+ if (
+ (!proxy.token || proxy.token.length === 0) &&
+ !isPresent(proxy, 'version')
+ ) {
+ proxy.version = 5;
+ }
+ } else if (proxy.type === 'hysteria') {
+ // auth_str 将会在未来某个时候删除 但是有的机场不规范
+ if (
+ isPresent(proxy, 'auth_str') &&
+ !isPresent(proxy, 'auth-str')
+ ) {
+ proxy['auth-str'] = proxy['auth_str'];
+ }
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ }
+ } else if (proxy.type === 'hysteria2') {
+ // 新版已更改
+ // if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
+ // proxy.obfs = proxy['obfs-password'];
+ // delete proxy['obfs-password'];
+ // }
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ }
+ } else if (proxy.type === 'wireguard') {
+ proxy.keepalive =
+ proxy.keepalive ?? proxy['persistent-keepalive'];
+ proxy['persistent-keepalive'] = proxy.keepalive;
+ proxy['preshared-key'] =
+ proxy['preshared-key'] ?? proxy['pre-shared-key'];
+ proxy['pre-shared-key'] = proxy['preshared-key'];
+ proxy.ip = getWireGuardAddressWithCIDR(proxy, 'ipv4');
+ proxy.ipv6 = getWireGuardAddressWithCIDR(proxy, 'ipv6');
+ } else if (proxy.type === 'snell' && proxy.version < 3) {
+ delete proxy.udp;
+ } else if (proxy.type === 'vless') {
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ } else if (proxy.type === 'ss') {
+ if (
+ isPresent(proxy, 'shadow-tls-password') &&
+ !isPresent(proxy, 'plugin')
+ ) {
+ proxy.plugin = 'shadow-tls';
+ proxy['plugin-opts'] = {
+ host: proxy['shadow-tls-sni'],
+ password: proxy['shadow-tls-password'],
+ version: proxy['shadow-tls-version'],
+ };
+ delete proxy['shadow-tls-password'];
+ delete proxy['shadow-tls-sni'];
+ delete proxy['shadow-tls-version'];
+ }
+ if (isShadowsocksOverTls(proxy)) {
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ // 先不删 没有明确的规范
+ // delete proxy.sni;
+ }
+ }
+ }
+
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'http'
+ ) {
+ let httpPath = proxy['http-opts']?.path;
+ if (
+ isPresent(proxy, 'http-opts.path') &&
+ !Array.isArray(httpPath)
+ ) {
+ proxy['http-opts'].path = [httpPath];
+ }
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ if (
+ isPresent(proxy, 'http-opts.headers.Host') &&
+ !Array.isArray(httpHost)
+ ) {
+ proxy['http-opts'].headers.Host = [httpHost];
+ }
+ }
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'h2'
+ ) {
+ let path = proxy['h2-opts']?.path;
+ if (
+ isPresent(proxy, 'h2-opts.path') &&
+ Array.isArray(path)
+ ) {
+ proxy['h2-opts'].path = path[0];
+ }
+ let host = proxy['h2-opts']?.headers?.host;
+ if (
+ isPresent(proxy, 'h2-opts.headers.Host') &&
+ !Array.isArray(host)
+ ) {
+ proxy['h2-opts'].headers.host = [host];
+ }
+ }
+ if (['ws'].includes(proxy.network)) {
+ const networkOptsKey = `${proxy.network}-opts`;
+ proxy[networkOptsKey] = proxy[networkOptsKey] || {};
+ if (!proxy[networkOptsKey].path) {
+ proxy[networkOptsKey].path = '/';
+ }
+ normalizeWebSocketEarlyDataPath(proxy[networkOptsKey]);
+ }
+
+ if (proxy['plugin-opts']?.tls) {
+ if (isPresent(proxy, 'skip-cert-verify')) {
+ proxy['plugin-opts']['skip-cert-verify'] =
+ proxy['plugin-opts']['skip-cert-verify'] ||
+ proxy['skip-cert-verify'];
+ }
+ }
+ if (
+ [
+ 'trojan',
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'anytls',
+ 'trusttunnel',
+ 'naive',
+ ].includes(proxy.type)
+ ) {
+ delete proxy.tls;
+ }
+
+ if (proxy['tls-fingerprint']) {
+ proxy.fingerprint = proxy['tls-fingerprint'];
+ }
+ delete proxy['tls-fingerprint'];
+
+ if (proxy['underlying-proxy']) {
+ proxy['dialer-proxy'] = proxy['underlying-proxy'];
+ }
+ delete proxy['underlying-proxy'];
+
+ if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
+ delete proxy.tls;
+ }
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+ delete proxy['ip-cidr'];
+ delete proxy['ipv6-cidr'];
+ if (type !== 'internal') {
+ for (const key in proxy) {
+ if (proxy[key] == null || /^_/i.test(key)) {
+ delete proxy[key];
+ }
+ }
+ deleteHttpUpgradeEarlyDataMetadata(
+ proxy[`${proxy.network}-opts`],
+ );
+ }
+ if (
+ ['grpc'].includes(proxy.network) &&
+ proxy[`${proxy.network}-opts`]
+ ) {
+ delete proxy[`${proxy.network}-opts`]['_grpc-type'];
+ delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
+ }
+ return proxy;
+ });
+ return produceProxyListOutput(list, type, opts);
+ };
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/producers/sing-box.js b/backend/src/core/proxy-utils/producers/sing-box.js
new file mode 100644
index 0000000000..2ceadcf9d7
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/sing-box.js
@@ -0,0 +1,1365 @@
+import ClashMeta_Producer from './clashmeta';
+import $ from '@/core/app';
+import { isPlainObject } from '@/utils';
+import { getWireGuardAddressWithCIDR, normalizePluginMuxValue } from './utils';
+import {
+ extractPathQueryParam,
+ getSafeIntegerPathQueryParam,
+} from '../transport-path';
+
+const ipVersions = {
+ ipv4: 'ipv4_only',
+ ipv6: 'ipv6_only',
+ 'v4-only': 'ipv4_only',
+ 'v6-only': 'ipv6_only',
+ 'ipv4-prefer': 'prefer_ipv4',
+ 'ipv6-prefer': 'prefer_ipv6',
+ 'prefer-v4': 'prefer_ipv4',
+ 'prefer-v6': 'prefer_ipv6',
+};
+
+const ipVersionParser = (proxy, parsedProxy) => {
+ const strategy = ipVersions[proxy['ip-version']];
+ if (proxy._dns_server && strategy) {
+ parsedProxy.domain_resolver = {
+ server: proxy._dns_server,
+ strategy,
+ };
+ }
+};
+const domainResolverParser = (proxy, parsedProxy) => {
+ if (proxy._domain_resolver) {
+ parsedProxy.domain_resolver = {
+ ...parsedProxy.domain_resolver,
+ ...proxy._domain_resolver,
+ };
+ }
+};
+const hasControlHTTPClient = (proxy) => {
+ const value = proxy['control-http-client'];
+ if (value === undefined || value === null) return false;
+ if (typeof value === 'string') return value.trim() !== '';
+ if (isPlainObject(value)) {
+ return Object.values(value).some(
+ (item) => item !== undefined && item !== null && item !== '',
+ );
+ }
+ return true;
+};
+const detourParser = (proxy, parsedProxy) => {
+ parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
+};
+const networkParser = (proxy, parsedProxy) => {
+ if (['tcp', 'udp'].includes(proxy._network)) {
+ parsedProxy.network = proxy._network;
+ return;
+ }
+ if (proxy.udp === false) {
+ parsedProxy.network = 'tcp';
+ }
+};
+const tfoParser = (proxy, parsedProxy) => {
+ parsedProxy.tcp_fast_open = false;
+ if (proxy.tfo) parsedProxy.tcp_fast_open = true;
+ if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
+ if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
+ if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
+};
+
+const smuxParser = (smux, proxy) => {
+ if (!smux || !smux.enabled) return;
+ proxy.multiplex = { enabled: true };
+ proxy.multiplex.protocol = smux.protocol;
+ if (smux['max-connections'])
+ proxy.multiplex.max_connections = parseInt(
+ `${smux['max-connections']}`,
+ 10,
+ );
+ if (smux['max-streams'])
+ proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
+ if (smux['min-streams'])
+ proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
+ if (smux.padding) proxy.multiplex.padding = true;
+ if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {
+ proxy.multiplex.brutal = {
+ enabled: true,
+ };
+ if (smux['brutal-opts']?.up)
+ proxy.multiplex.brutal.up_mbps = parseInt(
+ `${smux['brutal-opts']?.up}`,
+ 10,
+ );
+ if (smux['brutal-opts']?.down)
+ proxy.multiplex.brutal.down_mbps = parseInt(
+ `${smux['brutal-opts']?.down}`,
+ 10,
+ );
+ }
+};
+
+const wsParser = (proxy, parsedProxy) => {
+ const transport = { type: 'ws', headers: {} };
+ if (proxy['ws-opts']) {
+ const {
+ path: wsPath = '',
+ headers: wsHeaders = {},
+ 'max-early-data': max_early_data,
+ 'early-data-header-name': early_data_header_name,
+ } = proxy['ws-opts'];
+ transport.early_data_header_name = early_data_header_name;
+ transport.max_early_data = max_early_data
+ ? parseInt(max_early_data, 10)
+ : undefined;
+ if (wsPath !== '') transport.path = `${wsPath}`;
+ if (Object.keys(wsHeaders).length > 0) {
+ const headers = {};
+ for (const key of Object.keys(wsHeaders)) {
+ let value = wsHeaders[key];
+ if (value === '') continue;
+ if (!Array.isArray(value)) value = [`${value}`];
+ if (value.length > 0) headers[key] = value;
+ }
+ const { Host: wsHost } = headers;
+ if (wsHost.length === 1)
+ for (const item of `Host:${wsHost[0]}`.split('\n')) {
+ const [key, value] = item.split(':');
+ if (value.trim() === '') continue;
+ headers[key.trim()] = value.trim().split(',');
+ }
+ transport.headers = headers;
+ }
+ }
+ if (proxy['ws-headers']) {
+ const headers = {};
+ for (const key of Object.keys(proxy['ws-headers'])) {
+ let value = proxy['ws-headers'][key];
+ if (value === '') continue;
+ if (!Array.isArray(value)) value = [`${value}`];
+ if (value.length > 0) headers[key] = value;
+ }
+ const { Host: wsHost } = headers;
+ if (wsHost.length === 1)
+ for (const item of `Host:${wsHost[0]}`.split('\n')) {
+ const [key, value] = item.split(':');
+ if (value.trim() === '') continue;
+ headers[key.trim()] = value.trim().split(',');
+ }
+ for (const key of Object.keys(headers))
+ transport.headers[key] = headers[key];
+ }
+ if (proxy['ws-path'] && proxy['ws-path'] !== '')
+ transport.path = `${proxy['ws-path']}`;
+ if (transport.path) {
+ const { value: ed, parsed: maxEarlyData } =
+ getSafeIntegerPathQueryParam(transport.path, 'ed');
+ if (ed !== '') {
+ transport.path = extractPathQueryParam(transport.path, 'ed').path;
+ transport.early_data_header_name = 'Sec-WebSocket-Protocol';
+ transport.max_early_data = maxEarlyData;
+ }
+ }
+
+ if (parsedProxy.tls.insecure)
+ parsedProxy.tls.server_name = transport.headers.Host[0];
+ if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
+ transport.type = 'httpupgrade';
+ if (transport.headers.Host) {
+ transport.host = transport.headers.Host[0];
+ delete transport.headers.Host;
+ }
+ if (transport.max_early_data) delete transport.max_early_data;
+ if (transport.early_data_header_name)
+ delete transport.early_data_header_name;
+ }
+ for (const key of Object.keys(transport.headers)) {
+ const value = transport.headers[key];
+ if (value.length === 1) transport.headers[key] = value[0];
+ }
+ parsedProxy.transport = transport;
+};
+
+const h1Parser = (proxy, parsedProxy) => {
+ const transport = { type: 'http', headers: {} };
+ if (proxy['http-opts']) {
+ const {
+ method = '',
+ path: h1Path = '',
+ headers: h1Headers = {},
+ } = proxy['http-opts'];
+ if (method !== '') transport.method = method;
+ if (Array.isArray(h1Path)) {
+ transport.path = `${h1Path[0]}`;
+ } else if (h1Path !== '') transport.path = `${h1Path}`;
+ for (const key of Object.keys(h1Headers)) {
+ let value = h1Headers[key];
+ if (value === '') continue;
+ if (key.toLowerCase() === 'host') {
+ let host = value;
+ if (!Array.isArray(host))
+ host = `${host}`.split(',').map((i) => i.trim());
+ if (host.length > 0) transport.host = host;
+ continue;
+ }
+ if (!Array.isArray(value))
+ value = `${value}`.split(',').map((i) => i.trim());
+ if (value.length > 0) transport.headers[key] = value;
+ }
+ }
+ if (proxy['http-host'] && proxy['http-host'] !== '') {
+ let host = proxy['http-host'];
+ if (!Array.isArray(host))
+ host = `${host}`.split(',').map((i) => i.trim());
+ if (host.length > 0) transport.host = host;
+ }
+ // if (!transport.host) return;
+ if (proxy['http-path'] && proxy['http-path'] !== '') {
+ const path = proxy['http-path'];
+ if (Array.isArray(path)) {
+ transport.path = `${path[0]}`;
+ } else if (path !== '') transport.path = `${path}`;
+ }
+ if (parsedProxy.tls.insecure)
+ parsedProxy.tls.server_name = transport.host[0];
+ if (transport.host?.length === 1) transport.host = transport.host[0];
+ for (const key of Object.keys(transport.headers)) {
+ const value = transport.headers[key];
+ if (value.length === 1) transport.headers[key] = value[0];
+ }
+ parsedProxy.transport = transport;
+};
+
+const h2Parser = (proxy, parsedProxy) => {
+ const transport = { type: 'http' };
+ if (proxy['h2-opts']) {
+ let { host = '', path = '' } = proxy['h2-opts'];
+ if (path !== '') transport.path = `${path}`;
+ if (host !== '') {
+ if (!Array.isArray(host))
+ host = `${host}`.split(',').map((i) => i.trim());
+ if (host.length > 0) transport.host = host;
+ }
+ }
+ if (proxy['h2-host'] && proxy['h2-host'] !== '') {
+ let host = proxy['h2-host'];
+ if (!Array.isArray(host))
+ host = `${host}`.split(',').map((i) => i.trim());
+ if (host.length > 0) transport.host = host;
+ }
+ if (proxy['h2-path'] && proxy['h2-path'] !== '')
+ transport.path = `${proxy['h2-path']}`;
+ parsedProxy.tls.enabled = true;
+ if (parsedProxy.tls.insecure)
+ parsedProxy.tls.server_name = transport.host[0];
+ if (transport.host.length === 1) transport.host = transport.host[0];
+ parsedProxy.transport = transport;
+};
+
+const grpcParser = (proxy, parsedProxy) => {
+ const transport = { type: 'grpc' };
+ if (proxy['grpc-opts']) {
+ const serviceName = proxy['grpc-opts']['grpc-service-name'];
+ if (serviceName != null && serviceName !== '')
+ transport.service_name = `${serviceName}`;
+ }
+ parsedProxy.transport = transport;
+};
+
+const normalizePemLines = (value, label) => {
+ const items = Array.isArray(value) ? value : [value];
+ const lines = [];
+
+ for (const item of items) {
+ const normalized = `${item}`
+ .trim()
+ .replace(/\\r\\n/g, '\n')
+ .replace(/\\n/g, '\n');
+ if (normalized === '') continue;
+
+ for (const line of normalized.split(/\r?\n/)) {
+ const trimmed = line.trim();
+ if (trimmed !== '') lines.push(trimmed);
+ }
+ }
+
+ if (lines.length === 0) return undefined;
+ if (lines.some((line) => /^-----BEGIN [A-Za-z0-9 -]+-----$/.test(line))) {
+ return lines;
+ }
+ return [`-----BEGIN ${label}-----`, ...lines, `-----END ${label}-----`];
+};
+
+const tlsParser = (proxy, parsedProxy) => {
+ if (proxy.tls) parsedProxy.tls.enabled = true;
+ if (proxy.servername && proxy.servername !== '')
+ parsedProxy.tls.server_name = proxy.servername;
+ if (proxy.peer && proxy.peer !== '')
+ parsedProxy.tls.server_name = proxy.peer;
+ if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
+ if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
+ if (proxy.insecure) parsedProxy.tls.insecure = true;
+ if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
+ if (typeof proxy.alpn === 'string') {
+ parsedProxy.tls.alpn = [proxy.alpn];
+ } else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
+ if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
+ if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
+ if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
+ if (proxy['reality-opts']) {
+ parsedProxy.tls.reality = { enabled: true };
+ if (proxy['reality-opts']['public-key'])
+ parsedProxy.tls.reality.public_key =
+ proxy['reality-opts']['public-key'];
+ if (proxy['reality-opts']['short-id'])
+ parsedProxy.tls.reality.short_id =
+ proxy['reality-opts']['short-id'];
+ parsedProxy.tls.utls = { enabled: true };
+ }
+ if (
+ !['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
+ proxy['client-fingerprint'] &&
+ proxy['client-fingerprint'] !== ''
+ )
+ parsedProxy.tls.utls = {
+ enabled: true,
+ fingerprint: proxy['client-fingerprint'],
+ };
+ if (proxy._ech && isPlainObject(proxy._ech)) {
+ parsedProxy.tls.ech = proxy._ech;
+ } else if (proxy['ech-opts'] && isPlainObject(proxy['ech-opts'])) {
+ parsedProxy.tls.ech = parsedProxy.tls.ech || {};
+ parsedProxy.tls.ech.enabled = proxy['ech-opts'].enable;
+ const echOptsConfig = proxy['ech-opts'].config;
+ if (Array.isArray(echOptsConfig) || typeof echOptsConfig === 'string') {
+ const config = normalizePemLines(echOptsConfig, 'ECH CONFIGS');
+ if (config) parsedProxy.tls.ech.config = config;
+ }
+ parsedProxy.tls.ech.query_server_name =
+ proxy['ech-opts']['query-server-name'];
+ parsedProxy.tls.ech.config_path = proxy['ech-opts']['config-path'];
+ parsedProxy.tls.ech.fragment = proxy['ech-opts']['fragment'];
+ parsedProxy.tls.ech.fragment_fallback_delay =
+ proxy['ech-opts']['fragment-fallback-delay'];
+ parsedProxy.tls.ech.record_fragment =
+ proxy['ech-opts']['record-fragment'];
+ }
+ if (proxy._curve_preferences && Array.isArray(proxy._curve_preferences)) {
+ parsedProxy.tls.curve_preferences = proxy._curve_preferences;
+ }
+ if (proxy['_fragment']) parsedProxy.tls.fragment = !!proxy['_fragment'];
+ if (proxy['_fragment_fallback_delay'])
+ parsedProxy.tls.fragment_fallback_delay =
+ proxy['_fragment_fallback_delay'];
+ if (proxy['_record_fragment'])
+ parsedProxy.tls.record_fragment = !!proxy['_record_fragment'];
+ if (proxy['_certificate'])
+ parsedProxy.tls.certificate = proxy['_certificate'];
+ if (proxy['_certificate_path'])
+ parsedProxy.tls.certificate_path = proxy['_certificate_path'];
+ if (proxy['_certificate_public_key_sha256'])
+ parsedProxy.tls.certificate_public_key_sha256 =
+ proxy['_certificate_public_key_sha256'];
+ if (proxy['_client_certificate'])
+ parsedProxy.tls.client_certificate = proxy['_client_certificate'];
+ if (proxy['_client_certificate_path'])
+ parsedProxy.tls.client_certificate_path =
+ proxy['_client_certificate_path'];
+ if (proxy['_client_key']) parsedProxy.tls.client_key = proxy['_client_key'];
+ if (proxy['_client_key_path'])
+ parsedProxy.tls.client_key_path = proxy['_client_key_path'];
+ if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
+};
+
+const sshParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'ssh',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.username) parsedProxy.user = proxy.username;
+ if (proxy.password) parsedProxy.password = proxy.password;
+ // https://wiki.metacubex.one/config/proxies/ssh
+ // https://sing-box.sagernet.org/zh/configuration/outbound/ssh
+ if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
+ if (proxy['private-key'])
+ parsedProxy.private_key_path = proxy['private-key'];
+ if (proxy['private-key-passphrase'])
+ parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
+ if (proxy['server-fingerprint']) {
+ parsedProxy.host_key = [proxy['server-fingerprint']];
+ // https://manual.nssurge.com/policy/ssh.html
+ // Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)
+ // TODO: ?
+ parsedProxy.host_key_algorithms = [
+ proxy['server-fingerprint'].split(' ')[0],
+ ];
+ }
+ if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
+ if (proxy['host-key-algorithms'])
+ parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+
+const httpParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'http',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ tls: { enabled: false, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.username) parsedProxy.username = proxy.username;
+ if (proxy.password) parsedProxy.password = proxy.password;
+ if (proxy.headers) {
+ parsedProxy.headers = {};
+ for (const k of Object.keys(proxy.headers)) {
+ parsedProxy.headers[k] = `${proxy.headers[k]}`;
+ }
+ if (Object.keys(parsedProxy.headers).length === 0)
+ delete parsedProxy.headers;
+ }
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+
+const socks5Parser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'socks',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ version: '5',
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.username) parsedProxy.username = proxy.username;
+ if (proxy.password) parsedProxy.password = proxy.password;
+ if (proxy.uot) parsedProxy.udp_over_tcp = true;
+ if (proxy['udp-over-tcp']) {
+ parsedProxy.udp_over_tcp = {
+ enabled: true,
+ version:
+ !proxy['udp-over-tcp-version'] ||
+ proxy['udp-over-tcp-version'] === 1
+ ? 1
+ : 2,
+ };
+ }
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+
+const shadowTLSParser = (proxy = {}) => {
+ const pluginOpts = getShadowTLSPluginOpts(proxy);
+ const ssPart = {
+ tag: proxy.name,
+ type: 'shadowsocks',
+ method: proxy.cipher,
+ password: proxy.password,
+ detour: getShadowTLSTag(proxy),
+ };
+ if (proxy.uot) ssPart.udp_over_tcp = true;
+ if (proxy['udp-over-tcp']) {
+ ssPart.udp_over_tcp = {
+ enabled: true,
+ version:
+ !proxy['udp-over-tcp-version'] ||
+ proxy['udp-over-tcp-version'] === 1
+ ? 1
+ : 2,
+ };
+ }
+ networkParser(proxy, ssPart);
+ smuxParser(proxy.smux, ssPart);
+ return {
+ type: 'ss-with-st',
+ ssPart,
+ stPart: shadowTLSOutboundParser(proxy, pluginOpts),
+ };
+};
+
+const getShadowTLSTag = (proxy = {}) => `${proxy.name}_shadowtls`;
+
+const getShadowTLSPluginOpts = (proxy = {}) => {
+ if (proxy.plugin === 'shadow-tls' && proxy['plugin-opts']) {
+ return proxy['plugin-opts'];
+ }
+ if (
+ proxy['shadow-tls-password'] != null ||
+ proxy['shadow-tls-sni'] != null ||
+ proxy['shadow-tls-version'] != null
+ ) {
+ return {
+ host: proxy['shadow-tls-sni'],
+ password: proxy['shadow-tls-password'],
+ version: proxy['shadow-tls-version'],
+ };
+ }
+ return undefined;
+};
+
+const normalizeALPN = (alpn) => {
+ if (typeof alpn === 'string') {
+ return alpn
+ .split(',')
+ .map((item) => item.trim())
+ .filter((item) => item !== '');
+ }
+ if (Array.isArray(alpn)) return alpn;
+ return undefined;
+};
+
+const shadowTLSOutboundParser = (proxy = {}, pluginOpts) => {
+ if (!pluginOpts) throw new Error('shadow-tls plugin options are missing');
+
+ const stPart = {
+ tag: getShadowTLSTag(proxy),
+ type: 'shadowtls',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ version: pluginOpts.version,
+ password: pluginOpts.password,
+ tls: {
+ enabled: true,
+ server_name: pluginOpts.host,
+ utls: {
+ enabled: true,
+ fingerprint: proxy['client-fingerprint'],
+ },
+ },
+ };
+ if (stPart.server_port < 0 || stPart.server_port > 65535)
+ throw '端口值非法';
+ const alpn = normalizeALPN(pluginOpts.alpn);
+ if (alpn) stPart.tls.alpn = alpn;
+ if (proxy['fast-open'] === true) stPart.udp_fragment = true;
+ tfoParser(proxy, stPart);
+ detourParser(proxy, stPart);
+ ipVersionParser(proxy, stPart);
+ domainResolverParser(proxy, stPart);
+ return stPart;
+};
+
+const ssParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'shadowsocks',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ method: proxy.cipher,
+ password: proxy.password,
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.uot) parsedProxy.udp_over_tcp = true;
+ if (proxy['udp-over-tcp']) {
+ parsedProxy.udp_over_tcp = {
+ enabled: true,
+ version:
+ !proxy['udp-over-tcp-version'] ||
+ proxy['udp-over-tcp-version'] === 1
+ ? 1
+ : 2,
+ };
+ }
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ if (proxy.plugin) {
+ const optArr = [];
+ if (proxy.plugin === 'obfs') {
+ parsedProxy.plugin = 'obfs-local';
+ parsedProxy.plugin_opts = '';
+ if (proxy['obfs-host'])
+ proxy['plugin-opts'].host = proxy['obfs-host'];
+ Object.keys(proxy['plugin-opts']).forEach((k) => {
+ switch (k) {
+ case 'mode':
+ optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
+ break;
+ case 'host':
+ optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
+ break;
+ default:
+ optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
+ break;
+ }
+ });
+ }
+ if (proxy.plugin === 'v2ray-plugin') {
+ parsedProxy.plugin = 'v2ray-plugin';
+ if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
+ if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
+ Object.keys(proxy['plugin-opts']).forEach((k) => {
+ switch (k) {
+ case 'tls':
+ if (proxy['plugin-opts'].tls) optArr.push('tls');
+ break;
+ case 'host':
+ optArr.push(`host=${proxy['plugin-opts'].host}`);
+ break;
+ case 'path':
+ optArr.push(`path=${proxy['plugin-opts'].path}`);
+ break;
+ case 'headers':
+ optArr.push(
+ `headers=${JSON.stringify(
+ proxy['plugin-opts'].headers,
+ )}`,
+ );
+ break;
+ case 'mux': {
+ const mux = normalizePluginMuxValue(
+ proxy['plugin-opts'].mux,
+ );
+ if (mux) parsedProxy.multiplex = { enabled: true };
+ optArr.push(`mux=${mux}`);
+ break;
+ }
+ default:
+ optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
+ }
+ });
+ }
+ parsedProxy.plugin_opts = optArr.join(';');
+ }
+
+ return parsedProxy;
+};
+// eslint-disable-next-line no-unused-vars
+const ssrParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'shadowsocksr',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ method: proxy.cipher,
+ password: proxy.password,
+ obfs: proxy.obfs,
+ protocol: proxy.protocol,
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
+ if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
+ parsedProxy.protocol_param = proxy['protocol-param'];
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+
+const getSnellVersion = (version) => {
+ if (version == null) return undefined;
+ const normalized = `${version}`.trim();
+ if (!/^\d+$/.test(normalized)) return NaN;
+ return parseInt(normalized, 10);
+};
+
+const snellParser = (proxy = {}) => {
+ const version = getSnellVersion(proxy.version);
+ const shadowTLSPluginOpts = getShadowTLSPluginOpts(proxy);
+ if (
+ version != null &&
+ (![1, 2, 3, 4, 5].includes(version) || Number.isNaN(version))
+ ) {
+ throw new Error(
+ `Platform sing-box does not support snell version ${proxy.version}`,
+ );
+ }
+
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'snell',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ psk: proxy.psk,
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (version != null) parsedProxy.version = version;
+ if (proxy['obfs-opts']?.mode)
+ parsedProxy.obfs_mode = proxy['obfs-opts'].mode;
+ if (proxy['obfs-opts']?.host)
+ parsedProxy.obfs_host = proxy['obfs-opts'].host;
+ if (proxy.reuse && (version == null || version >= 4))
+ parsedProxy.reuse = true;
+ networkParser(proxy, parsedProxy);
+ if (shadowTLSPluginOpts) {
+ parsedProxy.detour = getShadowTLSTag(proxy);
+ delete parsedProxy.server;
+ delete parsedProxy.server_port;
+ } else {
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ }
+ return parsedProxy;
+};
+
+const vmessParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'vmess',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ uuid: proxy.uuid,
+ security: proxy.cipher,
+ alter_id: parseInt(`${proxy.alterId}`, 10),
+ tls: { enabled: false, server_name: proxy.server, insecure: false },
+ };
+ if (
+ [
+ 'auto',
+ 'none',
+ 'zero',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ 'aes-128-ctr',
+ ].indexOf(parsedProxy.security) === -1
+ )
+ parsedProxy.security = 'auto';
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
+ if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
+ if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
+ if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+
+const vlessParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'vless',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ uuid: proxy.uuid,
+ tls: { enabled: false, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ // if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;
+ if (proxy.flow != null) parsedProxy.flow = proxy.flow;
+ if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
+ if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
+ if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
+ if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const trojanParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'trojan',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ password: proxy.password,
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
+ if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const naiveParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'naive',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy.username) parsedProxy.username = proxy.username;
+ if (proxy.password) parsedProxy.password = proxy.password;
+ if (proxy.uot) parsedProxy.udp_over_tcp = true;
+ if (proxy['udp-over-tcp']) {
+ parsedProxy.udp_over_tcp = {
+ enabled: true,
+ version:
+ !proxy['udp-over-tcp-version'] ||
+ proxy['udp-over-tcp-version'] === 1
+ ? 1
+ : 2,
+ };
+ }
+ const insecure_concurrency = parseInt(
+ `${proxy['insecure-concurrency']}`,
+ 10,
+ );
+ if (Number.isInteger(insecure_concurrency) && insecure_concurrency >= 0)
+ parsedProxy.insecure_concurrency = insecure_concurrency;
+ if (proxy['extra-headers'])
+ parsedProxy.extra_headers = proxy['extra-headers'];
+ if (proxy.quic) parsedProxy.quic = !!proxy.quic;
+ if (proxy['quic-congestion-control'])
+ parsedProxy.quic_congestion_control = proxy['quic-congestion-control'];
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ if (parsedProxy.tls?.insecure) {
+ $.info(
+ `Platform sing-box: insecure is not supported on naive outbound`,
+ );
+ delete parsedProxy.tls.insecure;
+ }
+
+ return parsedProxy;
+};
+const hysteriaParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'hysteria',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ disable_mtu_discovery: false,
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['hop-interval'])
+ parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
+ ? `${proxy['hop-interval']}s`
+ : proxy['hop-interval'];
+ if (proxy['ports'])
+ parsedProxy.server_ports = proxy['ports'].split(/\s*,\s*/).map((p) => {
+ const range = p.replace(/\s*-\s*/g, ':');
+ return range.includes(':') ? range : `${range}:${range}`;
+ });
+ if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
+ if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ // eslint-disable-next-line no-control-regex
+ const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
+ // sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps
+ if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {
+ parsedProxy.up = `${proxy.up}`;
+ } else {
+ parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
+ }
+ if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {
+ parsedProxy.down = `${proxy.down}`;
+ } else {
+ parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
+ }
+ if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
+ if (proxy.recv_window_conn)
+ parsedProxy.recv_window_conn = proxy.recv_window_conn;
+ if (proxy['recv-window-conn'])
+ parsedProxy.recv_window_conn = proxy['recv-window-conn'];
+ if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
+ if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
+ if (proxy.disable_mtu_discovery) {
+ if (typeof proxy.disable_mtu_discovery === 'boolean') {
+ parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
+ } else {
+ if (proxy.disable_mtu_discovery === 1)
+ parsedProxy.disable_mtu_discovery = true;
+ }
+ }
+ networkParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const hysteria2Parser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'hysteria2',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ password: proxy.password,
+ obfs: {},
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['hop-interval'])
+ parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
+ ? `${proxy['hop-interval']}s`
+ : proxy['hop-interval'];
+ if (proxy['ports'])
+ parsedProxy.server_ports = proxy['ports'].split(/\s*,\s*/).map((p) => {
+ const range = p.replace(/\s*-\s*/g, ':');
+ return range.includes(':') ? range : `${range}:${range}`;
+ });
+ if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
+ if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
+ if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
+ if (proxy['obfs-password'])
+ parsedProxy.obfs.password = proxy['obfs-password'];
+ if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
+ networkParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const tuic5Parser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'tuic',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ uuid: proxy.uuid,
+ password: proxy.password,
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ if (
+ proxy['congestion-controller'] &&
+ proxy['congestion-controller'] !== 'cubic'
+ )
+ parsedProxy.congestion_control = proxy['congestion-controller'];
+ if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
+ parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
+ if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
+ if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
+ if (proxy['heartbeat-interval'])
+ parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
+ networkParser(proxy, parsedProxy);
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const anytlsParser = (proxy = {}) => {
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'anytls',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ password: proxy.password,
+ tls: { enabled: true, server_name: proxy.server, insecure: false },
+ };
+ if (/^\d+$/.test(proxy['idle-session-check-interval']))
+ parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
+ if (/^\d+$/.test(proxy['idle-session-timeout']))
+ parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
+ if (/^\d+$/.test(proxy['min-idle-session']))
+ parsedProxy.min_idle_session = parseInt(
+ `${proxy['min-idle-session']}`,
+ 10,
+ );
+ detourParser(proxy, parsedProxy);
+ tlsParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ return parsedProxy;
+};
+const tailscaleParser = (proxy = {}) => {
+ const useControlHTTPClient = hasControlHTTPClient(proxy);
+ const parsedProxy = {
+ tag: proxy.name,
+ type: 'tailscale',
+ control_http_client: proxy['control-http-client'],
+ udp_timeout: proxy['udp-timeout'],
+ state_directory: proxy['state-dir'] || proxy['state-directory'],
+ auth_key: proxy['auth-key'],
+ control_url: proxy['control-url'],
+ ephemeral: proxy.ephemeral,
+ hostname: proxy.hostname,
+ accept_routes: proxy['accept-routes'],
+ exit_node: proxy['exit-node'],
+ exit_node_allow_lan_access: proxy['exit-node-allow-lan-access'],
+ advertise_routes: Array.isArray(proxy['advertise-routes'])
+ ? proxy['advertise-routes']
+ : undefined,
+ advertise_exit_node: proxy['advertise-exit-node'],
+ advertise_tags: Array.isArray(proxy['advertise-tags'])
+ ? proxy['advertise-tags']
+ : undefined,
+ relay_server_static_endpoints: Array.isArray(
+ proxy['relay-server-static-endpoints'],
+ )
+ ? proxy['relay-server-static-endpoints']
+ : undefined,
+ system_interface: proxy['system-interface'],
+ system_interface_name: proxy['system-interface-name'],
+ };
+ if (/^\d+$/.test(proxy['system-interface-mtu']))
+ parsedProxy.system_interface_mtu = parseInt(
+ `${proxy['system-interface-mtu']}`,
+ 10,
+ );
+ if (/^\d+$/.test(proxy['relay-server-port']))
+ parsedProxy.relay_server_port = parseInt(
+ `${proxy['relay-server-port']}`,
+ 10,
+ );
+ if (!useControlHTTPClient) {
+ detourParser(proxy, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ }
+ return parsedProxy;
+};
+
+const wireguardParser = (proxy = {}) => {
+ const address = ['ipv4', 'ipv6']
+ .map((family) => getWireGuardAddressWithCIDR(proxy, family))
+ .filter((i) => i);
+ const parsedProxy = {
+ system: !!proxy.system,
+ mtu: proxy.mtu ? parseInt(`${proxy.mtu}`, 10) : undefined,
+ udp_timeout: proxy['udp-timeout'],
+ workers: proxy['workers']
+ ? parseInt(`${proxy['workers']}`, 10)
+ : undefined,
+ tag: proxy.name,
+ type: 'wireguard',
+ server: proxy.server,
+ server_port: parseInt(`${proxy.port}`, 10),
+ address,
+ private_key: proxy['private-key'],
+ peer_public_key: proxy['public-key'],
+ pre_shared_key: proxy['pre-shared-key'],
+ reserved: [],
+ };
+ if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
+ throw 'invalid port';
+ if (proxy['fast-open']) parsedProxy.udp_fragment = true;
+ if (typeof proxy.reserved === 'string') {
+ parsedProxy.reserved = proxy.reserved;
+ } else if (Array.isArray(proxy.reserved)) {
+ for (const r of proxy.reserved) parsedProxy.reserved.push(r);
+ } else {
+ delete parsedProxy.reserved;
+ }
+ if (!Array.isArray(proxy.peers) || proxy.peers.length === 0) {
+ proxy.peers = [{}];
+ }
+ if (proxy.peers && proxy.peers.length > 0) {
+ parsedProxy.peers = [];
+ for (const p of proxy.peers) {
+ let address;
+ let port;
+ if (p.server && p.port) {
+ address = p.server;
+ port = parseInt(`${p.port}`, 10);
+ } else {
+ address = parsedProxy.server;
+ port = parseInt(`${parsedProxy.server_port}`, 10);
+ }
+ const peer = {
+ address,
+ port,
+ persistent_keepalive_interval: p[
+ 'persistent-keepalive-interval'
+ ]
+ ? parseInt(`${p['persistent-keepalive-interval']}`, 10)
+ : undefined,
+ public_key:
+ p['public-key'] ||
+ p['public_key'] ||
+ parsedProxy.peer_public_key,
+ pre_shared_key:
+ p['pre-shared-key'] ||
+ p['pre_shared_key'] ||
+ parsedProxy.pre_shared_key,
+ allowed_ips: p['allowed-ips'] ||
+ p.allowed_ips || [
+ '0.0.0.0/0',
+ ...(proxy.ipv6 ? ['::/0'] : []),
+ ],
+ reserved: [],
+ };
+ if (typeof p.reserved === 'string') {
+ peer.reserved.push(p.reserved);
+ } else if (Array.isArray(p.reserved)) {
+ for (const r of p.reserved) peer.reserved.push(r);
+ } else {
+ delete peer.reserved;
+ }
+ if (!Array.isArray(peer.reserved) || peer.reserved.length === 0) {
+ peer.reserved = parsedProxy.reserved;
+ }
+ // if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
+ parsedProxy.peers.push(peer);
+ }
+ }
+ tfoParser(proxy, parsedProxy);
+ detourParser(proxy, parsedProxy);
+ smuxParser(proxy.smux, parsedProxy);
+ ipVersionParser(proxy, parsedProxy);
+ domainResolverParser(proxy, parsedProxy);
+ delete parsedProxy.server;
+ delete parsedProxy.server_port;
+ delete parsedProxy.pre_shared_key;
+ delete parsedProxy.peer_public_key;
+ delete parsedProxy.reserved;
+ return parsedProxy;
+};
+
+export default function singbox_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ const list = [];
+ ClashMeta_Producer()
+ .produce(proxies, 'internal', { 'include-unsupported-proxy': true })
+ .map((proxy) => {
+ try {
+ if (['xhttp'].includes(proxy.network))
+ throw new Error(
+ `Platform sing-box does not support network: ${proxy.network}`,
+ );
+ switch (proxy.type) {
+ case 'ssh':
+ list.push(sshParser(proxy));
+ break;
+ case 'http':
+ list.push(httpParser(proxy));
+ break;
+ case 'socks5':
+ if (proxy.tls) {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type} with tls`,
+ );
+ } else {
+ list.push(socks5Parser(proxy));
+ }
+ break;
+ case 'ss':
+ // if (!proxy.cipher) {
+ // proxy.cipher = 'none';
+ // }
+ // if (
+ // ![
+ // '2022-blake3-aes-128-gcm',
+ // '2022-blake3-aes-256-gcm',
+ // '2022-blake3-chacha20-poly1305',
+ // 'aes-128-cfb',
+ // 'aes-128-ctr',
+ // 'aes-128-gcm',
+ // 'aes-192-cfb',
+ // 'aes-192-ctr',
+ // 'aes-192-gcm',
+ // 'aes-256-cfb',
+ // 'aes-256-ctr',
+ // 'aes-256-gcm',
+ // 'chacha20-ietf',
+ // 'chacha20-ietf-poly1305',
+ // 'none',
+ // 'rc4-md5',
+ // 'xchacha20',
+ // 'xchacha20-ietf-poly1305',
+ // ].includes(proxy.cipher)
+ // ) {
+ // throw new Error(
+ // `cipher ${proxy.cipher} is not supported`,
+ // );
+ // }
+ if (proxy.plugin === 'shadow-tls') {
+ const { ssPart, stPart } =
+ shadowTLSParser(proxy);
+ list.push(ssPart);
+ list.push(stPart);
+ } else {
+ list.push(ssParser(proxy));
+ }
+ break;
+ case 'ssr':
+ if (opts['include-unsupported-proxy']) {
+ list.push(ssrParser(proxy));
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type}`,
+ );
+ }
+ break;
+ case 'snell':
+ if (opts['include-unsupported-proxy']) {
+ list.push(snellParser(proxy));
+ const shadowTLSPluginOpts =
+ getShadowTLSPluginOpts(proxy);
+ if (shadowTLSPluginOpts) {
+ list.push(
+ shadowTLSOutboundParser(
+ proxy,
+ shadowTLSPluginOpts,
+ ),
+ );
+ }
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type}`,
+ );
+ }
+ break;
+ case 'vmess':
+ if (
+ !proxy.network ||
+ ['tcp', 'ws', 'grpc', 'h2', 'http'].includes(
+ proxy.network,
+ )
+ ) {
+ list.push(vmessParser(proxy));
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
+ );
+ }
+ break;
+ case 'vless':
+ if (
+ proxy.encryption &&
+ proxy.encryption !== 'none'
+ ) {
+ throw new Error(
+ `VLESS encryption is not supported`,
+ );
+ }
+ if (
+ !proxy.flow ||
+ ['xtls-rprx-vision'].includes(proxy.flow)
+ ) {
+ list.push(vlessParser(proxy));
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
+ );
+ }
+ break;
+ case 'trojan':
+ if (!proxy.flow) {
+ list.push(trojanParser(proxy));
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
+ );
+ }
+ break;
+ case 'naive':
+ list.push(naiveParser(proxy));
+ break;
+ case 'hysteria':
+ list.push(hysteriaParser(proxy));
+ break;
+ case 'hysteria2':
+ list.push(
+ hysteria2Parser(
+ proxy,
+ opts['include-unsupported-proxy'],
+ ),
+ );
+ break;
+ case 'tuic':
+ if (!proxy.token || proxy.token.length === 0) {
+ list.push(tuic5Parser(proxy));
+ } else {
+ throw new Error(
+ `Platform sing-box does not support proxy type: TUIC v4`,
+ );
+ }
+ break;
+ case 'wireguard':
+ list.push(wireguardParser(proxy));
+ break;
+ case 'anytls':
+ list.push(anytlsParser(proxy));
+ break;
+ case 'tailscale':
+ list.push(tailscaleParser(proxy));
+ break;
+ default:
+ throw new Error(
+ `Platform sing-box does not support proxy type: ${proxy.type}`,
+ );
+ }
+ } catch (e) {
+ // console.log(e);
+ $.error(e.message ?? e);
+ }
+ });
+
+ if (type === 'internal') return list;
+
+ const categorized = list.reduce(
+ (result, item) => {
+ if (['wireguard', 'tailscale'].includes(item.type)) {
+ result.endpoints.push(item);
+ } else {
+ result.outbounds.push(item);
+ }
+ return result;
+ },
+ { outbounds: [], endpoints: [] },
+ );
+
+ return JSON.stringify(categorized, null, 2);
+ };
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/producers/stash.js b/backend/src/core/proxy-utils/producers/stash.js
new file mode 100644
index 0000000000..086d6ba34b
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/stash.js
@@ -0,0 +1,358 @@
+import {
+ isPresent,
+ produceProxyListOutput,
+ supportsShadowsocksV2rayPluginMode,
+} from '@/core/proxy-utils/producers/utils';
+import {
+ deleteHttpUpgradeEarlyDataMetadata,
+ normalizeWebSocketEarlyDataPath,
+} from '../transport-path';
+import $ from '@/core/app';
+
+export default function Stash_Producer() {
+ const type = 'ALL';
+ const produce = (proxies, type, opts = {}) => {
+ // https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
+ const list = proxies
+ .filter((proxy) => {
+ if (opts['include-unsupported-proxy']) return true;
+ if (
+ ![
+ 'ss',
+ 'ssr',
+ 'vmess',
+ 'socks5',
+ 'http',
+ 'snell',
+ 'trojan',
+ 'tuic',
+ 'vless',
+ 'wireguard',
+ 'hysteria',
+ 'hysteria2',
+ 'ssh',
+ 'juicity',
+ 'anytls',
+ 'tailscale',
+ ].includes(proxy.type) ||
+ (proxy.type === 'ss' &&
+ ![
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'rc4-md5',
+ 'chacha20-ietf',
+ 'xchacha20',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher)) ||
+ (proxy.type === 'snell' && proxy.version >= 4)
+ ) {
+ return false;
+ } else if (
+ !supportsShadowsocksV2rayPluginMode(proxy, ['websocket'])
+ ) {
+ return false;
+ } else if (
+ ['vless'].includes(proxy.type) &&
+ proxy['reality-opts'] &&
+ proxy.network &&
+ !['tcp'].includes(proxy.network)
+ ) {
+ return false;
+ } else if (
+ ['anytls'].includes(proxy.type) &&
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) &&
+ proxy['reality-opts']))
+ ) {
+ return false;
+ } else if (['xhttp'].includes(proxy.network)) {
+ return false;
+ } else if (
+ proxy.encryption &&
+ proxy.encryption !== 'none' &&
+ ['vless'].includes(proxy.type)
+ ) {
+ return false;
+ } else if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ return false;
+ }
+ return true;
+ })
+ .map((proxy) => {
+ if (proxy.type === 'vmess') {
+ // handle vmess aead
+ if (isPresent(proxy, 'aead')) {
+ if (proxy.aead) {
+ proxy.alterId = 0;
+ }
+ delete proxy.aead;
+ }
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
+ // https://stash.wiki/proxy-protocols/proxy-types#vmess
+ if (
+ isPresent(proxy, 'cipher') &&
+ ![
+ 'auto',
+ 'aes-128-gcm',
+ 'chacha20-poly1305',
+ 'none',
+ ].includes(proxy.cipher)
+ ) {
+ proxy.cipher = 'auto';
+ }
+ } else if (proxy.type === 'tuic') {
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ } else {
+ proxy.alpn = ['h3'];
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ delete proxy.tfo;
+ }
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
+ if (
+ (!proxy.token || proxy.token.length === 0) &&
+ !isPresent(proxy, 'version')
+ ) {
+ proxy.version = 5;
+ }
+ } else if (proxy.type === 'hysteria') {
+ // auth_str 将会在未来某个时候删除 但是有的机场不规范
+ if (
+ isPresent(proxy, 'auth_str') &&
+ !isPresent(proxy, 'auth-str')
+ ) {
+ proxy['auth-str'] = proxy['auth_str'];
+ }
+ if (isPresent(proxy, 'alpn')) {
+ proxy.alpn = Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : [proxy.alpn];
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ delete proxy.tfo;
+ }
+ if (
+ isPresent(proxy, 'down') &&
+ !isPresent(proxy, 'down-speed')
+ ) {
+ proxy['down-speed'] = proxy.down;
+ delete proxy.down;
+ }
+ if (
+ isPresent(proxy, 'up') &&
+ !isPresent(proxy, 'up-speed')
+ ) {
+ proxy['up-speed'] = proxy.up;
+ delete proxy.up;
+ }
+ if (isPresent(proxy, 'down-speed')) {
+ proxy['down-speed'] =
+ `${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
+ }
+ if (isPresent(proxy, 'up-speed')) {
+ proxy['up-speed'] =
+ `${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
+ }
+ } else if (proxy.type === 'hysteria2') {
+ if (
+ isPresent(proxy, 'password') &&
+ !isPresent(proxy, 'auth')
+ ) {
+ proxy.auth = proxy.password;
+ delete proxy.password;
+ }
+ if (
+ isPresent(proxy, 'tfo') &&
+ !isPresent(proxy, 'fast-open')
+ ) {
+ proxy['fast-open'] = proxy.tfo;
+ delete proxy.tfo;
+ }
+ if (
+ isPresent(proxy, 'down') &&
+ !isPresent(proxy, 'down-speed')
+ ) {
+ proxy['down-speed'] = proxy.down;
+ delete proxy.down;
+ }
+ if (
+ isPresent(proxy, 'up') &&
+ !isPresent(proxy, 'up-speed')
+ ) {
+ proxy['up-speed'] = proxy.up;
+ delete proxy.up;
+ }
+ if (isPresent(proxy, 'down-speed')) {
+ proxy['down-speed'] =
+ `${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
+ }
+ if (isPresent(proxy, 'up-speed')) {
+ proxy['up-speed'] =
+ `${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
+ }
+ } else if (proxy.type === 'wireguard') {
+ proxy.keepalive =
+ proxy.keepalive ?? proxy['persistent-keepalive'];
+ proxy['persistent-keepalive'] = proxy.keepalive;
+ proxy['preshared-key'] =
+ proxy['preshared-key'] ?? proxy['pre-shared-key'];
+ proxy['pre-shared-key'] = proxy['preshared-key'];
+ } else if (proxy.type === 'snell' && proxy.version < 3) {
+ delete proxy.udp;
+ } else if (proxy.type === 'vless') {
+ if (isPresent(proxy, 'sni')) {
+ proxy.servername = proxy.sni;
+ delete proxy.sni;
+ }
+ }
+
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'http'
+ ) {
+ let httpPath = proxy['http-opts']?.path;
+ if (
+ isPresent(proxy, 'http-opts.path') &&
+ !Array.isArray(httpPath)
+ ) {
+ proxy['http-opts'].path = [httpPath];
+ }
+ let httpHost = proxy['http-opts']?.headers?.Host;
+ if (
+ isPresent(proxy, 'http-opts.headers.Host') &&
+ !Array.isArray(httpHost)
+ ) {
+ proxy['http-opts'].headers.Host = [httpHost];
+ }
+ }
+ if (
+ ['vmess', 'vless'].includes(proxy.type) &&
+ proxy.network === 'h2'
+ ) {
+ let path = proxy['h2-opts']?.path;
+ if (
+ isPresent(proxy, 'h2-opts.path') &&
+ Array.isArray(path)
+ ) {
+ proxy['h2-opts'].path = path[0];
+ }
+ let host = proxy['h2-opts']?.headers?.host;
+ if (
+ isPresent(proxy, 'h2-opts.headers.Host') &&
+ !Array.isArray(host)
+ ) {
+ proxy['h2-opts'].headers.host = [host];
+ }
+ }
+ if (['ws'].includes(proxy.network)) {
+ const networkOptsKey = `${proxy.network}-opts`;
+ proxy[networkOptsKey] = proxy[networkOptsKey] || {};
+ if (!proxy[networkOptsKey].path) {
+ proxy[networkOptsKey].path = '/';
+ }
+ normalizeWebSocketEarlyDataPath(proxy[networkOptsKey]);
+ }
+
+ if (proxy['plugin-opts']?.tls) {
+ if (isPresent(proxy, 'skip-cert-verify')) {
+ proxy['plugin-opts']['skip-cert-verify'] =
+ proxy['plugin-opts']['skip-cert-verify'] ||
+ proxy['skip-cert-verify'];
+ }
+ }
+ if (
+ [
+ 'trojan',
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'anytls',
+ 'trusttunnel',
+ 'naive',
+ ].includes(proxy.type)
+ ) {
+ delete proxy.tls;
+ }
+ if (proxy['tls-fingerprint']) {
+ proxy['server-cert-fingerprint'] = proxy['tls-fingerprint'];
+ }
+ delete proxy['tls-fingerprint'];
+
+ if (proxy['underlying-proxy']) {
+ proxy['dialer-proxy'] = proxy['underlying-proxy'];
+ }
+ delete proxy['underlying-proxy'];
+
+ if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
+ delete proxy.tls;
+ }
+
+ if (proxy['test-url']) {
+ proxy['benchmark-url'] = proxy['test-url'];
+ delete proxy['test-url'];
+ }
+ if (proxy['test-timeout']) {
+ proxy['benchmark-timeout'] = proxy['test-timeout'];
+ delete proxy['test-timeout'];
+ }
+
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+ delete proxy['ip-cidr'];
+ delete proxy['ipv6-cidr'];
+ if (type !== 'internal') {
+ for (const key in proxy) {
+ if (proxy[key] == null || /^_/i.test(key)) {
+ delete proxy[key];
+ }
+ }
+ deleteHttpUpgradeEarlyDataMetadata(
+ proxy[`${proxy.network}-opts`],
+ );
+ }
+ if (
+ ['grpc'].includes(proxy.network) &&
+ proxy[`${proxy.network}-opts`]
+ ) {
+ delete proxy[`${proxy.network}-opts`]['_grpc-type'];
+ delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
+ }
+ return proxy;
+ });
+ return produceProxyListOutput(list, type, opts);
+ };
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/producers/surfboard.js b/backend/src/core/proxy-utils/producers/surfboard.js
new file mode 100644
index 0000000000..831b12e8b8
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/surfboard.js
@@ -0,0 +1,354 @@
+import { Result, isPresent } from './utils';
+import { isNotBlank } from '@/utils';
+// import $ from '@/core/app';
+
+const targetPlatform = 'Surfboard';
+
+function hasNonBlankValue(value) {
+ return value != null && `${value}`.trim().length > 0;
+}
+
+export default function Surfboard_Producer() {
+ const produce = (proxy) => {
+ if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,
+ );
+ }
+ proxy.name = proxy.name.replace(/=|,/g, '');
+ switch (proxy.type) {
+ case 'ss':
+ return shadowsocks(proxy);
+ case 'trojan':
+ return trojan(proxy);
+ case 'vmess':
+ return vmess(proxy);
+ case 'http':
+ return http(proxy);
+ case 'snell':
+ return snell(proxy);
+ case 'socks5':
+ return socks5(proxy);
+ case 'hysteria2':
+ return hysteria2(proxy);
+ case 'wireguard-surge':
+ return wireguard(proxy);
+ }
+ if (proxy.type === 'anytls') {
+ if (
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) && proxy['reality-opts']))
+ ) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support proxy type ${proxy.type} with network or REALITY`,
+ );
+ }
+
+ return anytls(proxy);
+ }
+ throw new Error(
+ `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
+ );
+ };
+ return { produce };
+}
+function hysteria2(proxy) {
+ if (proxy.obfs || proxy['obfs-password']) {
+ throw new Error(`Surfboard Hysteria2 does not support obfs`);
+ }
+
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
+
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ if (hasNonBlankValue(proxy.ports)) {
+ result.append(
+ `,port-hopping="${String(proxy.ports).replace(/,/g, ';')}"`,
+ );
+ }
+
+ if (hasNonBlankValue(proxy['hop-interval'])) {
+ result.append(`,port-hopping-interval=${proxy['hop-interval']}`);
+ }
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // download-bandwidth
+ result.appendIfPresent(
+ `,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
+ 'down',
+ );
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+function anytls(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // reuse
+ result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
+
+ return result.toString();
+}
+function snell(proxy) {
+ if (proxy.version > 3) {
+ throw new Error(
+ `Platform ${targetPlatform} does not support snell version ${proxy.version}`,
+ );
+ }
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,version=${proxy.version}`, 'version');
+ result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
+
+ // obfs
+ result.appendIfPresent(
+ `,obfs=${proxy['obfs-opts']?.mode}`,
+ 'obfs-opts.mode',
+ );
+ result.appendIfPresent(
+ `,obfs-host=${proxy['obfs-opts']?.host}`,
+ 'obfs-opts.host',
+ );
+ result.appendIfPresent(
+ `,obfs-uri=${proxy['obfs-opts']?.path}`,
+ 'obfs-opts.path',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ if (proxy.version >= 3) {
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+ }
+
+ return result.toString();
+}
+function shadowsocks(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ if (
+ ![
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ 'rc4',
+ 'rc4-md5',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'bf-cfb',
+ 'camellia-128-cfb',
+ 'camellia-192-cfb',
+ 'camellia-256-cfb',
+ 'salsa20',
+ 'chacha20',
+ 'chacha20-ietf',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher)
+ ) {
+ throw new Error(`cipher ${proxy.cipher} is not supported`);
+ }
+ result.append(`,encrypt-method=${proxy.cipher}`);
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ // obfs
+ if (isPresent(proxy, 'plugin')) {
+ if (proxy.plugin === 'obfs') {
+ result.append(`,obfs=${proxy['plugin-opts'].mode}`);
+ result.appendIfPresent(
+ `,obfs-host=${proxy['plugin-opts'].host}`,
+ 'plugin-opts.host',
+ );
+ result.appendIfPresent(
+ `,obfs-uri=${proxy['plugin-opts'].path}`,
+ 'plugin-opts.path',
+ );
+ } else {
+ throw new Error(`plugin ${proxy.plugin} is not supported`);
+ }
+ }
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+
+function trojan(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,password=${proxy.password}`, 'password');
+
+ // transport
+ handleTransport(result, proxy);
+
+ // tls
+ result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+
+function vmess(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
+
+ // transport
+ handleTransport(result, proxy);
+
+ // AEAD
+ if (isPresent(proxy, 'aead')) {
+ result.append(`,vmess-aead=${proxy.aead}`);
+ } else {
+ result.append(`,vmess-aead=${proxy.alterId === 0}`);
+ }
+
+ // tls
+ result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+
+function http(proxy) {
+ const result = new Result(proxy);
+ const type = proxy.tls ? 'https' : 'http';
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,${proxy.username}`, 'username');
+ result.appendIfPresent(`,${proxy.password}`, 'password');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+
+function socks5(proxy) {
+ const result = new Result(proxy);
+ const type = proxy.tls ? 'socks5-tls' : 'socks5';
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,${proxy.username}`, 'username');
+ result.appendIfPresent(`,${proxy.password}`, 'password');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ return result.toString();
+}
+
+function wireguard(proxy) {
+ const result = new Result(proxy);
+
+ result.append(`${proxy.name}=wireguard`);
+
+ result.appendIfPresent(
+ `,section-name=${proxy['section-name']}`,
+ 'section-name',
+ );
+
+ return result.toString();
+}
+
+function handleTransport(result, proxy) {
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ result.append(`,ws=true`);
+ if (isPresent(proxy, 'ws-opts')) {
+ result.appendIfPresent(
+ `,ws-path=${proxy['ws-opts'].path}`,
+ 'ws-opts.path',
+ );
+ if (isPresent(proxy, 'ws-opts.headers')) {
+ const headers = proxy['ws-opts'].headers;
+ const value = Object.keys(headers)
+ .map((k) => {
+ let v = headers[k];
+ if (['Host'].includes(k)) {
+ v = `"${v}"`;
+ }
+ return `${k}:${v}`;
+ })
+ .join('|');
+ if (isNotBlank(value)) {
+ result.append(`,ws-headers=${value}`);
+ }
+ }
+ }
+ } else if (['tcp'].includes(proxy.network) && proxy['reality-opts']) {
+ throw new Error(`reality is unsupported`);
+ } else if (!['tcp'].includes(proxy.network)) {
+ throw new Error(`network ${proxy.network} is unsupported`);
+ }
+ }
+}
diff --git a/backend/src/core/proxy-utils/producers/surge.js b/backend/src/core/proxy-utils/producers/surge.js
new file mode 100644
index 0000000000..3c966dd6e5
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/surge.js
@@ -0,0 +1,1490 @@
+import { Result, isPresent } from './utils';
+import { isNotBlank, getIfNotBlank } from '@/utils';
+import $ from '@/core/app';
+
+const targetPlatform = 'Surge';
+
+export class SurgeUnsupportedProxyError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = 'SurgeUnsupportedProxyError';
+ }
+}
+
+function unsupported(message) {
+ return new SurgeUnsupportedProxyError(message);
+}
+
+const ipVersions = {
+ dual: 'dual',
+ ipv4: 'v4-only',
+ ipv6: 'v6-only',
+ 'ipv4-prefer': 'prefer-v4',
+ 'ipv6-prefer': 'prefer-v6',
+};
+
+function stripSurgeQuotes(value) {
+ if (typeof value !== 'string') return value;
+
+ const trimmed = value.trim();
+ const quote = trimmed[0];
+ if (
+ (quote === '"' || quote === "'") &&
+ trimmed[trimmed.length - 1] === quote
+ ) {
+ return trimmed.slice(1, -1).replace(/\\(["'\\])/g, '$1');
+ }
+
+ return trimmed.replace(/\\(["'\\])/g, '$1');
+}
+
+function quoteSurgeValue(value) {
+ return `"${String(stripSurgeQuotes(value))
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')}"`;
+}
+
+function hasNonBlankValue(value) {
+ return value != null && `${value}`.trim().length > 0;
+}
+
+function appendClientCert(result, proxy) {
+ const clientCert = isPresent(proxy, 'keystore-client-cert')
+ ? proxy['keystore-client-cert']
+ : proxy['client-cert'];
+ if (
+ isPresent(proxy, 'keystore-client-cert') ||
+ isPresent(proxy, 'client-cert')
+ ) {
+ result.append(`,client-cert=${quoteSurgeValue(clientCert)}`);
+ }
+}
+
+function appendSshPrivateKey(result, proxy) {
+ const privateKey = isPresent(proxy, 'keystore-private-key')
+ ? proxy['keystore-private-key']
+ : proxy['private-key'];
+ if (
+ isPresent(proxy, 'keystore-private-key') ||
+ isPresent(proxy, 'private-key')
+ ) {
+ result.append(`,private-key=${quoteSurgeValue(privateKey)}`);
+ }
+}
+
+function warnMaxStreamsIfNeeded(proxy) {
+ if (!isPresent(proxy, 'max-streams')) return;
+
+ const maxStreams = Number(stripSurgeQuotes(proxy['max-streams']));
+ if (!Number.isInteger(maxStreams) || maxStreams <= 3) return;
+
+ $.warn(
+ `Surge ${proxy.type} proxy ${proxy.name}: max-streams=${maxStreams} is greater than 3. Too many streams sharing one TCP connection may hurt performance.`,
+ );
+}
+
+export default function Surge_Producer() {
+ const produce = (proxy, type, opts = {}) => {
+ if (
+ ['ws'].includes(proxy.network) &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ throw unsupported(
+ `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`,
+ );
+ }
+ proxy.name = proxy.name.replace(/=|,/g, '');
+ if (proxy.ports) {
+ proxy.ports = String(proxy.ports);
+ }
+ switch (proxy.type) {
+ case 'ss':
+ return shadowsocks(proxy);
+ case 'trojan':
+ return trojan(proxy);
+ case 'vmess':
+ return vmess(proxy, opts['include-unsupported-proxy']);
+ case 'http':
+ return http(proxy);
+ case 'h2-connect':
+ return h2Connect(proxy);
+ case 'direct':
+ return direct(proxy);
+ case 'socks5':
+ return socks5(proxy);
+ case 'snell':
+ return snell(proxy);
+ case 'tuic':
+ return tuic(proxy);
+ case 'wireguard-surge':
+ return wireguard_surge(proxy);
+ case 'hysteria2':
+ return hysteria2(proxy);
+ case 'ssh':
+ return ssh(proxy);
+ case 'trusttunnel':
+ return trusttunnel(proxy);
+ }
+
+ if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
+ return wireguard(proxy);
+ }
+ if (proxy.type === 'anytls') {
+ if (
+ proxy.network &&
+ (!['tcp'].includes(proxy.network) ||
+ (['tcp'].includes(proxy.network) && proxy['reality-opts']))
+ ) {
+ throw unsupported(
+ `Platform ${targetPlatform} does not support proxy type ${proxy.type} with network or REALITY`,
+ );
+ }
+
+ return anytls(proxy);
+ }
+ throw unsupported(
+ `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
+ );
+ };
+ return { produce };
+}
+
+function shadowsocks(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ if (!proxy.cipher) {
+ proxy.cipher = 'none';
+ }
+ if (
+ ![
+ 'aes-128-gcm',
+ 'aes-192-gcm',
+ 'aes-256-gcm',
+ 'chacha20-ietf-poly1305',
+ 'xchacha20-ietf-poly1305',
+ 'rc4',
+ 'rc4-md5',
+ 'aes-128-cfb',
+ 'aes-192-cfb',
+ 'aes-256-cfb',
+ 'aes-128-ctr',
+ 'aes-192-ctr',
+ 'aes-256-ctr',
+ 'bf-cfb',
+ 'camellia-128-cfb',
+ 'camellia-192-cfb',
+ 'camellia-256-cfb',
+ 'cast5-cfb',
+ 'des-cfb',
+ 'idea-cfb',
+ 'rc2-cfb',
+ 'seed-cfb',
+ 'salsa20',
+ 'chacha20',
+ 'chacha20-ietf',
+ 'none',
+ '2022-blake3-aes-128-gcm',
+ '2022-blake3-aes-256-gcm',
+ ].includes(proxy.cipher)
+ ) {
+ throw unsupported(`cipher ${proxy.cipher} is not supported`);
+ }
+ result.append(`,encrypt-method=${proxy.cipher}`);
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // obfs
+ if (isPresent(proxy, 'plugin')) {
+ if (proxy.plugin === 'obfs') {
+ result.append(`,obfs=${proxy['plugin-opts'].mode}`);
+ result.appendIfPresent(
+ `,obfs-host=${proxy['plugin-opts'].host}`,
+ 'plugin-opts.host',
+ );
+ result.appendIfPresent(
+ `,obfs-uri=${proxy['plugin-opts'].path}`,
+ 'plugin-opts.path',
+ );
+ } else if (!['shadow-tls'].includes(proxy.plugin)) {
+ throw unsupported(`plugin ${proxy.plugin} is not supported`);
+ }
+ }
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ // udp-port
+ result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
+ } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
+ const password = proxy['plugin-opts'].password;
+ const host = proxy['plugin-opts'].host;
+ const version = proxy['plugin-opts'].version;
+ if (password) {
+ result.append(`,shadow-tls-password=${password}`);
+ if (host) {
+ result.append(`,shadow-tls-sni=${host}`);
+ }
+ if (version) {
+ if (version < 2) {
+ throw unsupported(
+ `shadow-tls version ${version} is not supported`,
+ );
+ }
+ result.append(`,shadow-tls-version=${version}`);
+ }
+ // udp-port
+ result.appendIfPresent(
+ `,udp-port=${proxy['udp-port']}`,
+ 'udp-port',
+ );
+ }
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function trojan(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // transport
+ handleTransport(result, proxy);
+
+ // tls
+ result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function anytls(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ // reuse
+ result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
+
+ return result.toString();
+}
+function trusttunnel(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=trust-tunnel,${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username="${proxy.username}"`, 'username');
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+ appendHeaders(result, proxy);
+ warnMaxStreamsIfNeeded(proxy);
+ result.appendIfPresent(
+ `,max-streams=${proxy['max-streams']}`,
+ 'max-streams',
+ );
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ // reuse
+ result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
+
+ return result.toString();
+}
+function h2Connect(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=h2-connect,${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username="${proxy.username}"`, 'username');
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+ appendHeaders(result, proxy);
+ warnMaxStreamsIfNeeded(proxy);
+ result.appendIfPresent(
+ `,max-streams=${proxy['max-streams']}`,
+ 'max-streams',
+ );
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ if (proxy.tfo) {
+ $.info(`Option tfo is not supported by Surge, thus omitted`);
+ }
+
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function vmess(proxy, includeUnsupportedProxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // transport
+ handleTransport(result, proxy, includeUnsupportedProxy);
+
+ // AEAD
+ if (isPresent(proxy, 'aead')) {
+ result.append(`,vmess-aead=${proxy.aead}`);
+ } else {
+ result.append(`,vmess-aead=${proxy.alterId === 0}`);
+ }
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls
+ result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function ssh(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username="${proxy.username}"`, 'username');
+ // 所有的类似的字段都有双引号的问题 暂不处理
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ // https://manual.nssurge.com/policy/ssh.html
+ // 需配合 Keystore
+ appendSshPrivateKey(result, proxy);
+ result.appendIfPresent(
+ `,idle-timeout=${proxy['idle-timeout']}`,
+ 'idle-timeout',
+ );
+ result.appendIfPresent(
+ `,server-fingerprint="${proxy['server-fingerprint']}"`,
+ 'server-fingerprint',
+ );
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+function http(proxy) {
+ const result = new Result(proxy);
+ const type = proxy.tls ? 'https' : 'http';
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username="${proxy.username}"`, 'username');
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+ appendHeaders(result, proxy);
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+function direct(proxy) {
+ const result = new Result(proxy);
+ const type = 'direct';
+ result.append(`${proxy.name}=${type}`);
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function socks5(proxy) {
+ const result = new Result(proxy);
+ const type = proxy.tls ? 'socks5-tls' : 'socks5';
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,username="${proxy.username}"`, 'username');
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tfo
+ if (proxy.tfo) {
+ $.info(`Option tfo is not supported by Surge, thus omitted`);
+ }
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function appendHeaders(result, proxy) {
+ const value = formatHeaders(proxy.headers);
+ if (isNotBlank(value)) {
+ result.append(`,headers="${value}"`);
+ }
+}
+
+function formatHeaders(headers) {
+ if (!headers || typeof headers !== 'object') {
+ return '';
+ }
+
+ return Object.entries(headers)
+ .filter(([key, value]) => isNotBlank(key) && value != null)
+ .map(([key, value]) => `${key}:"${escapeHeaderValue(value)}"`)
+ .join(';');
+}
+
+function escapeHeaderValue(value) {
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+}
+
+function snell(proxy) {
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
+ result.appendIfPresent(`,version=${proxy.version}`, 'version');
+ result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // obfs
+ result.appendIfPresent(
+ `,obfs=${proxy['obfs-opts']?.mode}`,
+ 'obfs-opts.mode',
+ );
+ result.appendIfPresent(
+ `,obfs-host=${proxy['obfs-opts']?.host}`,
+ 'obfs-opts.host',
+ );
+ result.appendIfPresent(
+ `,obfs-uri=${proxy['obfs-opts']?.path}`,
+ 'obfs-opts.path',
+ );
+
+ // tfo
+ result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ // reuse
+ result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
+
+ return result.toString();
+}
+
+function tuic(proxy) {
+ const result = new Result(proxy);
+ // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
+ let type = proxy.type;
+ if (!proxy.token || proxy.token.length === 0) {
+ type = 'tuic-v5';
+ }
+ result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
+
+ result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+ result.appendIfPresent(`,token=${proxy.token}`, 'token');
+
+ result.appendIfPresent(
+ `,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
+ 'alpn',
+ );
+
+ if (hasNonBlankValue(proxy.ports)) {
+ result.append(
+ `,port-hopping="${String(proxy.ports).replace(/,/g, ';')}"`,
+ );
+ }
+
+ if (hasNonBlankValue(proxy['hop-interval'])) {
+ result.append(`,port-hopping-interval=${proxy['hop-interval']}`);
+ }
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+
+ // tls fingerprint
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tfo
+ if (isPresent(proxy, 'tfo')) {
+ result.append(`,tfo=${proxy['tfo']}`);
+ } else if (isPresent(proxy, 'fast-open')) {
+ result.append(`,tfo=${proxy['fast-open']}`);
+ }
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
+
+ return result.toString();
+}
+
+function wireguard(proxy) {
+ if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
+ proxy.server = proxy.peers[0].server;
+ proxy.port = proxy.peers[0].port;
+ proxy.ip = proxy.peers[0].ip;
+ proxy.ipv6 = proxy.peers[0].ipv6;
+ proxy['public-key'] = proxy.peers[0]['public-key'];
+ proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
+ // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
+ proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
+ proxy.reserved = proxy.peers[0].reserved;
+ }
+ const result = new Result(proxy);
+
+ result.append(`# > WireGuard Proxy ${proxy.name}
+# ${proxy.name}=wireguard`);
+
+ proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
+
+ result.appendIfPresent(
+ `,section-name=${proxy['section-name']}`,
+ 'section-name',
+ );
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ result.append(`
+# > WireGuard Section ${proxy.name}
+[WireGuard ${proxy['section-name']}]
+private-key = ${proxy['private-key']}`);
+
+ result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip');
+ result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');
+ if (proxy.dns) {
+ if (Array.isArray(proxy.dns)) {
+ proxy.dns = proxy.dns.join(', ');
+ }
+ result.append(`\ndns-server = ${proxy.dns}`);
+ }
+ result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu');
+
+ if (ip_version === 'prefer-v6') {
+ result.append(`\nprefer-ipv6 = true`);
+ }
+ const allowedIps = Array.isArray(proxy['allowed-ips'])
+ ? proxy['allowed-ips'].join(',')
+ : proxy['allowed-ips'];
+ let reserved = Array.isArray(proxy.reserved)
+ ? proxy.reserved.join('/')
+ : proxy.reserved;
+ let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
+
+ const peer = {
+ 'public-key': proxy['public-key'],
+ 'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
+ endpoint: `${proxy.server}:${proxy.port}`,
+ keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
+ 'client-id': reserved,
+ 'preshared-key': presharedKey,
+ };
+ result.append(
+ `\npeer = (${Object.keys(peer)
+ .filter((k) => peer[k] != null)
+ .map((k) => `${k} = ${peer[k]}`)
+ .join(', ')})`,
+ );
+ return result.toString();
+}
+function wireguard_surge(proxy) {
+ const result = new Result(proxy);
+
+ result.append(`${proxy.name}=wireguard`);
+
+ result.appendIfPresent(
+ `,section-name=${proxy['section-name']}`,
+ 'section-name',
+ );
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ return result.toString();
+}
+
+function hysteria2(proxy) {
+ if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
+ throw unsupported(`only salamander obfs is supported`);
+ }
+
+ const result = new Result(proxy);
+ result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
+
+ result.appendIfPresent(`,password="${proxy.password}"`, 'password');
+
+ if (hasNonBlankValue(proxy.ports)) {
+ result.append(
+ `,port-hopping="${String(proxy.ports).replace(/,/g, ';')}"`,
+ );
+ }
+
+ if (hasNonBlankValue(proxy['hop-interval'])) {
+ result.append(`,port-hopping-interval=${proxy['hop-interval']}`);
+ }
+
+ if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
+ result.append(`,salamander-password="${proxy['obfs-password']}"`);
+ }
+
+ const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
+ result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // tls verification
+ result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
+ result.appendIfPresent(
+ `,skip-cert-verify=${proxy['skip-cert-verify']}`,
+ 'skip-cert-verify',
+ );
+ appendClientCert(result, proxy);
+ result.appendIfPresent(
+ `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
+ 'tls-fingerprint',
+ );
+
+ // tfo
+ if (isPresent(proxy, 'tfo')) {
+ result.append(`,tfo=${proxy['tfo']}`);
+ } else if (isPresent(proxy, 'fast-open')) {
+ result.append(`,tfo=${proxy['fast-open']}`);
+ }
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+ result.appendIfPresent(
+ `,test-timeout=${proxy['test-timeout']}`,
+ 'test-timeout',
+ );
+ result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
+ result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
+ result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
+ result.appendIfPresent(
+ `,allow-other-interface=${proxy['allow-other-interface']}`,
+ 'allow-other-interface',
+ );
+ result.appendIfPresent(
+ `,interface=${proxy['interface-name']}`,
+ 'interface-name',
+ );
+ result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
+
+ // shadow-tls
+ if (isPresent(proxy, 'shadow-tls-password')) {
+ result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
+
+ result.appendIfPresent(
+ `,shadow-tls-version=${proxy['shadow-tls-version']}`,
+ 'shadow-tls-version',
+ );
+ result.appendIfPresent(
+ `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
+ 'shadow-tls-sni',
+ );
+ }
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ // underlying-proxy
+ result.appendIfPresent(
+ `,underlying-proxy=${proxy['underlying-proxy']}`,
+ 'underlying-proxy',
+ );
+
+ // download-bandwidth
+ result.appendIfPresent(
+ `,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
+ 'down',
+ );
+
+ result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
+
+ return result.toString();
+}
+
+function handleTransport(result, proxy, includeUnsupportedProxy) {
+ if (isPresent(proxy, 'network')) {
+ if (proxy.network === 'ws') {
+ result.append(`,ws=true`);
+ if (isPresent(proxy, 'ws-opts')) {
+ result.appendIfPresent(
+ `,ws-path=${proxy['ws-opts'].path}`,
+ 'ws-opts.path',
+ );
+ if (isPresent(proxy, 'ws-opts.headers')) {
+ const headers = proxy['ws-opts'].headers;
+ const value = Object.keys(headers)
+ .map((k) => {
+ let v = headers[k];
+ // if (['Host'].includes(k)) {
+ v = `"${v}"`;
+ // }
+ return `${k}:${v}`;
+ })
+ .join('|');
+ if (isNotBlank(value)) {
+ result.append(`,ws-headers=${value}`);
+ }
+ }
+ }
+ } else {
+ if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
+ $.info(
+ `Include Unsupported Proxy: network ${proxy.network} -> tcp`,
+ );
+ } else if (
+ ['tcp'].includes(proxy.network) &&
+ proxy['reality-opts']
+ ) {
+ throw unsupported(`reality is unsupported`);
+ } else if (!['tcp'].includes(proxy.network)) {
+ throw unsupported(`network ${proxy.network} is unsupported`);
+ }
+ }
+ }
+}
diff --git a/backend/src/core/proxy-utils/producers/surgemac.js b/backend/src/core/proxy-utils/producers/surgemac.js
new file mode 100644
index 0000000000..6eb400e0dc
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/surgemac.js
@@ -0,0 +1,266 @@
+import { Base64 } from 'js-base64';
+import { Result, isPresent } from './utils';
+import Surge_Producer, { SurgeUnsupportedProxyError } from './surge';
+import ClashMeta_Producer from './clashmeta';
+import { isIPv4, isIPv6 } from '@/utils';
+import $ from '@/core/app';
+
+const targetPlatform = 'SurgeMac';
+
+const surge_Producer = Surge_Producer();
+
+export default function SurgeMac_Producer() {
+ const produce = (proxy, type, opts = {}) => {
+ switch (proxy.type) {
+ case 'external':
+ return external(proxy);
+ // case 'ssr':
+ // return shadowsocksr(proxy);
+ default: {
+ if (opts.mihomoExternal || proxy._mihomoExternal) {
+ return mihomo(proxy, type, opts) || '';
+ }
+ try {
+ return surge_Producer.produce(proxy, type, opts);
+ } catch (e) {
+ if (
+ opts.useMihomoExternal &&
+ e instanceof SurgeUnsupportedProxyError
+ ) {
+ const output = mihomo(proxy, type, opts) || '';
+ if (!output) {
+ throw e;
+ }
+ $.log(
+ `${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
+ );
+ return output;
+ }
+
+ if (e instanceof SurgeUnsupportedProxyError) {
+ throw new Error(
+ `${e.message}. Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,
+ );
+ }
+
+ throw e;
+ }
+ }
+ }
+ };
+ return { produce };
+}
+function external(proxy) {
+ const result = new Result(proxy);
+ if (!proxy.exec || !proxy['local-port']) {
+ throw new Error(`${proxy.type}: exec and local-port are required`);
+ }
+ result.append(
+ `${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`,
+ );
+
+ if (Array.isArray(proxy.args)) {
+ proxy.args.map((args) => {
+ result.append(`,args="${args}"`);
+ });
+ }
+ if (Array.isArray(proxy.addresses)) {
+ proxy.addresses.map((addresses) => {
+ result.append(`,addresses=${addresses}`);
+ });
+ }
+
+ result.appendIfPresent(
+ `,no-error-alert=${proxy['no-error-alert']}`,
+ 'no-error-alert',
+ );
+
+ // udp
+ result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
+
+ // tfo
+ if (isPresent(proxy, 'tfo')) {
+ result.append(`,tfo=${proxy['tfo']}`);
+ } else if (isPresent(proxy, 'fast-open')) {
+ result.append(`,tfo=${proxy['fast-open']}`);
+ }
+
+ // test-url
+ result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
+
+ // block-quic
+ result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
+
+ return result.toString();
+}
+// eslint-disable-next-line no-unused-vars
+function shadowsocksr(proxy) {
+ const external_proxy = {
+ ...proxy,
+ type: 'external',
+ exec: proxy.exec || '/usr/local/bin/ssr-local',
+ 'local-port': '__SubStoreLocalPort__',
+ args: [],
+ addresses: [],
+ 'local-address':
+ proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1',
+ };
+
+ // https://manual.nssurge.com/policy/external-proxy.html
+ if (isIP(proxy.server)) {
+ external_proxy.addresses.push(proxy.server);
+ } else {
+ $.log(
+ `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
+ );
+ }
+
+ for (const [key, value] of Object.entries({
+ cipher: '-m',
+ obfs: '-o',
+ 'obfs-param': '-g',
+ password: '-k',
+ port: '-p',
+ protocol: '-O',
+ 'protocol-param': '-G',
+ server: '-s',
+ 'local-port': '-l',
+ 'local-address': '-b',
+ })) {
+ if (external_proxy[key] != null) {
+ external_proxy.args.push(value);
+ external_proxy.args.push(external_proxy[key]);
+ }
+ }
+
+ return external(external_proxy);
+}
+// eslint-disable-next-line no-unused-vars
+function mihomo(proxy, type, opts) {
+ const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
+ if (clashProxy) {
+ let localPort = opts?.localPort || proxy._localPort || 65535;
+ const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
+ ? false
+ : true;
+ const dns = {
+ enable: true,
+ ipv6,
+ 'default-nameserver': opts?.defaultNameserver ||
+ proxy._defaultNameserver || [
+ '180.76.76.76',
+ '52.80.52.52',
+ '119.28.28.28',
+ '223.6.6.6',
+ ],
+ nameserver: opts?.nameserver ||
+ proxy._nameserver || [
+ 'https://doh.pub/dns-query',
+ 'https://dns.alidns.com/dns-query',
+ 'https://doh-pure.onedns.net/dns-query',
+ ],
+ };
+ const merge = opts?.merge || proxy._merge;
+ let result;
+ if (merge) {
+ const socks5 = {
+ name: proxy.name,
+ type: 'socks5',
+ server: '127.0.0.1',
+ port: localPort,
+ udp: true,
+ };
+ result = surge_Producer.produce(socks5, 'socks5', opts);
+
+ opts._merged = opts._merged || {
+ name: opts?.mergeName || proxy._mergeName || 'mihomo merged',
+ exec: opts?.exec || proxy._exec || '/usr/local/bin/mihomo',
+ config: {
+ // 最后输出的时候加
+ // 'mixed-port':,
+ ipv6,
+ mode: 'global',
+ dns,
+ proxies: [],
+ 'proxy-groups': [
+ {
+ name: 'GLOBAL',
+ type: 'fallback',
+ proxies: [],
+ },
+ ],
+ listeners: [],
+ },
+ };
+ const proxyName = `${localPort}`;
+ opts._merged.config.listeners.push({
+ name: `socks5-${localPort}`,
+ type: 'socks',
+ port: localPort,
+ listen: '127.0.0.1',
+ udp: true,
+ proxy: proxyName,
+ });
+ opts._merged.config['proxy-groups'][0].proxies.push(proxyName);
+ opts._merged.config.proxies.push({
+ ...clashProxy,
+ name: proxyName,
+ });
+ opts._merged.config = {
+ ...opts._merged.config,
+ ...(opts?.config || proxy._config || {}),
+ };
+ } else {
+ const external_proxy = {
+ name: proxy.name,
+ type: 'external',
+ udp: true,
+ exec: opts?.exec || proxy._exec || '/usr/local/bin/mihomo',
+ 'local-port': localPort,
+ args: [
+ '-config',
+ Base64.encode(
+ JSON.stringify({
+ 'mixed-port': localPort,
+ ipv6,
+ mode: 'global',
+ dns,
+ proxies: [
+ {
+ ...clashProxy,
+ name: 'proxy',
+ },
+ ],
+ 'proxy-groups': [
+ {
+ name: 'GLOBAL',
+ type: 'select',
+ proxies: ['proxy'],
+ },
+ ],
+ ...(opts?.config || proxy._config || {}),
+ }),
+ ),
+ ],
+ addresses: [],
+ };
+
+ // https://manual.nssurge.com/policy/external-proxy.html
+ if (isIP(proxy.server)) {
+ external_proxy.addresses.push(proxy.server);
+ } else {
+ $.warn(
+ `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
+ );
+ }
+
+ result = external(external_proxy);
+ }
+ opts.localPort = localPort - 1;
+ return result;
+ }
+}
+
+function isIP(ip) {
+ return isIPv4(ip) || isIPv6(ip);
+}
diff --git a/backend/src/core/proxy-utils/producers/uri.js b/backend/src/core/proxy-utils/producers/uri.js
new file mode 100644
index 0000000000..603425bd13
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/uri.js
@@ -0,0 +1,1533 @@
+/* eslint-disable no-case-declarations */
+import { Base64 } from 'js-base64';
+import $ from '@/core/app';
+import { isIPv6, isPlainObject } from '@/utils';
+import { getWireGuardAddressWithCIDR, normalizePluginMuxValue } from './utils';
+import {
+ normalizeXhttpIntegerValue,
+ normalizeXhttpNonNegativeRange,
+ normalizeXhttpPositiveRange,
+ normalizeXhttpScalarUpperBound,
+} from '../xhttp-utils';
+import {
+ extractPathQueryParam,
+ parseSafeIntegerValue,
+ setPathQueryParam,
+} from '../transport-path';
+import {
+ buildXrayEchConfigListFromMihomo,
+ buildXrayEchFieldsFromMihomo,
+} from '../ech-utils';
+
+function toStringHeaderMap(headers, { excludeHost = false } = {}) {
+ if (!isPlainObject(headers)) {
+ return undefined;
+ }
+
+ const parsedHeaders = {};
+ for (const [key, value] of Object.entries(headers)) {
+ if (typeof value !== 'string' || value === '') {
+ continue;
+ }
+ if (excludeHost && /^host$/i.test(key)) {
+ continue;
+ }
+ parsedHeaders[key] = value;
+ }
+
+ return Object.keys(parsedHeaders).length > 0 ? parsedHeaders : undefined;
+}
+
+function getHttpUpgradeEarlyData(transportOpts, path) {
+ const httpUpgradeEd = getSafeEarlyDataValue(
+ transportOpts?.['_v2ray-http-upgrade-ed'],
+ );
+ if (httpUpgradeEd !== '') return httpUpgradeEd;
+
+ const pathEd = getSafeEarlyDataValue(
+ extractPathQueryParam(path || '/', 'ed').value,
+ );
+ return pathEd !== '' ? pathEd : 2560;
+}
+
+function setHttpUpgradeEarlyDataPath(path, transportOpts) {
+ if (!transportOpts?.['v2ray-http-upgrade-fast-open']) {
+ return path;
+ }
+
+ return setPathQueryParam(
+ path || '/',
+ 'ed',
+ getHttpUpgradeEarlyData(transportOpts, path),
+ );
+}
+
+function setWebSocketEarlyDataPath(path, transportOpts) {
+ const earlyDataValue = transportOpts?.['max-early-data'];
+ const earlyData = getSafeEarlyDataValue(earlyDataValue);
+ if (earlyData === '') {
+ if (earlyDataValue != null && `${earlyDataValue}` !== '') {
+ return path == null ? path : extractPathQueryParam(path, 'ed').path;
+ }
+ return path;
+ }
+
+ const earlyDataHeaderName = transportOpts?.['early-data-header-name'];
+ if (
+ earlyDataHeaderName &&
+ earlyDataHeaderName !== 'Sec-WebSocket-Protocol'
+ ) {
+ return path == null ? path : extractPathQueryParam(path, 'ed').path;
+ }
+
+ return setPathQueryParam(path || '/', 'ed', earlyData);
+}
+
+function getSafeEarlyDataValue(value) {
+ if (value == null || `${value}` === '') return '';
+ return parseSafeIntegerValue(value) == null ? '' : `${value}`;
+}
+
+function parseIntegerLikeValue(value) {
+ return normalizeXhttpIntegerValue(value);
+}
+
+function getSerializableXhttpRangeValue(value) {
+ return normalizeXhttpNonNegativeRange(value);
+}
+
+function warnEchDefaultDns({
+ defaultDns,
+ dnsFieldPath,
+ echOptsPath,
+ proxyName,
+ queryServerName,
+}) {
+ const proxyLabel = proxyName || '未命名节点';
+ $.warn(
+ `URI ECH: 节点 "${proxyLabel}" 的 ${echOptsPath} 已开启且设置 query-server-name="${queryServerName}", 但未设置 ${dnsFieldPath}; 已使用默认 DNS ${defaultDns}. 如需自定义, 请设置 ${dnsFieldPath}.`,
+ );
+}
+
+function getTransportHost(network, transportOpts = {}) {
+ if (network === 'h2') {
+ return (
+ transportOpts.host ??
+ transportOpts.headers?.host ??
+ transportOpts.headers?.Host
+ );
+ }
+ if (network === 'xhttp') {
+ return (
+ transportOpts.host ??
+ transportOpts.headers?.Host ??
+ transportOpts.headers?.host
+ );
+ }
+ return (
+ transportOpts.headers?.Host ??
+ transportOpts.headers?.host ??
+ transportOpts.host
+ );
+}
+
+function mapReuseSettingsToXmux(reuseSettings) {
+ if (!isPlainObject(reuseSettings)) {
+ return undefined;
+ }
+
+ const xmux = {};
+ const reuseFieldMap = {
+ 'max-connections': 'maxConnections',
+ 'max-concurrency': 'maxConcurrency',
+ 'c-max-reuse-times': 'cMaxReuseTimes',
+ 'h-max-request-times': 'hMaxRequestTimes',
+ 'h-max-reusable-secs': 'hMaxReusableSecs',
+ };
+
+ for (const [sourceKey, targetKey] of Object.entries(reuseFieldMap)) {
+ const normalizedValue = normalizeXhttpNonNegativeRange(
+ reuseSettings[sourceKey],
+ );
+ if (normalizedValue != null) {
+ xmux[targetKey] =
+ typeof normalizedValue === 'number'
+ ? `${normalizedValue}`
+ : normalizedValue;
+ }
+ }
+
+ const hKeepAlivePeriod = parseIntegerLikeValue(
+ reuseSettings['h-keep-alive-period'],
+ );
+ if (hKeepAlivePeriod != null) {
+ xmux.hKeepAlivePeriod = hKeepAlivePeriod;
+ }
+
+ return Object.keys(xmux).length > 0 ? xmux : undefined;
+}
+
+function applyStructuredXhttpExtraFields(
+ target,
+ xhttpOpts,
+ { excludeHostHeader = true, xmuxTarget = 'root' } = {},
+) {
+ if (!isPlainObject(target) || !isPlainObject(xhttpOpts)) {
+ return;
+ }
+
+ const headers = toStringHeaderMap(xhttpOpts.headers, {
+ excludeHost: excludeHostHeader,
+ });
+ if (headers) {
+ target.headers = headers;
+ }
+
+ if (xhttpOpts['no-grpc-header'] === true) {
+ target.noGRPCHeader = true;
+ }
+ if (xhttpOpts['x-padding-bytes']) {
+ target.xPaddingBytes = xhttpOpts['x-padding-bytes'];
+ }
+ if (xhttpOpts['x-padding-obfs-mode'] === true) {
+ target.xPaddingObfsMode = true;
+ }
+ if (xhttpOpts['x-padding-key']) {
+ target.xPaddingKey = xhttpOpts['x-padding-key'];
+ }
+ if (xhttpOpts['x-padding-header']) {
+ target.xPaddingHeader = xhttpOpts['x-padding-header'];
+ }
+ if (xhttpOpts['x-padding-placement']) {
+ target.xPaddingPlacement = xhttpOpts['x-padding-placement'];
+ }
+ if (xhttpOpts['x-padding-method']) {
+ target.xPaddingMethod = xhttpOpts['x-padding-method'];
+ }
+ if (xhttpOpts['uplink-http-method']) {
+ target.uplinkHTTPMethod = xhttpOpts['uplink-http-method'];
+ }
+ if (xhttpOpts['session-placement']) {
+ target.sessionPlacement = xhttpOpts['session-placement'];
+ }
+ if (xhttpOpts['session-key']) {
+ target.sessionKey = xhttpOpts['session-key'];
+ }
+ if (xhttpOpts['seq-placement']) {
+ target.seqPlacement = xhttpOpts['seq-placement'];
+ }
+ if (xhttpOpts['seq-key']) {
+ target.seqKey = xhttpOpts['seq-key'];
+ }
+ if (xhttpOpts['uplink-data-placement']) {
+ target.uplinkDataPlacement = xhttpOpts['uplink-data-placement'];
+ }
+ if (xhttpOpts['uplink-data-key']) {
+ target.uplinkDataKey = xhttpOpts['uplink-data-key'];
+ }
+
+ const uplinkChunkSize = getSerializableXhttpRangeValue(
+ xhttpOpts['uplink-chunk-size'],
+ );
+ if (uplinkChunkSize != null) {
+ target.uplinkChunkSize = uplinkChunkSize;
+ }
+
+ if (xhttpOpts['sc-max-each-post-bytes'] != null) {
+ const scMaxEachPostBytes = normalizeXhttpScalarUpperBound(
+ xhttpOpts['sc-max-each-post-bytes'],
+ );
+ if (scMaxEachPostBytes != null) {
+ target.scMaxEachPostBytes = scMaxEachPostBytes;
+ }
+ }
+
+ if (xhttpOpts['sc-min-posts-interval-ms'] != null) {
+ const scMinPostsIntervalMs = normalizeXhttpPositiveRange(
+ xhttpOpts['sc-min-posts-interval-ms'],
+ );
+ if (scMinPostsIntervalMs != null) {
+ target.scMinPostsIntervalMs = scMinPostsIntervalMs;
+ }
+ }
+
+ const xmux = mapReuseSettingsToXmux(xhttpOpts['reuse-settings']);
+ if (xmux) {
+ if (xmuxTarget === 'extra') {
+ target.extra = {
+ ...(isPlainObject(target.extra) ? target.extra : {}),
+ xmux,
+ };
+ } else {
+ target.xmux = xmux;
+ }
+ }
+}
+
+function buildXhttpDownloadSettings(
+ downloadSettings,
+ outerXhttpOpts = {},
+ proxy = {},
+) {
+ if (!isPlainObject(downloadSettings)) {
+ return undefined;
+ }
+
+ const explicitNetwork =
+ typeof downloadSettings.network === 'string'
+ ? downloadSettings.network.toLowerCase()
+ : '';
+ const normalizedNetwork =
+ explicitNetwork === 'xhttp' || explicitNetwork === 'splithttp'
+ ? 'xhttp'
+ : undefined;
+
+ const result = {};
+ if (downloadSettings.server) {
+ result.address = downloadSettings.server;
+ }
+ const parsedPort = normalizeXhttpIntegerValue(downloadSettings.port, {
+ allowNegative: false,
+ });
+ if (parsedPort != null) {
+ result.port = parsedPort;
+ }
+
+ const realityOpts = isPlainObject(downloadSettings['reality-opts'])
+ ? downloadSettings['reality-opts']
+ : undefined;
+ if (realityOpts) {
+ result.security = 'reality';
+ } else if (downloadSettings.tls) {
+ result.security = 'tls';
+ }
+
+ const tlsSettings = {};
+ if (downloadSettings.servername) {
+ tlsSettings.serverName = downloadSettings.servername;
+ }
+ if (downloadSettings['client-fingerprint']) {
+ tlsSettings.fingerprint = downloadSettings['client-fingerprint'];
+ }
+ if (downloadSettings['skip-cert-verify']) {
+ tlsSettings.allowInsecure = true;
+ }
+ if (downloadSettings.alpn) {
+ tlsSettings.alpn = Array.isArray(downloadSettings.alpn)
+ ? downloadSettings.alpn
+ : [downloadSettings.alpn];
+ }
+ const echFields = buildXrayEchFieldsFromMihomo(
+ downloadSettings['ech-opts'],
+ undefined,
+ {
+ dnsFieldPath: 'xhttp-opts.download-settings.ech-opts._dns',
+ warnDefaultDns: (context) =>
+ warnEchDefaultDns({
+ ...context,
+ echOptsPath: 'xhttp-opts.download-settings.ech-opts',
+ proxyName: proxy.name,
+ }),
+ },
+ );
+ if (echFields.echConfigList) {
+ tlsSettings.echConfigList = echFields.echConfigList;
+ }
+ if (echFields.echForceQuery) {
+ tlsSettings.echForceQuery = echFields.echForceQuery;
+ }
+ if (echFields.echSockopt) {
+ tlsSettings.echSockopt = cloneXhttpExtraValue(echFields.echSockopt);
+ }
+ if (Object.keys(tlsSettings).length > 0) {
+ result.tlsSettings = tlsSettings;
+ }
+
+ if (realityOpts) {
+ const realitySettings = {};
+ if (downloadSettings.servername) {
+ realitySettings.serverName = downloadSettings.servername;
+ }
+ if (downloadSettings['client-fingerprint']) {
+ realitySettings.fingerprint =
+ downloadSettings['client-fingerprint'];
+ }
+ if (realityOpts['public-key']) {
+ realitySettings.publicKey = realityOpts['public-key'];
+ }
+ if (realityOpts['short-id']) {
+ realitySettings.shortId = realityOpts['short-id'];
+ }
+ if (Object.keys(realitySettings).length > 0) {
+ result.realitySettings = realitySettings;
+ }
+ }
+
+ const xhttpSettings = {};
+ // Mirror Mihomo's inheritance: path and host fall back to outer xhttp-opts
+ // when not explicitly set in download-settings. Mode is never a field in
+ // Mihomo's XHTTPDownloadSettings struct, so it always comes from outer.
+ const dsPath = downloadSettings.path ?? outerXhttpOpts.path;
+ if (dsPath) {
+ xhttpSettings.path = dsPath;
+ }
+ const downloadHost =
+ getTransportHost('xhttp', downloadSettings) ??
+ getTransportHost('xhttp', outerXhttpOpts);
+ if (downloadHost) {
+ xhttpSettings.host = downloadHost;
+ }
+ const mode = downloadSettings.mode ?? outerXhttpOpts.mode;
+ if (mode) {
+ xhttpSettings.mode = mode;
+ }
+ applyStructuredXhttpExtraFields(xhttpSettings, downloadSettings, {
+ excludeHostHeader: true,
+ xmuxTarget: 'extra',
+ });
+ if (Object.keys(xhttpSettings).length > 0) {
+ result.xhttpSettings = xhttpSettings;
+ }
+
+ if (Object.keys(result).length === 0 && normalizedNetwork == null) {
+ return undefined;
+ }
+
+ // Treat nested downloadSettings.network as a supported structured field.
+ // Fresh structured exports still default to xhttp so Xray sees a nested
+ // StreamConfig instead of falling back to tcp.
+ return {
+ ...(result.address != null ? { address: result.address } : {}),
+ network: normalizedNetwork || 'xhttp',
+ ...(result.port != null ? { port: result.port } : {}),
+ ...(result.security != null ? { security: result.security } : {}),
+ ...(result.tlsSettings != null
+ ? { tlsSettings: result.tlsSettings }
+ : {}),
+ ...(result.realitySettings != null
+ ? { realitySettings: result.realitySettings }
+ : {}),
+ ...(result.xhttpSettings != null
+ ? { xhttpSettings: result.xhttpSettings }
+ : {}),
+ };
+}
+
+function buildStructuredVlessExtraObject(proxy) {
+ const xhttpOpts = proxy['xhttp-opts'] || {};
+ const extra = {};
+ applyStructuredXhttpExtraFields(extra, xhttpOpts, {
+ excludeHostHeader: true,
+ xmuxTarget: 'root',
+ });
+
+ const downloadSettings = buildXhttpDownloadSettings(
+ xhttpOpts['download-settings'],
+ xhttpOpts,
+ proxy,
+ );
+ if (downloadSettings) {
+ extra.downloadSettings = downloadSettings;
+ }
+
+ return Object.keys(extra).length > 0 ? extra : undefined;
+}
+
+function cloneXhttpExtraValue(value) {
+ if (Array.isArray(value)) {
+ return value.map(cloneXhttpExtraValue);
+ }
+
+ if (isPlainObject(value)) {
+ const clonedValue = {};
+ for (const [key, entryValue] of Object.entries(value)) {
+ clonedValue[key] = cloneXhttpExtraValue(entryValue);
+ }
+ return clonedValue;
+ }
+
+ return value;
+}
+
+function mergeUnsupportedXhttpExtraValue(baseValue, unsupportedValue) {
+ if (baseValue == null) {
+ return cloneXhttpExtraValue(unsupportedValue);
+ }
+
+ if (Array.isArray(baseValue) || Array.isArray(unsupportedValue)) {
+ return cloneXhttpExtraValue(baseValue);
+ }
+
+ if (isPlainObject(baseValue) && isPlainObject(unsupportedValue)) {
+ return mergeUnsupportedXhttpExtraObject(baseValue, unsupportedValue);
+ }
+
+ return cloneXhttpExtraValue(baseValue);
+}
+
+function mergeUnsupportedXhttpExtraObject(baseObject, unsupportedObject) {
+ const mergedExtra = isPlainObject(baseObject)
+ ? cloneXhttpExtraValue(baseObject)
+ : {};
+ if (!isPlainObject(unsupportedObject)) {
+ return mergedExtra;
+ }
+
+ for (const [key, value] of Object.entries(unsupportedObject)) {
+ if (!Object.prototype.hasOwnProperty.call(mergedExtra, key)) {
+ mergedExtra[key] = cloneXhttpExtraValue(value);
+ continue;
+ }
+
+ mergedExtra[key] = mergeUnsupportedXhttpExtraValue(
+ mergedExtra[key],
+ value,
+ );
+ }
+
+ return mergedExtra;
+}
+
+function getExplicitExtraOverride(proxy) {
+ if (typeof proxy._extra === 'string') {
+ return proxy._extra;
+ }
+
+ // `_extra` only accepts JSON-like plain objects here. Broader object checks
+ // would accidentally stringify instances such as Date/Map/class values.
+ if (isPlainObject(proxy._extra)) {
+ return JSON.stringify(proxy._extra);
+ }
+
+ return undefined;
+}
+
+function buildVlessExtra(proxy) {
+ const explicitExtraOverride = getExplicitExtraOverride(proxy);
+ if (explicitExtraOverride != null) {
+ // `_extra` is an explicit user override for the final URI extra. When
+ // present as a string or plain object, we bypass the structured xhttp
+ // rebuild entirely so users can hand-author extra without needing to
+ // keep other structured fields in sync.
+ return explicitExtraOverride;
+ }
+
+ if (proxy.network !== 'xhttp') {
+ return proxy._extra || '';
+ }
+
+ const structuredExtra = buildStructuredVlessExtraObject(proxy);
+
+ // IMPORTANT: `_extra_unsupported` is only the sidecar for URI extra fields
+ // that Mihomo does not model structurally yet, and it only participates
+ // when the user did not explicitly set `_extra`. Without an explicit
+ // override, supported xhttp fields must still be emitted from the current
+ // structured Mihomo node so later edits are reflected on export, while
+ // `_extra_unsupported` fills the holes needed for VLESS URI -> node ->
+ // VLESS URI lossless round-trips. That also means supported-field format
+ // conflicts are resolved by the structured emitters here, e.g.
+ // sc-max-each-post-bytes still emits the compatibility upper bound while
+ // sc-min-posts-interval-ms keeps range.
+ const mergedExtra = mergeUnsupportedXhttpExtraObject(
+ structuredExtra,
+ proxy._extra_unsupported,
+ );
+
+ return Object.keys(mergedExtra).length > 0
+ ? JSON.stringify(mergedExtra)
+ : '';
+}
+
+function vless(proxy) {
+ let security = 'none';
+ const isReality = proxy['reality-opts'];
+ let sid = '';
+ let pbk = '';
+ let spx = '';
+ if (isReality) {
+ security = 'reality';
+ const publicKey = proxy['reality-opts']?.['public-key'];
+ if (publicKey) {
+ pbk = `&pbk=${encodeURIComponent(publicKey)}`;
+ }
+ const shortId = proxy['reality-opts']?.['short-id'];
+ if (shortId) {
+ sid = `&sid=${encodeURIComponent(shortId)}`;
+ }
+ const spiderX = proxy['reality-opts']?.['_spider-x'];
+ if (spiderX) {
+ spx = `&spx=${encodeURIComponent(spiderX)}`;
+ }
+ } else if (proxy.tls) {
+ security = 'tls';
+ }
+ let alpn = '';
+ if (proxy.alpn) {
+ alpn = `&alpn=${encodeURIComponent(
+ Array.isArray(proxy.alpn) ? proxy.alpn : proxy.alpn.join(','),
+ )}`;
+ }
+ let allowInsecure = '';
+ if (proxy['skip-cert-verify']) {
+ allowInsecure = `&allowInsecure=1`;
+ }
+ let h2 = '';
+ if (proxy._h2) {
+ h2 = `&h2=1`;
+ }
+ let pcs = '';
+ if (proxy['tls-fingerprint']) {
+ pcs = `&pcs=${encodeURIComponent(proxy['tls-fingerprint'])}`;
+ }
+ let ech = '';
+ const echConfigList = buildXrayEchConfigListFromMihomo(
+ proxy['ech-opts'],
+ proxy._echConfigList,
+ {
+ dnsFieldPath: 'ech-opts._dns',
+ warnDefaultDns: (context) =>
+ warnEchDefaultDns({
+ ...context,
+ echOptsPath: 'ech-opts',
+ proxyName: proxy.name,
+ }),
+ },
+ );
+ if (echConfigList) {
+ ech = `&ech=${encodeURIComponent(echConfigList)}`;
+ }
+ let sni = '';
+ if (proxy.sni) {
+ sni = `&sni=${encodeURIComponent(proxy.sni)}`;
+ }
+ let fp = '';
+ if (proxy['client-fingerprint']) {
+ fp = `&fp=${encodeURIComponent(proxy['client-fingerprint'])}`;
+ }
+ let flow = '';
+ if (proxy.flow) {
+ flow = `&flow=${encodeURIComponent(proxy.flow)}`;
+ }
+ let extra = '';
+ const extraPayload = buildVlessExtra(proxy);
+ if (extraPayload) {
+ extra = `&extra=${encodeURIComponent(extraPayload)}`;
+ }
+ let mode = '';
+ if (
+ ['xhttp'].includes(proxy.network) &&
+ proxy[`${proxy.network}-opts`]?.mode
+ ) {
+ mode = `&mode=${encodeURIComponent(
+ proxy[`${proxy.network}-opts`].mode,
+ )}`;
+ } else if (proxy._mode) {
+ mode = `&mode=${encodeURIComponent(proxy._mode)}`;
+ }
+ let pqv = '';
+ if (proxy._pqv) {
+ pqv = `&pqv=${encodeURIComponent(proxy._pqv)}`;
+ }
+ let encryption = '';
+ if (proxy.encryption) {
+ encryption = `&encryption=${encodeURIComponent(proxy.encryption)}`;
+ }
+ let vlessType = proxy.network;
+ if (proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade']) {
+ vlessType = 'httpupgrade';
+ } else if (proxy.network === 'http') {
+ vlessType = 'tcp';
+ } else if (proxy.network === 'h2') {
+ vlessType = 'http';
+ }
+
+ let vlessTransport = `&type=${encodeURIComponent(vlessType)}`;
+ if (proxy.network === 'http') {
+ vlessTransport += '&headerType=http';
+ }
+ if (['grpc'].includes(proxy.network)) {
+ // https://github.com/XTLS/Xray-core/issues/91
+ vlessTransport += `&mode=${encodeURIComponent(
+ proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
+ )}`;
+ const authority = proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
+ if (authority) {
+ vlessTransport += `&authority=${encodeURIComponent(authority)}`;
+ }
+ }
+
+ const transportOpts = proxy[`${proxy.network}-opts`] || {};
+ const isVlessHttpUpgrade =
+ proxy.network === 'ws' && transportOpts?.['v2ray-http-upgrade'];
+ let vlessTransportServiceName =
+ transportOpts?.[`${proxy.network}-service-name`];
+ let vlessTransportPath = transportOpts?.path;
+ let vlessTransportHost = getTransportHost(proxy.network, transportOpts);
+ const vlessWsEarlyData = getSafeEarlyDataValue(
+ proxy['ws-opts']?.['max-early-data'],
+ );
+ if (Array.isArray(vlessTransportPath)) {
+ vlessTransportPath = vlessTransportPath[0];
+ }
+ if (isVlessHttpUpgrade && transportOpts?.['v2ray-http-upgrade-fast-open']) {
+ vlessTransportPath = setHttpUpgradeEarlyDataPath(
+ vlessTransportPath,
+ transportOpts,
+ );
+ } else if (
+ proxy.network === 'ws' &&
+ proxy['ws-opts']?.['max-early-data'] != null &&
+ vlessTransportPath
+ ) {
+ vlessTransportPath = extractPathQueryParam(
+ vlessTransportPath,
+ 'ed',
+ ).path;
+ }
+ if (vlessTransportPath) {
+ vlessTransport += `&path=${encodeURIComponent(vlessTransportPath)}`;
+ }
+ if (vlessTransportHost) {
+ vlessTransport += `&host=${encodeURIComponent(
+ Array.isArray(vlessTransportHost)
+ ? vlessTransportHost[0]
+ : vlessTransportHost,
+ )}`;
+ }
+ if (vlessTransportServiceName) {
+ vlessTransport += `&serviceName=${encodeURIComponent(
+ vlessTransportServiceName,
+ )}`;
+ }
+ if (proxy.network === 'http' && proxy['http-opts']?.method) {
+ vlessTransport += `&method=${encodeURIComponent(
+ proxy['http-opts'].method,
+ )}`;
+ }
+ if (proxy.network === 'kcp') {
+ if (proxy.seed) {
+ vlessTransport += `&seed=${encodeURIComponent(proxy.seed)}`;
+ }
+ if (proxy.headerType) {
+ vlessTransport += `&headerType=${encodeURIComponent(
+ proxy.headerType,
+ )}`;
+ }
+ }
+ if (
+ proxy.network === 'ws' &&
+ !isVlessHttpUpgrade &&
+ vlessWsEarlyData !== ''
+ ) {
+ vlessTransport += `&ed=${encodeURIComponent(vlessWsEarlyData)}`;
+ }
+ const earlyDataHeaderName = proxy['ws-opts']?.['early-data-header-name'];
+ if (
+ earlyDataHeaderName &&
+ (isVlessHttpUpgrade ||
+ proxy['ws-opts']?.['max-early-data'] == null ||
+ earlyDataHeaderName !== 'Sec-WebSocket-Protocol')
+ ) {
+ vlessTransport += `&eh=${encodeURIComponent(earlyDataHeaderName)}`;
+ }
+
+ let packetEncoding = '';
+ if (proxy['packet-addr']) {
+ packetEncoding = '&packetEncoding=packet';
+ } else if (proxy.udp === true && !proxy.xudp) {
+ packetEncoding = '&packetEncoding=none';
+ }
+
+ return `vless://${proxy.uuid}@${proxy.server}:${
+ proxy.port
+ }?security=${encodeURIComponent(
+ security,
+ )}${vlessTransport}${packetEncoding}${alpn}${allowInsecure}${pcs}${ech}${h2}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}${pqv}${encryption}#${encodeURIComponent(
+ proxy.name,
+ )}`;
+}
+
+export default function URI_Producer() {
+ const type = 'SINGLE';
+ const produce = (proxy) => {
+ let result = '';
+ delete proxy.subName;
+ delete proxy.collectionName;
+ delete proxy.id;
+ delete proxy.resolved;
+ delete proxy['no-resolve'];
+ for (const key in proxy) {
+ if (proxy[key] == null) {
+ delete proxy[key];
+ }
+ }
+ if (
+ [
+ 'tuic',
+ 'hysteria',
+ 'hysteria2',
+ 'juicity',
+ 'trusttunnel',
+ ].includes(proxy.type)
+ ) {
+ delete proxy.tls;
+ }
+ if (
+ !['vmess'].includes(proxy.type) &&
+ proxy.server &&
+ isIPv6(proxy.server)
+ ) {
+ proxy.server = `[${proxy.server}]`;
+ }
+ switch (proxy.type) {
+ case 'socks5':
+ result = `socks://${encodeURIComponent(
+ Base64.encode(
+ `${proxy.username ?? ''}:${proxy.password ?? ''}`,
+ ),
+ )}@${proxy.server}:${proxy.port}#${proxy.name}`;
+ break;
+ case 'ss':
+ const userinfo = `${proxy.cipher}:${proxy.password}`;
+ result = `ss://${
+ proxy.cipher?.startsWith('2022-blake3-')
+ ? `${encodeURIComponent(
+ proxy.cipher,
+ )}:${encodeURIComponent(proxy.password)}`
+ : Base64.encode(userinfo)
+ }@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
+ let query = '';
+ if (proxy.plugin) {
+ query += '&plugin=';
+ const opts = proxy['plugin-opts'];
+ switch (proxy.plugin) {
+ case 'obfs':
+ query += encodeURIComponent(
+ `simple-obfs;obfs=${opts.mode}${
+ opts.host ? ';obfs-host=' + opts.host : ''
+ }`,
+ );
+ break;
+ case 'v2ray-plugin':
+ const mux = normalizePluginMuxValue(opts.mux);
+ // 为了兼容性 多输出 mode 和 host 两个字段
+ query += encodeURIComponent(
+ `v2ray-plugin;obfs=${opts.mode};mode=${
+ opts.mode
+ }${opts.host ? ';obfs-host=' + opts.host : ''}${
+ opts.host ? ';host=' + opts.host : ''
+ }${opts.path ? ';path=' + opts.path : ''}${
+ opts.tls ? ';tls' : ''
+ }${opts.sni ? ';sni=' + opts.sni : ''}${
+ opts['skip-cert-verify']
+ ? ';skip-cert-verify=' +
+ opts['skip-cert-verify']
+ : ''
+ }${mux != null ? ';mux=' + mux : ''}`,
+ );
+ break;
+ case 'shadow-tls':
+ query += encodeURIComponent(
+ `shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
+ );
+ break;
+ default:
+ throw new Error(
+ `Unsupported plugin option: ${proxy.plugin}`,
+ );
+ }
+ }
+ if (proxy['udp-over-tcp']) {
+ query += '&uot=1';
+ }
+ if (proxy.tfo) {
+ query += '&tfo=1';
+ }
+ let ssTransport = '';
+ if (proxy.network) {
+ let ssType = proxy.network;
+ if (
+ proxy.network === 'ws' &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ ssType = 'httpupgrade';
+ }
+ ssTransport = `&type=${encodeURIComponent(ssType)}`;
+ if (['grpc'].includes(proxy.network)) {
+ let ssTransportServiceName =
+ proxy[`${proxy.network}-opts`]?.[
+ `${proxy.network}-service-name`
+ ];
+ let ssTransportAuthority =
+ proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
+ if (ssTransportServiceName) {
+ ssTransport += `&serviceName=${encodeURIComponent(
+ ssTransportServiceName,
+ )}`;
+ }
+ if (ssTransportAuthority) {
+ ssTransport += `&authority=${encodeURIComponent(
+ ssTransportAuthority,
+ )}`;
+ }
+ ssTransport += `&mode=${encodeURIComponent(
+ proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
+ 'gun',
+ )}`;
+ }
+ const ssTransportOpts =
+ proxy[`${proxy.network}-opts`] || {};
+ const isSsHttpUpgrade =
+ proxy.network === 'ws' &&
+ ssTransportOpts?.['v2ray-http-upgrade'];
+ let ssTransportPath = ssTransportOpts?.path;
+ let ssTransportHost = ssTransportOpts?.headers?.Host;
+ if (Array.isArray(ssTransportPath)) {
+ ssTransportPath = ssTransportPath[0];
+ }
+ if (isSsHttpUpgrade) {
+ ssTransportPath = setHttpUpgradeEarlyDataPath(
+ ssTransportPath,
+ ssTransportOpts,
+ );
+ } else if (proxy.network === 'ws') {
+ ssTransportPath = setWebSocketEarlyDataPath(
+ ssTransportPath,
+ ssTransportOpts,
+ );
+ }
+ if (ssTransportPath) {
+ ssTransport += `&path=${encodeURIComponent(
+ ssTransportPath,
+ )}`;
+ }
+ if (ssTransportHost) {
+ ssTransport += `&host=${encodeURIComponent(
+ Array.isArray(ssTransportHost)
+ ? ssTransportHost[0]
+ : ssTransportHost,
+ )}`;
+ }
+ }
+ let ssFp = '';
+ if (proxy['client-fingerprint']) {
+ ssFp = `&fp=${encodeURIComponent(
+ proxy['client-fingerprint'],
+ )}`;
+ }
+ let ssAlpn = '';
+ if (proxy.alpn) {
+ ssAlpn = `&alpn=${encodeURIComponent(
+ Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : proxy.alpn.join(','),
+ )}`;
+ }
+ const ssIsReality = proxy['reality-opts'];
+ let ssSid = '';
+ let ssPbk = '';
+ let ssSpx = '';
+ let ssSecurity = proxy.tls ? '&security=tls' : '';
+ let ssMode = '';
+ let ssExtra = '';
+ if (ssIsReality) {
+ ssSecurity = `&security=reality`;
+ const publicKey = proxy['reality-opts']?.['public-key'];
+ if (publicKey) {
+ ssPbk = `&pbk=${encodeURIComponent(publicKey)}`;
+ }
+ const shortId = proxy['reality-opts']?.['short-id'];
+ if (shortId) {
+ ssSid = `&sid=${encodeURIComponent(shortId)}`;
+ }
+ const spiderX = proxy['reality-opts']?.['_spider-x'];
+ if (spiderX) {
+ ssSpx = `&spx=${encodeURIComponent(spiderX)}`;
+ }
+ if (proxy._extra) {
+ ssExtra = `&extra=${encodeURIComponent(proxy._extra)}`;
+ }
+ if (proxy._mode) {
+ ssMode = `&mode=${encodeURIComponent(proxy._mode)}`;
+ }
+ }
+ if (proxy.tls) {
+ query += `&sni=${encodeURIComponent(
+ proxy.sni || proxy.server,
+ )}${proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''}`;
+ }
+ query += `${ssTransport}${ssAlpn}${ssFp}${ssSecurity}${ssSid}${ssPbk}${ssSpx}${ssMode}${ssExtra}#${encodeURIComponent(
+ proxy.name,
+ )}`;
+ result += query.replace(/^&/, '?');
+ break;
+ case 'ssr':
+ result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${
+ proxy.cipher
+ }:${proxy.obfs}:${Base64.encode(proxy.password)}/`;
+ result += `?remarks=${Base64.encode(proxy.name)}${
+ proxy['obfs-param']
+ ? '&obfsparam=' + Base64.encode(proxy['obfs-param'])
+ : ''
+ }${
+ proxy['protocol-param']
+ ? '&protocolparam=' +
+ Base64.encode(proxy['protocol-param'])
+ : ''
+ }`;
+ result = 'ssr://' + Base64.encode(result);
+ break;
+ case 'vmess':
+ // V2RayN URI format
+ let type = '';
+ let net = proxy.network || 'tcp';
+ if (proxy.network === 'http') {
+ net = 'tcp';
+ type = 'http';
+ } else if (
+ proxy.network === 'ws' &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ net = 'httpupgrade';
+ }
+ result = {
+ v: '2',
+ ps: proxy.name,
+ add: proxy.server,
+ port: `${proxy.port}`,
+ id: proxy.uuid,
+ aid: `${proxy.alterId || 0}`,
+ scy: proxy.cipher,
+ net,
+ type,
+ tls: proxy.tls ? 'tls' : '',
+ alpn: Array.isArray(proxy.alpn)
+ ? proxy.alpn.join(',')
+ : proxy.alpn,
+ fp: proxy['client-fingerprint'],
+ };
+ if (proxy.tls && proxy.sni) {
+ result.sni = proxy.sni;
+ }
+ // obfs
+ if (proxy.network) {
+ const vmessTransportOpts =
+ proxy[`${proxy.network}-opts`] || {};
+ const isVmessHttpUpgrade =
+ proxy.network === 'ws' &&
+ vmessTransportOpts?.['v2ray-http-upgrade'];
+ let vmessTransportPath = vmessTransportOpts?.path;
+ let vmessTransportHost = vmessTransportOpts?.headers?.Host;
+
+ if (['grpc'].includes(proxy.network)) {
+ result.path =
+ proxy[`${proxy.network}-opts`]?.[
+ 'grpc-service-name'
+ ];
+ // https://github.com/XTLS/Xray-core/issues/91
+ result.type =
+ proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
+ 'gun';
+ result.host =
+ proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
+ } else if (['kcp', 'quic'].includes(proxy.network)) {
+ // https://github.com/XTLS/Xray-core/issues/91
+ result.type =
+ proxy[`${proxy.network}-opts`]?.[
+ `_${proxy.network}-type`
+ ] || 'none';
+ result.host =
+ proxy[`${proxy.network}-opts`]?.[
+ `_${proxy.network}-host`
+ ];
+ result.path =
+ proxy[`${proxy.network}-opts`]?.[
+ `_${proxy.network}-path`
+ ];
+ } else {
+ if (Array.isArray(vmessTransportPath)) {
+ vmessTransportPath = vmessTransportPath[0];
+ }
+ if (isVmessHttpUpgrade) {
+ vmessTransportPath = setHttpUpgradeEarlyDataPath(
+ vmessTransportPath,
+ vmessTransportOpts,
+ );
+ } else if (proxy.network === 'ws') {
+ vmessTransportPath = setWebSocketEarlyDataPath(
+ vmessTransportPath,
+ vmessTransportOpts,
+ );
+ }
+ if (vmessTransportPath) {
+ result.path = vmessTransportPath;
+ }
+ if (vmessTransportHost) {
+ result.host = Array.isArray(vmessTransportHost)
+ ? vmessTransportHost[0]
+ : vmessTransportHost;
+ }
+ }
+ }
+ result = 'vmess://' + Base64.encode(JSON.stringify(result));
+ break;
+ case 'vless':
+ result = vless(proxy);
+ break;
+ case 'trojan':
+ let trojanTransport = '';
+ if (proxy.network) {
+ let trojanType = proxy.network;
+ if (
+ proxy.network === 'ws' &&
+ proxy['ws-opts']?.['v2ray-http-upgrade']
+ ) {
+ trojanType = 'httpupgrade';
+ }
+ trojanTransport = `&type=${encodeURIComponent(trojanType)}`;
+ if (['grpc'].includes(proxy.network)) {
+ let trojanTransportServiceName =
+ proxy[`${proxy.network}-opts`]?.[
+ `${proxy.network}-service-name`
+ ];
+ let trojanTransportAuthority =
+ proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
+ if (trojanTransportServiceName) {
+ trojanTransport += `&serviceName=${encodeURIComponent(
+ trojanTransportServiceName,
+ )}`;
+ }
+ if (trojanTransportAuthority) {
+ trojanTransport += `&authority=${encodeURIComponent(
+ trojanTransportAuthority,
+ )}`;
+ }
+ trojanTransport += `&mode=${encodeURIComponent(
+ proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
+ 'gun',
+ )}`;
+ }
+ const trojanTransportOpts =
+ proxy[`${proxy.network}-opts`] || {};
+ const isTrojanHttpUpgrade =
+ proxy.network === 'ws' &&
+ trojanTransportOpts?.['v2ray-http-upgrade'];
+ let trojanTransportPath = trojanTransportOpts?.path;
+ let trojanTransportHost =
+ trojanTransportOpts?.headers?.Host;
+ if (Array.isArray(trojanTransportPath)) {
+ trojanTransportPath = trojanTransportPath[0];
+ }
+ if (isTrojanHttpUpgrade) {
+ trojanTransportPath = setHttpUpgradeEarlyDataPath(
+ trojanTransportPath,
+ trojanTransportOpts,
+ );
+ } else if (proxy.network === 'ws') {
+ trojanTransportPath = setWebSocketEarlyDataPath(
+ trojanTransportPath,
+ trojanTransportOpts,
+ );
+ }
+ if (trojanTransportPath) {
+ trojanTransport += `&path=${encodeURIComponent(
+ trojanTransportPath,
+ )}`;
+ }
+ if (trojanTransportHost) {
+ trojanTransport += `&host=${encodeURIComponent(
+ Array.isArray(trojanTransportHost)
+ ? trojanTransportHost[0]
+ : trojanTransportHost,
+ )}`;
+ }
+ }
+ let trojanFp = '';
+ if (proxy['client-fingerprint']) {
+ trojanFp = `&fp=${encodeURIComponent(
+ proxy['client-fingerprint'],
+ )}`;
+ }
+ let trojanPcs = '';
+ if (proxy['tls-fingerprint']) {
+ trojanPcs = `&pcs=${encodeURIComponent(
+ proxy['tls-fingerprint'],
+ )}`;
+ }
+ let trojanAlpn = '';
+ if (proxy.alpn) {
+ trojanAlpn = `&alpn=${encodeURIComponent(
+ Array.isArray(proxy.alpn)
+ ? proxy.alpn
+ : proxy.alpn.join(','),
+ )}`;
+ }
+ const trojanIsReality = proxy['reality-opts'];
+ let trojanSid = '';
+ let trojanPbk = '';
+ let trojanSpx = '';
+ let trojanSecurity = '';
+ let trojanMode = '';
+ let trojanExtra = '';
+ if (trojanIsReality) {
+ trojanSecurity = `&security=reality`;
+ const publicKey = proxy['reality-opts']?.['public-key'];
+ if (publicKey) {
+ trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
+ }
+ const shortId = proxy['reality-opts']?.['short-id'];
+ if (shortId) {
+ trojanSid = `&sid=${encodeURIComponent(shortId)}`;
+ }
+ const spiderX = proxy['reality-opts']?.['_spider-x'];
+ if (spiderX) {
+ trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
+ }
+ if (proxy._extra) {
+ trojanExtra = `&extra=${encodeURIComponent(
+ proxy._extra,
+ )}`;
+ }
+ if (proxy._mode) {
+ trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
+ }
+ }
+ result = `trojan://${proxy.password}@${proxy.server}:${
+ proxy.port
+ }?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
+ proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
+ }${trojanTransport}${trojanAlpn}${trojanFp}${trojanPcs}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
+ proxy.name,
+ )}`;
+ break;
+ case 'hysteria2':
+ let hysteria2params = [];
+ if (proxy['hop-interval']) {
+ hysteria2params.push(
+ `hop-interval=${proxy['hop-interval']}`,
+ );
+ }
+ if (proxy['keepalive']) {
+ hysteria2params.push(`keepalive=${proxy['keepalive']}`);
+ }
+ if (proxy['skip-cert-verify']) {
+ hysteria2params.push(`insecure=1`);
+ }
+ if (proxy.obfs) {
+ hysteria2params.push(
+ `obfs=${encodeURIComponent(proxy.obfs)}`,
+ );
+ if (proxy['obfs-password']) {
+ hysteria2params.push(
+ `obfs-password=${encodeURIComponent(
+ proxy['obfs-password'],
+ )}`,
+ );
+ }
+ }
+ if (proxy.sni) {
+ hysteria2params.push(
+ `sni=${encodeURIComponent(proxy.sni)}`,
+ );
+ }
+ if (proxy.ports) {
+ hysteria2params.push(`mport=${proxy.ports}`);
+ }
+ if (proxy['tls-fingerprint']) {
+ hysteria2params.push(
+ `pinSHA256=${encodeURIComponent(
+ proxy['tls-fingerprint'],
+ )}`,
+ );
+ }
+ if (proxy.tfo) {
+ hysteria2params.push(`fastopen=1`);
+ }
+ result = `hysteria2://${encodeURIComponent(proxy.password)}@${
+ proxy.server
+ }:${proxy.port}?${hysteria2params.join(
+ '&',
+ )}#${encodeURIComponent(proxy.name)}`;
+ break;
+ case 'hysteria':
+ let hysteriaParams = [];
+ Object.keys(proxy).forEach((key) => {
+ if (!['name', 'type', 'server', 'port'].includes(key)) {
+ const i = key.replace(/-/, '_');
+ if (['alpn'].includes(key)) {
+ if (proxy[key]) {
+ hysteriaParams.push(
+ `${i}=${encodeURIComponent(
+ Array.isArray(proxy[key])
+ ? proxy[key][0]
+ : proxy[key],
+ )}`,
+ );
+ }
+ } else if (['skip-cert-verify'].includes(key)) {
+ if (proxy[key]) {
+ hysteriaParams.push(`insecure=1`);
+ }
+ } else if (['tfo', 'fast-open'].includes(key)) {
+ if (
+ proxy[key] &&
+ !hysteriaParams.includes('fastopen=1')
+ ) {
+ hysteriaParams.push(`fastopen=1`);
+ }
+ } else if (['ports'].includes(key)) {
+ hysteriaParams.push(`mport=${proxy[key]}`);
+ } else if (['auth-str'].includes(key)) {
+ hysteriaParams.push(`auth=${proxy[key]}`);
+ } else if (['up'].includes(key)) {
+ hysteriaParams.push(`upmbps=${proxy[key]}`);
+ } else if (['down'].includes(key)) {
+ hysteriaParams.push(`downmbps=${proxy[key]}`);
+ } else if (['_obfs'].includes(key)) {
+ hysteriaParams.push(`obfs=${proxy[key]}`);
+ } else if (['obfs'].includes(key)) {
+ hysteriaParams.push(`obfsParam=${proxy[key]}`);
+ } else if (['sni'].includes(key)) {
+ hysteriaParams.push(`peer=${proxy[key]}`);
+ } else if (proxy[key] && !/^_/i.test(key)) {
+ hysteriaParams.push(
+ `${i}=${encodeURIComponent(proxy[key])}`,
+ );
+ }
+ }
+ });
+
+ result = `hysteria://${proxy.server}:${
+ proxy.port
+ }?${hysteriaParams.join('&')}#${encodeURIComponent(
+ proxy.name,
+ )}`;
+ break;
+
+ case 'tuic':
+ if (!proxy.token || proxy.token.length === 0) {
+ let tuicParams = [];
+ Object.keys(proxy).forEach((key) => {
+ if (
+ ![
+ 'name',
+ 'type',
+ 'uuid',
+ 'password',
+ 'server',
+ 'port',
+ 'tls',
+ ].includes(key)
+ ) {
+ const i = key.replace(/-/, '_');
+ if (['alpn'].includes(key)) {
+ if (proxy[key]) {
+ tuicParams.push(
+ `${i}=${encodeURIComponent(
+ Array.isArray(proxy[key])
+ ? proxy[key][0]
+ : proxy[key],
+ )}`,
+ );
+ }
+ } else if (['skip-cert-verify'].includes(key)) {
+ if (proxy[key]) {
+ tuicParams.push(`allow_insecure=1`);
+ }
+ } else if (['tfo', 'fast-open'].includes(key)) {
+ if (
+ proxy[key] &&
+ !tuicParams.includes('fast_open=1')
+ ) {
+ tuicParams.push(`fast_open=1`);
+ }
+ } else if (
+ ['disable-sni', 'reduce-rtt'].includes(key) &&
+ proxy[key]
+ ) {
+ tuicParams.push(`${i.replace(/-/g, '_')}=1`);
+ } else if (
+ ['congestion-controller'].includes(key)
+ ) {
+ tuicParams.push(
+ `congestion_control=${proxy[key]}`,
+ );
+ } else if (proxy[key] && !/^_/i.test(key)) {
+ tuicParams.push(
+ `${i.replace(
+ /-/g,
+ '_',
+ )}=${encodeURIComponent(proxy[key])}`,
+ );
+ }
+ }
+ });
+
+ result = `tuic://${encodeURIComponent(
+ proxy.uuid,
+ )}:${encodeURIComponent(proxy.password)}@${proxy.server}:${
+ proxy.port
+ }?${tuicParams.join('&')}#${encodeURIComponent(
+ proxy.name,
+ )}`;
+ }
+ break;
+ case 'anytls':
+ result = vless({
+ ...proxy,
+ uuid: proxy.password,
+ network: proxy.network || 'tcp',
+ }).replace('vless', 'anytls');
+ // 偷个懒
+ let anytlsParams = [];
+ Object.keys(proxy).forEach((key) => {
+ if (
+ ![
+ 'name',
+ 'type',
+ 'password',
+ 'server',
+ 'port',
+ 'tls',
+ ].includes(key)
+ ) {
+ const i = key.replace(/-/, '_');
+ if (['alpn'].includes(key)) {
+ if (proxy[key]) {
+ anytlsParams.push(
+ `${i}=${encodeURIComponent(
+ Array.isArray(proxy[key])
+ ? proxy[key][0]
+ : proxy[key],
+ )}`,
+ );
+ }
+ } else if (['skip-cert-verify'].includes(key)) {
+ if (proxy[key]) {
+ anytlsParams.push(`insecure=1`);
+ }
+ } else if (['udp'].includes(key)) {
+ if (proxy[key]) {
+ anytlsParams.push(`udp=1`);
+ }
+ } else if (
+ proxy[key] &&
+ !/^_|client-fingerprint/i.test(key) &&
+ ['number', 'string', 'boolean'].includes(
+ typeof proxy[key],
+ )
+ ) {
+ anytlsParams.push(
+ `${i.replace(/-/g, '_')}=${encodeURIComponent(
+ proxy[key],
+ )}`,
+ );
+ }
+ }
+ });
+ // Parse existing query parameters from result
+ const urlParts = result.split('?');
+ let baseUrl = urlParts[0];
+ let existingParams = {};
+
+ if (urlParts.length > 1) {
+ const queryString = urlParts[1].split('#')[0]; // Remove fragment if exists
+ const pairs = queryString.split('&');
+ pairs.forEach((pair) => {
+ const [key, value] = pair.split('=');
+ if (key) {
+ existingParams[key] = value;
+ }
+ });
+ }
+
+ // Merge anytlsParams with existing parameters
+ anytlsParams.forEach((param) => {
+ const [key, value] = param.split('=');
+ if (key) {
+ existingParams[key] = value;
+ }
+ });
+
+ // Reconstruct query string
+ const newParams = Object.keys(existingParams)
+ .map((key) => `${key}=${existingParams[key]}`)
+ .join('&');
+
+ // Get fragment part if exists
+ const fragmentMatch = result.match(/#(.*)$/);
+ const fragment = fragmentMatch ? `#${fragmentMatch[1]}` : '';
+
+ result = `${baseUrl}?${newParams}${fragment}`;
+ // result = `anytls://${encodeURIComponent(proxy.password)}@${
+ // proxy.server
+ // }:${proxy.port}/?${anytlsParams.join('&')}#${encodeURIComponent(
+ // proxy.name,
+ // )}`;
+ break;
+ case 'wireguard':
+ let wireguardParams = [];
+
+ Object.keys(proxy).forEach((key) => {
+ if (
+ ![
+ 'name',
+ 'type',
+ 'server',
+ 'port',
+ 'ip',
+ 'ipv6',
+ 'ip-cidr',
+ 'ipv6-cidr',
+ 'private-key',
+ ].includes(key)
+ ) {
+ if (['public-key'].includes(key)) {
+ wireguardParams.push(
+ `publickey=${encodeURIComponent(proxy[key])}`,
+ );
+ } else if (['udp'].includes(key)) {
+ if (proxy[key]) {
+ wireguardParams.push(`${key}=1`);
+ }
+ } else if (proxy[key] && !/^_/i.test(key)) {
+ wireguardParams.push(
+ `${key}=${encodeURIComponent(proxy[key])}`,
+ );
+ }
+ }
+ });
+ const wireguardIPv4 = getWireGuardAddressWithCIDR(
+ proxy,
+ 'ipv4',
+ );
+ const wireguardIPv6 = getWireGuardAddressWithCIDR(
+ proxy,
+ 'ipv6',
+ );
+ if (wireguardIPv4 && wireguardIPv6) {
+ wireguardParams.push(
+ `address=${encodeURIComponent(
+ `${wireguardIPv4},${wireguardIPv6}`,
+ )}`,
+ );
+ } else if (wireguardIPv4) {
+ wireguardParams.push(
+ `address=${encodeURIComponent(wireguardIPv4)}`,
+ );
+ } else if (wireguardIPv6) {
+ wireguardParams.push(
+ `address=${encodeURIComponent(wireguardIPv6)}`,
+ );
+ }
+ result = `wireguard://${encodeURIComponent(
+ proxy['private-key'],
+ )}@${proxy.server}:${proxy.port}/?${wireguardParams.join(
+ '&',
+ )}#${encodeURIComponent(proxy.name)}`;
+ break;
+ }
+ return result;
+ };
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/producers/utils.js b/backend/src/core/proxy-utils/producers/utils.js
new file mode 100644
index 0000000000..90ccf63a01
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/utils.js
@@ -0,0 +1,168 @@
+import _ from 'lodash';
+import YAML from '@/utils/yaml';
+import { isIPv4, isIPv6 } from '@/utils';
+
+export class Result {
+ constructor(proxy) {
+ this.proxy = proxy;
+ this.output = [];
+ }
+
+ append(data) {
+ if (typeof data === 'undefined') {
+ throw new Error('required field is missing');
+ }
+ this.output.push(data);
+ }
+
+ appendIfPresent(data, attr) {
+ if (isPresent(this.proxy, attr)) {
+ this.append(data);
+ }
+ }
+
+ toString() {
+ return this.output.join('');
+ }
+}
+
+export function isPresent(obj, attr) {
+ const data = _.get(obj, attr);
+ return typeof data !== 'undefined' && data !== null;
+}
+
+export function isShadowsocksOverTls(proxy) {
+ const normalizedNetwork =
+ typeof proxy?.network === 'string'
+ ? proxy.network.trim().toLowerCase()
+ : proxy?.network;
+ return (
+ proxy?.type === 'ss' &&
+ proxy?.tls === true &&
+ !isPresent(proxy, 'plugin') &&
+ (!isPresent(proxy, 'network') || normalizedNetwork === 'tcp')
+ );
+}
+
+export function normalizePluginMuxValue(mux) {
+ if (typeof mux === 'boolean') return Number(mux);
+ if (typeof mux === 'string') {
+ const normalized = mux.trim().toLowerCase();
+ if (normalized === 'true') return 1;
+ if (normalized === 'false') return 0;
+ if (/^\d+$/.test(normalized)) return parseInt(normalized, 10);
+ }
+ return mux;
+}
+
+export function normalizePluginMuxBooleanValue(mux) {
+ return Boolean(normalizePluginMuxValue(mux));
+}
+
+export function supportsShadowsocksV2rayPluginMode(proxy, supportedModes) {
+ if (proxy?.type !== 'ss' || proxy?.plugin !== 'v2ray-plugin') return true;
+
+ const normalizedMode =
+ typeof proxy?.['plugin-opts']?.mode === 'string'
+ ? proxy['plugin-opts'].mode.trim().toLowerCase()
+ : proxy?.['plugin-opts']?.mode;
+
+ return supportedModes.includes(normalizedMode);
+}
+
+function parseWireGuardCIDR(cidr, max) {
+ if (cidr == null) return undefined;
+ const normalized = `${cidr}`.trim();
+ if (!/^\d+$/.test(normalized)) return undefined;
+ const parsed = parseInt(normalized, 10);
+ if (parsed < 0 || parsed > max) return undefined;
+ return parsed;
+}
+
+function parseWireGuardInterfaceAddress(value, family) {
+ if (value == null) return null;
+ const raw = `${value}`.trim();
+ if (!raw) return null;
+ const [, hostRaw = raw, cidrRaw] = /^(.*?)(?:\/(\d+))?$/.exec(raw) || [];
+ const host = `${hostRaw}`.trim().replace(/^\[/, '').replace(/\]$/, '');
+ const isIPv4Family = family === 'ipv4';
+ const isValid = isIPv4Family ? isIPv4(host) : isIPv6(host);
+ if (!isValid) return null;
+ const max = isIPv4Family ? 32 : 128;
+ return {
+ address: host,
+ cidr: parseWireGuardCIDR(cidrRaw, max),
+ };
+}
+
+function normalizeWireGuardInterfaceAddress(proxy, config) {
+ const { addressKey, cidrKey, family, defaultCIDR } = config;
+ const parsed = parseWireGuardInterfaceAddress(proxy[addressKey], family);
+ if (!parsed) {
+ if (
+ proxy[addressKey] == null ||
+ `${proxy[addressKey]}`.trim().length === 0
+ ) {
+ delete proxy[cidrKey];
+ }
+ return;
+ }
+ proxy[addressKey] = parsed.address;
+ const normalizedCIDR = parseWireGuardCIDR(proxy[cidrKey], defaultCIDR);
+ proxy[cidrKey] = normalizedCIDR ?? parsed.cidr ?? defaultCIDR;
+}
+
+export function normalizeWireGuardInterface(proxy = {}) {
+ normalizeWireGuardInterfaceAddress(proxy, {
+ addressKey: 'ip',
+ cidrKey: 'ip-cidr',
+ family: 'ipv4',
+ defaultCIDR: 32,
+ });
+ normalizeWireGuardInterfaceAddress(proxy, {
+ addressKey: 'ipv6',
+ cidrKey: 'ipv6-cidr',
+ family: 'ipv6',
+ defaultCIDR: 128,
+ });
+ return proxy;
+}
+
+export function getWireGuardAddressWithCIDR(proxy = {}, family = 'ipv4') {
+ const config =
+ family === 'ipv6'
+ ? { addressKey: 'ipv6', cidrKey: 'ipv6-cidr', defaultCIDR: 128 }
+ : { addressKey: 'ip', cidrKey: 'ip-cidr', defaultCIDR: 32 };
+ const parsed = parseWireGuardInterfaceAddress(
+ proxy[config.addressKey],
+ family,
+ );
+ if (!parsed) return undefined;
+ const normalizedCIDR = parseWireGuardCIDR(
+ proxy[config.cidrKey],
+ config.defaultCIDR,
+ );
+ return `${parsed.address}/${
+ normalizedCIDR ?? parsed.cidr ?? config.defaultCIDR
+ }`;
+}
+
+export function produceProxyListOutput(list, type, opts = {}) {
+ if (type === 'internal') return list;
+
+ if (opts.prettyYaml || opts['pretty-yaml']) {
+ return YAML.safeDump(
+ {
+ proxies: list,
+ },
+ {
+ lineWidth: -1,
+ },
+ );
+ }
+
+ return (
+ 'proxies:\n' +
+ list.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n').join('')
+ );
+}
diff --git a/backend/src/core/proxy-utils/producers/v2ray.js b/backend/src/core/proxy-utils/producers/v2ray.js
new file mode 100644
index 0000000000..4e41a2529e
--- /dev/null
+++ b/backend/src/core/proxy-utils/producers/v2ray.js
@@ -0,0 +1,24 @@
+/* eslint-disable no-case-declarations */
+import { Base64 } from 'js-base64';
+import URI_Producer from './uri';
+import $ from '@/core/app';
+
+const URI = URI_Producer();
+
+export default function V2Ray_Producer() {
+ const type = 'ALL';
+ const produce = (proxies) => {
+ let result = [];
+ proxies.map((proxy) => {
+ try {
+ result.push(URI.produce(proxy));
+ } catch (err) {
+ $.error(`Cannot produce proxy: ${proxy.name}\nReason: ${err}`);
+ }
+ });
+
+ return Base64.encode(result.join('\n'));
+ };
+
+ return { type, produce };
+}
diff --git a/backend/src/core/proxy-utils/transport-path.js b/backend/src/core/proxy-utils/transport-path.js
new file mode 100644
index 0000000000..8b27775234
--- /dev/null
+++ b/backend/src/core/proxy-utils/transport-path.js
@@ -0,0 +1,164 @@
+function decodeQueryComponent(value) {
+ try {
+ return decodeURIComponent(`${value}`.replace(/\+/g, '%20'));
+ } catch (e) {
+ return value;
+ }
+}
+
+function splitQueryPart(part) {
+ const separatorIndex = part.indexOf('=');
+ if (separatorIndex === -1) {
+ return {
+ key: decodeQueryComponent(part),
+ value: '',
+ };
+ }
+
+ return {
+ key: decodeQueryComponent(part.slice(0, separatorIndex)),
+ value: decodeQueryComponent(part.slice(separatorIndex + 1)),
+ };
+}
+
+export function parseSafeIntegerValue(value) {
+ if (!/^\d+$/.test(`${value}`)) return null;
+
+ const parsed = parseInt(`${value}`, 10);
+ return Number.isSafeInteger(parsed) ? parsed : null;
+}
+
+export function extractPathQueryParam(rawPath, paramName) {
+ const path = rawPath == null ? '' : `${rawPath}`;
+ const queryIndex = path.indexOf('?');
+
+ if (queryIndex === -1) {
+ return {
+ path,
+ value: '',
+ };
+ }
+
+ const basePath = path.slice(0, queryIndex);
+ const query = path.slice(queryIndex + 1);
+ const keptParts = [];
+ let value = '';
+
+ for (const part of query.split('&')) {
+ if (part === '') continue;
+
+ const parsed = splitQueryPart(part);
+ if (parsed.key === paramName) {
+ if (value === '' && parsed.value !== '') {
+ value = parsed.value;
+ }
+ continue;
+ }
+
+ keptParts.push(part);
+ }
+
+ return {
+ path:
+ keptParts.length > 0
+ ? `${basePath}?${keptParts.join('&')}`
+ : basePath,
+ value,
+ };
+}
+
+export function getPathQueryParam(rawPath, paramName) {
+ const path = rawPath == null ? '' : `${rawPath}`;
+ const queryIndex = path.indexOf('?');
+
+ if (queryIndex === -1) return '';
+
+ const query = path.slice(queryIndex + 1);
+ for (const part of query.split('&')) {
+ if (part === '') continue;
+
+ const parsed = splitQueryPart(part);
+ if (parsed.key === paramName && parsed.value !== '') {
+ return parsed.value;
+ }
+ }
+
+ return '';
+}
+
+export function getSafeIntegerPathQueryParam(rawPath, paramName) {
+ const value = getPathQueryParam(rawPath, paramName);
+ const parsed = parseSafeIntegerValue(value);
+
+ if (parsed == null) {
+ return {
+ value: '',
+ parsed: null,
+ };
+ }
+
+ return {
+ value,
+ parsed,
+ };
+}
+
+function appendPathQueryParam(path, paramName, value) {
+ const separator = path.includes('?')
+ ? path.endsWith('?') || path.endsWith('&')
+ ? ''
+ : '&'
+ : '?';
+
+ return `${path}${separator}${encodeURIComponent(
+ paramName,
+ )}=${encodeURIComponent(`${value}`)}`;
+}
+
+export function setPathQueryParam(rawPath, paramName, value) {
+ const path = rawPath == null || rawPath === '' ? '/' : `${rawPath}`;
+ const { path: pathWithoutParam } = extractPathQueryParam(path, paramName);
+
+ return appendPathQueryParam(pathWithoutParam, paramName, value);
+}
+
+export function normalizeWebSocketEarlyDataPath(wsOpts) {
+ const networkPath = wsOpts?.path;
+ if (!wsOpts) return;
+
+ const { value: ed, parsed: maxEarlyData } = getSafeIntegerPathQueryParam(
+ networkPath,
+ 'ed',
+ );
+ if (wsOpts['v2ray-http-upgrade']) {
+ if (ed !== '') {
+ wsOpts.path = extractPathQueryParam(networkPath, 'ed').path;
+ wsOpts['v2ray-http-upgrade-fast-open'] = true;
+ if (
+ wsOpts['_v2ray-http-upgrade-ed'] == null ||
+ `${wsOpts['_v2ray-http-upgrade-ed']}` === ''
+ ) {
+ wsOpts['_v2ray-http-upgrade-ed'] = ed;
+ }
+ }
+ delete wsOpts['early-data-header-name'];
+ delete wsOpts['max-early-data'];
+ return;
+ }
+
+ if (ed === '') return;
+
+ wsOpts.path = extractPathQueryParam(networkPath, 'ed').path;
+ if (wsOpts['early-data-header-name'] == null) {
+ wsOpts['early-data-header-name'] = 'Sec-WebSocket-Protocol';
+ }
+ if (wsOpts['max-early-data'] == null) {
+ wsOpts['max-early-data'] = maxEarlyData;
+ }
+}
+
+export function deleteHttpUpgradeEarlyDataMetadata(wsOpts) {
+ if (!wsOpts) return;
+
+ delete wsOpts['_v2ray-http-upgrade-ed'];
+}
diff --git a/backend/src/core/proxy-utils/validators/index.js b/backend/src/core/proxy-utils/validators/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/src/core/proxy-utils/xhttp-utils.js b/backend/src/core/proxy-utils/xhttp-utils.js
new file mode 100644
index 0000000000..23f714b24c
--- /dev/null
+++ b/backend/src/core/proxy-utils/xhttp-utils.js
@@ -0,0 +1,133 @@
+function parseNormalizedXhttpRangeBounds(
+ value,
+ { allowZeroUpperBound = true } = {},
+) {
+ if (typeof value !== 'string' && typeof value !== 'number') {
+ return undefined;
+ }
+
+ // Mirror Mihomo's ParseRange behavior: accept explicit positive signs,
+ // leading zeros, and ascending ranges. We also compact whitespace so the
+ // emitted value stays compatible with Xray's Int32Range string parser,
+ // which does not trim around the internal `-`.
+ const parseUnsignedIntegerToken = (token) => {
+ const normalizedToken = token.trim();
+ if (!/^\+?\d+$/.test(normalizedToken)) {
+ return undefined;
+ }
+
+ const parsedInteger = parseInt(normalizedToken, 10);
+ return Number.isSafeInteger(parsedInteger) ? parsedInteger : undefined;
+ };
+
+ const normalizedValue = `${value}`.trim();
+ const rangeParts = normalizedValue.split('-');
+ if (rangeParts.length === 1) {
+ const normalizedInteger = parseUnsignedIntegerToken(rangeParts[0]);
+ const minimumAllowedValue = allowZeroUpperBound ? 0 : 1;
+ return normalizedInteger >= minimumAllowedValue
+ ? {
+ lowerBound: normalizedInteger,
+ upperBound: normalizedInteger,
+ }
+ : undefined;
+ }
+
+ if (rangeParts.length !== 2) {
+ return undefined;
+ }
+
+ const lowerBound = parseUnsignedIntegerToken(rangeParts[0]);
+ const upperBound = parseUnsignedIntegerToken(rangeParts[1]);
+ if (lowerBound == null || upperBound == null) {
+ return undefined;
+ }
+
+ const minimumAllowedUpperBound = allowZeroUpperBound ? 0 : 1;
+ return upperBound >= minimumAllowedUpperBound && upperBound >= lowerBound
+ ? {
+ lowerBound,
+ upperBound,
+ }
+ : undefined;
+}
+
+function parseNormalizedXhttpPositiveRangeBounds(value) {
+ return parseNormalizedXhttpRangeBounds(value, {
+ allowZeroUpperBound: false,
+ });
+}
+
+export function normalizeXhttpScalarUpperBound(value) {
+ // IMPORTANT: the legacy-client compatibility reason for collapsing ranges
+ // to an upper bound specifically applies to sc-max-each-post-bytes.
+ // Mihomo first shipped sc-max-each-post-bytes as an int-only field, then
+ // only later added true range support, so emitting `lower-upper` here can
+ // still break older clients that predate that change. Once the new
+ // official Mihomo stable release with ranged sc-max-each-post-bytes is
+ // broadly deployed, switch that field back to preserving full ranges and
+ // remove the upper-bound compatibility logic tied to it.
+ const normalizedBounds = parseNormalizedXhttpPositiveRangeBounds(value);
+ return normalizedBounds?.upperBound;
+}
+
+export function normalizeXhttpPositiveRange(value) {
+ // IMPORTANT: unlike sc-max-each-post-bytes, sc-min-posts-interval-ms does
+ // not need an old-client compatibility shim. Mihomo introduced
+ // sc-min-posts-interval-ms with range semantics from day one, so we should
+ // keep exporting its full normalized range form instead of collapsing it.
+ const normalizedBounds = parseNormalizedXhttpPositiveRangeBounds(value);
+ if (!normalizedBounds) {
+ return undefined;
+ }
+
+ const { lowerBound, upperBound } = normalizedBounds;
+ return lowerBound === upperBound ? upperBound : `${lowerBound}-${upperBound}`;
+}
+
+export function normalizeXhttpNonNegativeRange(value) {
+ const normalizedBounds = parseNormalizedXhttpRangeBounds(value);
+ if (!normalizedBounds) {
+ return undefined;
+ }
+
+ const { lowerBound, upperBound } = normalizedBounds;
+ return lowerBound === upperBound ? upperBound : `${lowerBound}-${upperBound}`;
+}
+
+export function normalizeXhttpIntegerValue(
+ value,
+ { allowNegative = true } = {},
+) {
+ if (
+ typeof value === 'number' &&
+ Number.isFinite(value) &&
+ Number.isSafeInteger(value)
+ ) {
+ if (!allowNegative && value < 0) {
+ return undefined;
+ }
+ return value;
+ }
+
+ if (typeof value !== 'string') {
+ return undefined;
+ }
+
+ const normalizedValue = value.trim();
+ const integerPattern = allowNegative ? /^[+-]?\d+$/ : /^\+?\d+$/;
+ if (!integerPattern.test(normalizedValue)) {
+ return undefined;
+ }
+
+ const parsedInteger = parseInt(normalizedValue, 10);
+ if (!Number.isSafeInteger(parsedInteger)) {
+ return undefined;
+ }
+
+ if (!allowNegative && parsedInteger < 0) {
+ return undefined;
+ }
+
+ return parsedInteger;
+}
diff --git a/backend/src/core/rule-utils/index.js b/backend/src/core/rule-utils/index.js
new file mode 100644
index 0000000000..0152d71ede
--- /dev/null
+++ b/backend/src/core/rule-utils/index.js
@@ -0,0 +1,69 @@
+import RULE_PREPROCESSORS from './preprocessors';
+import RULE_PRODUCERS from './producers';
+import RULE_PARSERS from './parsers';
+import $ from '@/core/app';
+
+export const RuleUtils = (function () {
+ function preprocess(raw) {
+ for (const processor of RULE_PREPROCESSORS) {
+ try {
+ if (processor.test(raw)) {
+ $.info(`Pre-processor [${processor.name}] activated`);
+ return processor.parse(raw);
+ }
+ } catch (e) {
+ $.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
+ }
+ }
+ return raw;
+ }
+
+ function parse(raw) {
+ raw = preprocess(raw);
+ for (const parser of RULE_PARSERS) {
+ let matched;
+ try {
+ matched = parser.test(raw);
+ } catch (err) {
+ matched = false;
+ }
+ if (matched) {
+ $.info(`Rule parser [${parser.name}] is activated!`);
+ return parser.parse(raw);
+ }
+ }
+ }
+
+ function produce(rules, targetPlatform) {
+ const producer = RULE_PRODUCERS[targetPlatform];
+ if (!producer) {
+ throw new Error(
+ `Target platform: ${targetPlatform} is not supported!`,
+ );
+ }
+ if (
+ typeof producer.type === 'undefined' ||
+ producer.type === 'SINGLE'
+ ) {
+ return rules
+ .map((rule) => {
+ try {
+ return producer.func(rule);
+ } catch (err) {
+ console.log(
+ `ERROR: cannot produce rule: ${JSON.stringify(
+ rule,
+ )}\nReason: ${err}`,
+ );
+ return '';
+ }
+ })
+ .filter((line) => line.length > 0)
+ .join('\n');
+ } else if (producer.type === 'ALL') {
+ return producer.func(rules);
+ }
+ }
+
+ return { parse, produce };
+})();
diff --git a/backend/src/core/rule-utils/parsers.js b/backend/src/core/rule-utils/parsers.js
new file mode 100644
index 0000000000..755665243c
--- /dev/null
+++ b/backend/src/core/rule-utils/parsers.js
@@ -0,0 +1,59 @@
+const RULE_TYPES_MAPPING = [
+ [/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
+ [/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
+ [/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
+ [/^USER-AGENT$/i, 'USER-AGENT'],
+ [/^PROCESS-NAME$/, 'PROCESS-NAME'],
+ [/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
+ [/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
+ [/^(IN|SRC)-PORT$/, 'IN-PORT'],
+ [/^PROTOCOL$/, 'PROTOCOL'],
+ [/^IP-CIDR$/i, 'IP-CIDR'],
+ [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
+ [/^GEOIP$/i, 'GEOIP'],
+ [/^GEOSITE$/i, 'GEOSITE'],
+];
+
+function AllRuleParser() {
+ const name = 'Universal Rule Parser';
+ const test = () => true;
+ const parse = (raw) => {
+ const lines = raw.split('\n');
+ const result = [];
+ for (let line of lines) {
+ line = line.trim();
+ // skip empty line
+ if (line.length === 0) continue;
+ // skip comments
+ if (/\s*#/.test(line)) continue;
+ try {
+ const params = line.split(',').map((w) => w.trim());
+ let rawType = params[0];
+ let matched = false;
+ for (const item of RULE_TYPES_MAPPING) {
+ const regex = item[0];
+ if (regex.test(rawType)) {
+ matched = true;
+ const rule = {
+ type: item[1],
+ content: params[1],
+ };
+ if (
+ ['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
+ ) {
+ rule.options = params.slice(2);
+ }
+ result.push(rule);
+ }
+ }
+ if (!matched) throw new Error('Invalid rule type: ' + rawType);
+ } catch (e) {
+ console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
+ }
+ }
+ return result;
+ };
+ return { name, test, parse };
+}
+
+export default [AllRuleParser()];
diff --git a/backend/src/core/rule-utils/preprocessors.js b/backend/src/core/rule-utils/preprocessors.js
new file mode 100644
index 0000000000..8e78d128f8
--- /dev/null
+++ b/backend/src/core/rule-utils/preprocessors.js
@@ -0,0 +1,18 @@
+function HTML() {
+ const name = 'HTML';
+ const test = (raw) => /^/.test(raw);
+ // simply discard HTML
+ const parse = () => '';
+ return { name, test, parse };
+}
+
+function ClashProvider() {
+ const name = 'Clash Provider';
+ const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
+ const parse = (raw) => {
+ return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
+ };
+ return { name, test, parse };
+}
+
+export default [HTML(), ClashProvider()];
diff --git a/backend/src/core/rule-utils/producers.js b/backend/src/core/rule-utils/producers.js
new file mode 100644
index 0000000000..d11577ffe1
--- /dev/null
+++ b/backend/src/core/rule-utils/producers.js
@@ -0,0 +1,101 @@
+import YAML from '@/utils/yaml';
+
+function QXFilter() {
+ const type = 'SINGLE';
+ const func = (rule) => {
+ // skip unsupported rules
+ const UNSUPPORTED = [
+ 'URL-REGEX',
+ 'DEST-PORT',
+ 'SRC-IP',
+ 'IN-PORT',
+ 'PROTOCOL',
+ 'GEOSITE',
+ 'GEOIP',
+ ];
+ if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
+
+ const TRANSFORM = {
+ 'DOMAIN-KEYWORD': 'HOST-KEYWORD',
+ 'DOMAIN-SUFFIX': 'HOST-SUFFIX',
+ DOMAIN: 'HOST',
+ 'IP-CIDR6': 'IP6-CIDR',
+ };
+
+ // QX does not support the no-resolve option
+ return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;
+ };
+ return { type, func };
+}
+
+function SurgeRuleSet() {
+ const type = 'SINGLE';
+ const func = (rule) => {
+ const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
+ if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
+ let output = `${rule.type},${rule.content}`;
+ if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
+ output +=
+ rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
+ }
+ return output;
+ };
+ return { type, func };
+}
+
+function LoonRules() {
+ const type = 'SINGLE';
+ const func = (rule) => {
+ // skip unsupported rules
+ const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
+ if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
+ if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
+ // Loon only supports the no-resolve option
+ rule.options = rule.options.filter((option) =>
+ ['no-resolve'].includes(option),
+ );
+ }
+ return SurgeRuleSet().func(rule);
+ };
+ return { type, func };
+}
+
+function ClashRuleProvider() {
+ const type = 'ALL';
+ const func = (rules) => {
+ const TRANSFORM = {
+ 'DEST-PORT': 'DST-PORT',
+ 'SRC-IP': 'SRC-IP-CIDR',
+ 'IN-PORT': 'SRC-PORT',
+ };
+ const conf = {
+ payload: rules.map((rule) => {
+ let output = `${TRANSFORM[rule.type] || rule.type},${
+ rule.content
+ }`;
+ if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
+ if (rule.options) {
+ // Clash only supports the no-resolve option
+ rule.options = rule.options.filter((option) =>
+ ['no-resolve'].includes(option),
+ );
+ }
+ output +=
+ rule.options?.length > 0
+ ? `,${rule.options.join(',')}`
+ : '';
+ }
+ return output;
+ }),
+ };
+ return YAML.dump(conf);
+ };
+ return { type, func };
+}
+
+export default {
+ QX: QXFilter(),
+ Surge: SurgeRuleSet(),
+ Loon: LoonRules(),
+ Clash: ClashRuleProvider(),
+};
diff --git a/backend/src/main.js b/backend/src/main.js
new file mode 100644
index 0000000000..594f6ec031
--- /dev/null
+++ b/backend/src/main.js
@@ -0,0 +1,26 @@
+/**
+ * ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
+ * ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
+ * ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
+ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
+ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
+ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
+ * Advanced Subscription Manager for QX, Loon, Surge and Clash.
+ * @author: Peng-YM
+ * @github: https://github.com/sub-store-org/Sub-Store
+ * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
+ */
+import { version } from '../package.json';
+import $ from '@/core/app';
+console.log(
+ `
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ Sub-Store -- v${version}
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+`,
+);
+import migrate from '@/utils/migration';
+import serve from '@/restful';
+
+migrate();
+serve();
diff --git a/backend/src/products/cron-sync-artifacts.js b/backend/src/products/cron-sync-artifacts.js
new file mode 100644
index 0000000000..8a5978dcac
--- /dev/null
+++ b/backend/src/products/cron-sync-artifacts.js
@@ -0,0 +1,299 @@
+import { version } from '../../package.json';
+import {
+ SETTINGS_KEY,
+ ARTIFACTS_KEY,
+ SUBS_KEY,
+ COLLECTIONS_KEY,
+} from '@/constants';
+import $ from '@/core/app';
+import {
+ markArtifactProducedWithoutUpload,
+ produceArtifact,
+ shouldUploadArtifact,
+ uploadArtifactBatches,
+} from '@/restful/sync';
+import { findByName } from '@/utils/database';
+import {
+ resolveCronArtifactSyncPolicy,
+ shouldSkipCronArtifactWithoutUploadCredentials,
+} from '@/utils/artifact-sync-policy';
+
+!(async function () {
+ let arg;
+ if (typeof $argument != 'undefined') {
+ // eslint-disable-next-line no-undef
+ arg = parseArgument($argument);
+ } else {
+ arg = {};
+ }
+ let sub_names = (arg?.subscription ?? arg?.sub ?? '')
+ .split(/,|,/g)
+ .map((i) => i.trim())
+ .filter((i) => i.length > 0)
+ .map((i) => decodeURIComponent(i));
+ let col_names = (arg?.collection ?? arg?.col ?? '')
+ .split(/,|,/g)
+ .map((i) => i.trim())
+ .filter((i) => i.length > 0)
+ .map((i) => decodeURIComponent(i));
+ if (sub_names.length > 0 || col_names.length > 0) {
+ if (sub_names.length > 0)
+ await produceArtifacts(sub_names, 'subscription');
+ if (col_names.length > 0)
+ await produceArtifacts(col_names, 'collection');
+ } else {
+ const settings = $.read(SETTINGS_KEY);
+ const artifacts = $.read(ARTIFACTS_KEY);
+ if (!artifacts || artifacts.length === 0) return;
+
+ const policy = resolveCronArtifactSyncPolicy({ artifacts, settings });
+
+ if (policy.shouldRun) await doSync(arg, policy);
+ }
+})().finally(() => $.done());
+
+function parseArgument(rawArgument) {
+ if (rawArgument == null) return {};
+ if (typeof rawArgument === 'object') return rawArgument;
+ return Object.fromEntries(
+ `${rawArgument}`
+ .split('&')
+ .filter(Boolean)
+ .map((item) => {
+ const [key, ...value] = item.split('=');
+ return [key, value.join('=')];
+ }),
+ );
+}
+
+function isTruthyArgument(value, defaultValue = true) {
+ if (value == null || value === '') return defaultValue;
+ const normalized = `${value}`
+ .trim()
+ .replace(/^["']|["']$/g, '')
+ .toLowerCase();
+ return !['false', '0', 'no', 'off'].includes(normalized);
+}
+
+async function produceArtifacts(names, type) {
+ try {
+ if (names.length > 0) {
+ $.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`);
+ await Promise.all(
+ names.map(async (name) => {
+ try {
+ await produceArtifact({
+ type,
+ name,
+ });
+ } catch (e) {
+ $.error(`${type} ${name} error: ${e.message ?? e}`);
+ }
+ }),
+ );
+ $.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`);
+ }
+ } catch (e) {
+ $.error(`produceArtifacts error: ${e.message ?? e}`);
+ }
+}
+async function doSync(arg = {}, { canUpload = true } = {}) {
+ const syncSuccessNotify = isTruthyArgument(arg.sync_success_notify);
+ console.log(
+ `
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ Sub-Store Sync -- v${version}
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+`,
+ );
+
+ $.info('开始同步所有远程配置...');
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const files = {};
+
+ try {
+ const valid = [];
+ const invalid = [];
+ const producedWithoutUpload = [];
+ const skippedWithoutUploadCredentials = [];
+ const allSubs = $.read(SUBS_KEY);
+ const allCols = $.read(COLLECTIONS_KEY);
+ const subNames = [];
+ let enabledCount = 0;
+ allArtifacts.map((artifact) => {
+ if (artifact.sync && artifact.source) {
+ enabledCount++;
+ if (
+ shouldSkipCronArtifactWithoutUploadCredentials(artifact, {
+ canUpload,
+ })
+ ) {
+ return;
+ }
+ if (artifact.type === 'subscription') {
+ const subName = artifact.source;
+ const sub = findByName(allSubs, subName);
+ if (sub && sub.url && !subNames.includes(subName)) {
+ subNames.push(subName);
+ }
+ } else if (artifact.type === 'collection') {
+ const collection = findByName(allCols, artifact.source);
+ if (collection && Array.isArray(collection.subscriptions)) {
+ collection.subscriptions.map((subName) => {
+ const sub = findByName(allSubs, subName);
+ if (sub && sub.url && !subNames.includes(subName)) {
+ subNames.push(subName);
+ }
+ });
+ }
+ }
+ }
+ });
+
+ if (enabledCount === 0) {
+ $.info(
+ `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
+ );
+ return;
+ }
+
+ if (subNames.length > 0) {
+ await Promise.all(
+ subNames.map(async (subName) => {
+ try {
+ await produceArtifact({
+ type: 'subscription',
+ name: subName,
+ awaitCustomCache: true,
+ });
+ } catch (e) {
+ // $.error(`${e.message ?? e}`);
+ }
+ }),
+ );
+ }
+ await Promise.all(
+ allArtifacts.map(async (artifact) => {
+ try {
+ if (artifact.sync && artifact.source) {
+ if (
+ shouldSkipCronArtifactWithoutUploadCredentials(
+ artifact,
+ { canUpload },
+ )
+ ) {
+ skippedWithoutUploadCredentials.push(artifact.name);
+ return;
+ }
+
+ $.info(`正在同步云配置:${artifact.name}...`);
+
+ const useMihomoExternal =
+ artifact.platform === 'SurgeMac';
+
+ if (useMihomoExternal) {
+ $.info(
+ `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
+ );
+ }
+ const output = await produceArtifact({
+ type: artifact.type,
+ name: artifact.source,
+ platform: artifact.platform,
+ produceOpts: {
+ 'include-unsupported-proxy':
+ artifact.includeUnsupportedProxy,
+ useMihomoExternal,
+ prettyYaml: artifact.prettyYaml,
+ },
+ });
+
+ // if (!output || output.length === 0)
+ // throw new Error('该配置的结果为空 不进行上传');
+
+ if (shouldUploadArtifact(artifact)) {
+ files[encodeURIComponent(artifact.name)] = {
+ content: output,
+ };
+ valid.push(artifact.name);
+ } else {
+ markArtifactProducedWithoutUpload(artifact);
+ producedWithoutUpload.push(artifact.name);
+ }
+ }
+ } catch (e) {
+ $.error(
+ `生成同步配置 ${artifact.name} 发生错误: ${
+ e.message ?? e
+ }`,
+ );
+ invalid.push(artifact.name);
+ }
+ }),
+ );
+
+ const producedCount = valid.length + producedWithoutUpload.length;
+ $.info(
+ `${producedCount} 个同步配置生成成功: ${valid
+ .concat(producedWithoutUpload)
+ .join(', ')}`,
+ );
+ $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
+ if (producedWithoutUpload.length > 0) {
+ $.info(
+ `${
+ producedWithoutUpload.length
+ } 个同步配置仅生成未上传: ${producedWithoutUpload.join(', ')}`,
+ );
+ }
+ if (skippedWithoutUploadCredentials.length > 0) {
+ $.info(
+ `${
+ skippedWithoutUploadCredentials.length
+ } 个同步配置因未设置 GitHub Token 已跳过上传: ${skippedWithoutUploadCredentials.join(
+ ', ',
+ )}`,
+ );
+ }
+
+ if (producedCount === 0) {
+ throw new Error(
+ `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
+ );
+ }
+
+ const uploaded = await uploadArtifactBatches({
+ allArtifacts,
+ files,
+ valid,
+ invalid,
+ });
+
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ $.info('同步配置执行完成');
+
+ if (invalid.length > 0) {
+ $.notify(
+ '🌍 Sub-Store',
+ `同步配置成功 ${
+ uploaded.length + producedWithoutUpload.length
+ } 个, 失败 ${invalid.length} 个${
+ skippedWithoutUploadCredentials.length
+ ? `, 跳过 ${skippedWithoutUploadCredentials.length} 个需上传配置`
+ : ''
+ }, 详情请查看日志`,
+ );
+ } else if (syncSuccessNotify) {
+ $.notify(
+ '🌍 Sub-Store',
+ '同步配置完成',
+ skippedWithoutUploadCredentials.length
+ ? `已跳过 ${skippedWithoutUploadCredentials.length} 个需上传配置(未设置 GitHub Token)`
+ : undefined,
+ );
+ }
+ } catch (e) {
+ $.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
+ $.error(`无法同步配置到 Gist,原因:${e.stack ?? e.message ?? e}`);
+ }
+}
diff --git a/backend/src/products/proxy-utils.esm.js b/backend/src/products/proxy-utils.esm.js
new file mode 100644
index 0000000000..68034b6387
--- /dev/null
+++ b/backend/src/products/proxy-utils.esm.js
@@ -0,0 +1,5 @@
+import { ProxyUtils } from '@/core/proxy-utils';
+
+const { parse, produce } = ProxyUtils;
+
+export { parse, produce };
diff --git a/backend/src/products/resource-parser.loon.js b/backend/src/products/resource-parser.loon.js
new file mode 100644
index 0000000000..4851756733
--- /dev/null
+++ b/backend/src/products/resource-parser.loon.js
@@ -0,0 +1,100 @@
+/* eslint-disable no-undef */
+import { ProxyUtils } from '@/core/proxy-utils';
+import { RuleUtils } from '@/core/rule-utils';
+import { version } from '../../package.json';
+import download from '@/utils/download';
+
+let result = '';
+let resource = typeof $resource !== 'undefined' ? $resource : '';
+let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';
+let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
+
+!(async () => {
+ console.log(
+ `
+ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ Sub-Store -- v${version}
+ Loon -- ${$loon}
+ ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ `,
+ );
+
+ let arg;
+ if (typeof $argument != 'undefined') {
+ arg = Object.fromEntries(
+ $argument.split('&').map((item) => item.split('=')),
+ );
+ } else {
+ arg = {};
+ }
+ console.log(`arg: ${JSON.stringify(arg)}`);
+
+ const RESOURCE_TYPE = {
+ PROXY: 1,
+ RULE: 2,
+ };
+ if (!arg.resourceUrlOnly) {
+ result = resource;
+ }
+
+ if (resourceType === RESOURCE_TYPE.PROXY) {
+ if (!arg.resourceUrlOnly) {
+ try {
+ let proxies = ProxyUtils.parse(resource);
+ result = ProxyUtils.produce(proxies, 'Loon', undefined, {
+ 'include-unsupported-proxy': arg?.includeUnsupportedProxy,
+ });
+ } catch (e) {
+ console.log('解析器: 使用 resource 出现错误');
+ console.log(e.message ?? e);
+ }
+ }
+ if ((!result || /^\s*$/.test(result)) && resourceUrl) {
+ console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
+ try {
+ let raw = await download(
+ resourceUrl,
+ arg?.ua,
+ arg?.timeout,
+ undefined,
+ undefined,
+ undefined,
+ arg?.noCache,
+ true,
+ );
+ let proxies = ProxyUtils.parse(raw);
+ result = ProxyUtils.produce(proxies, 'Loon', undefined, {
+ 'include-unsupported-proxy': arg?.includeUnsupportedProxy,
+ });
+ } catch (e) {
+ console.log(e.message ?? e);
+ }
+ }
+ } else if (resourceType === RESOURCE_TYPE.RULE) {
+ if (!arg.resourceUrlOnly) {
+ try {
+ const rules = RuleUtils.parse(resource);
+ result = RuleUtils.produce(rules, 'Loon');
+ } catch (e) {
+ console.log(e.message ?? e);
+ }
+ }
+ if ((!result || /^\s*$/.test(result)) && resourceUrl) {
+ console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);
+ try {
+ let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
+ let rules = RuleUtils.parse(raw);
+ result = RuleUtils.produce(rules, 'Loon');
+ } catch (e) {
+ console.log(e.message ?? e);
+ }
+ }
+ }
+})()
+ .catch(async (e) => {
+ console.log('解析器: 出现错误');
+ console.log(e.message ?? e);
+ })
+ .finally(() => {
+ $done(result || '');
+ });
diff --git a/backend/src/products/sub-store-0.js b/backend/src/products/sub-store-0.js
new file mode 100644
index 0000000000..182ddb04e7
--- /dev/null
+++ b/backend/src/products/sub-store-0.js
@@ -0,0 +1,49 @@
+/**
+ * 路由拆分 - 本文件只包含不涉及到解析器的 RESTFul API
+ */
+
+import { version } from '../../package.json';
+console.log(
+ `
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ Sub-Store -- v${version}
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+`,
+);
+
+import migrate from '@/utils/migration';
+import express from '@/vendor/express';
+import $ from '@/core/app';
+import registerCollectionRoutes from '@/restful/collections';
+import registerSubscriptionRoutes from '@/restful/subscriptions';
+import registerArtifactRoutes from '@/restful/artifacts';
+import registerSettingRoutes from '@/restful/settings';
+import registerMiscRoutes from '@/restful/miscs';
+import registerSortRoutes from '@/restful/sort';
+import registerFileRoutes from '@/restful/file';
+import registerTokenRoutes from '@/restful/token';
+import registerArchiveRoutes from '@/restful/archives';
+import registerModuleRoutes from '@/restful/module';
+import registerLogRoutes from '@/restful/logs';
+
+migrate();
+serve();
+
+function serve() {
+ const $app = express({ substore: $ });
+
+ // register routes
+ registerCollectionRoutes($app);
+ registerSubscriptionRoutes($app);
+ registerTokenRoutes($app);
+ registerFileRoutes($app);
+ registerModuleRoutes($app);
+ registerArtifactRoutes($app);
+ registerSettingRoutes($app);
+ registerSortRoutes($app);
+ registerArchiveRoutes($app);
+ registerMiscRoutes($app);
+ registerLogRoutes($app);
+
+ $app.start();
+}
diff --git a/backend/src/products/sub-store-1.js b/backend/src/products/sub-store-1.js
new file mode 100644
index 0000000000..1a17b8ec88
--- /dev/null
+++ b/backend/src/products/sub-store-1.js
@@ -0,0 +1,39 @@
+/**
+ * 路由拆分 - 本文件仅包含使用到解析器的 RESTFul API
+ */
+
+import { version } from '../../package.json';
+import migrate from '@/utils/migration';
+import express from '@/vendor/express';
+import $ from '@/core/app';
+import registerDownloadRoutes from '@/restful/download';
+import registerPreviewRoutes from '@/restful/preview';
+import registerSyncRoutes from '@/restful/sync';
+import registerNodeInfoRoutes from '@/restful/node-info';
+
+console.log(
+ `
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+ Sub-Store -- v${version}
+┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
+`,
+);
+
+migrate();
+serve();
+
+function serve() {
+ const $app = express({ substore: $ });
+
+ // register routes
+ registerDownloadRoutes($app);
+ registerPreviewRoutes($app);
+ registerSyncRoutes($app);
+ registerNodeInfoRoutes($app);
+
+ $app.options('/', (req, res) => {
+ res.status(200).end();
+ });
+
+ $app.start();
+}
diff --git a/backend/src/restful/archives.js b/backend/src/restful/archives.js
new file mode 100644
index 0000000000..71523dba54
--- /dev/null
+++ b/backend/src/restful/archives.js
@@ -0,0 +1,114 @@
+import { failed, success } from '@/restful/response';
+import {
+ InternalServerError,
+ RequestInvalidError,
+ ResourceNotFoundError,
+} from '@/restful/errors';
+import {
+ ensureArchiveStore,
+ getArchiveEntries,
+ getRequiredArchiveEntry,
+ removeArchiveEntry,
+} from '@/utils/archive';
+import { createSubscriptionItem } from '@/restful/subscriptions';
+import { createCollectionItem } from '@/restful/collections';
+import { createFileItem } from '@/restful/file';
+import { createArtifactItem } from '@/restful/artifacts';
+import { createTokenItem } from '@/restful/token';
+
+export default function register($app) {
+ ensureArchiveStore();
+
+ $app.route('/api/archives').get(getAllArchiveEntries);
+ $app.route('/api/archives/:id')
+ .get(getArchiveDetail)
+ .delete(deleteArchiveEntry);
+ $app.post('/api/archives/:id/restore', restoreArchiveEntry);
+}
+
+function getAllArchiveEntries(_, res) {
+ success(res, getArchiveEntries());
+}
+
+function getArchiveDetail(req, res) {
+ try {
+ success(res, getRequiredArchiveEntry(req.params.id));
+ } catch (error) {
+ failed(res, error, error instanceof ResourceNotFoundError ? 404 : 500);
+ }
+}
+
+function deleteArchiveEntry(req, res) {
+ try {
+ const entry = removeArchiveEntry(req.params.id);
+ success(res, entry);
+ } catch (error) {
+ failed(res, error, error instanceof ResourceNotFoundError ? 404 : 500);
+ }
+}
+
+function restoreArchiveEntry(req, res) {
+ try {
+ const entry = getRequiredArchiveEntry(req.params.id);
+ const restored = restoreArchivedEntry(entry);
+ removeArchiveEntry(req.params.id);
+ success(res, restored);
+ } catch (error) {
+ const mappedError =
+ error instanceof RequestInvalidError ||
+ error instanceof ResourceNotFoundError
+ ? error
+ : new InternalServerError(
+ 'ARCHIVE_RESTORE_FAILED',
+ 'Failed to restore archive entry',
+ `Reason: ${error.message ?? error}`,
+ );
+ const statusCode =
+ mappedError instanceof ResourceNotFoundError
+ ? 404
+ : mappedError instanceof RequestInvalidError
+ ? 400
+ : 500;
+ failed(
+ res,
+ mappedError,
+ statusCode,
+ );
+ }
+}
+
+function restoreArchivedEntry(entry) {
+ const snapshot = JSON.parse(JSON.stringify(entry.snapshot));
+ switch (entry.itemType) {
+ case 'sub':
+ return createSubscriptionItem(snapshot);
+ case 'col':
+ return createCollectionItem(snapshot);
+ case 'file':
+ return createFileItem(snapshot);
+ case 'artifact':
+ return createArtifactItem(normalizeArtifactSnapshotForRestore(snapshot));
+ case 'share':
+ return createTokenItem(snapshot, {
+ mode: snapshot.mode,
+ expiresIn: snapshot.expiresIn,
+ exp: snapshot.exp,
+ });
+ default:
+ throw new RequestInvalidError(
+ 'INVALID_ARCHIVE_TYPE',
+ `Unsupported archive item type: ${entry.itemType}`,
+ );
+ }
+}
+
+function normalizeArtifactSnapshotForRestore(snapshot) {
+ const nextSnapshot = {
+ ...snapshot,
+ };
+ delete nextSnapshot.updated;
+ delete nextSnapshot.url;
+ return nextSnapshot;
+}
+
+export { restoreArchivedEntry };
diff --git a/backend/src/restful/artifacts.js b/backend/src/restful/artifacts.js
new file mode 100644
index 0000000000..037d392714
--- /dev/null
+++ b/backend/src/restful/artifacts.js
@@ -0,0 +1,447 @@
+import $ from '@/core/app';
+
+import {
+ ARTIFACT_REPOSITORY_KEY,
+ ARTIFACTS_KEY,
+ SETTINGS_KEY,
+} from '@/constants';
+import {
+ deleteByName,
+ findByName,
+ insertByPosition,
+ updateByName,
+} from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+import { failed, success } from '@/restful/response';
+import {
+ InternalServerError,
+ RequestInvalidError,
+ ResourceNotFoundError,
+} from '@/restful/errors';
+import Gist from '@/utils/gist';
+import { archiveArtifact } from '@/utils/archive';
+import {
+ normalizeArtifactCron,
+ refreshArtifactCronJobs,
+} from '@/utils/artifact-cron';
+
+const ARTIFACT_GIST_PLACEHOLDER_FILENAME = '.sub-store-placeholder';
+const ARTIFACT_GIST_PLACEHOLDER_CONTENT = [
+ 'Sub-Store placeholder',
+ 'This file keeps the Gist alive when all sync configuration files are deleted.',
+].join('\n');
+const DEFAULT_ARTIFACT_SYNC_BATCH_SIZE = 10;
+
+function normalizeArtifactSyncBatchSize(value) {
+ const batchSize = Math.floor(Number(value));
+
+ if (!isFinite(batchSize) || batchSize <= 0) {
+ return DEFAULT_ARTIFACT_SYNC_BATCH_SIZE;
+ }
+
+ return batchSize;
+}
+
+export default function register($app) {
+ // Initialization
+ if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
+
+ // RESTful APIs
+ $app.get('/api/artifacts/restore', restoreArtifacts);
+
+ $app.route('/api/artifacts')
+ .get(getAllArtifacts)
+ .post(createArtifact)
+ .put(replaceArtifact);
+
+ $app.route('/api/artifact/:name')
+ .get(getArtifact)
+ .patch(updateArtifact)
+ .delete(deleteArtifact);
+}
+
+async function restoreArtifacts(_, res) {
+ $.info('开始恢复远程配置...');
+ try {
+ const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
+ if (!gistToken) {
+ return Promise.reject('未设置 GitHub Token!');
+ }
+ const manager = new Gist({
+ token: gistToken,
+ key: ARTIFACT_REPOSITORY_KEY,
+ syncPlatform,
+ });
+
+ try {
+ const gist = await manager.locate();
+ if (!gist?.files) {
+ throw new Error(`找不到 Sub-Store Gist 文件列表`);
+ }
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const failed = [];
+ Object.keys(gist.files).map((key) => {
+ const filename = gist.files[key]?.filename;
+ if (filename) {
+ if (isArtifactGistPlaceholder(filename)) {
+ $.info(`忽略 Gist 占位文件: ${filename}`);
+ return;
+ }
+ if (encodeURIComponent(filename) !== filename) {
+ $.error(`文件名 ${filename} 未编码 不保存`);
+ failed.push(filename);
+ } else {
+ const artifact = findByName(allArtifacts, filename);
+ if (artifact) {
+ updateByName(allArtifacts, filename, {
+ ...artifact,
+ url: gist.files[key]?.raw_url.replace(
+ /\/raw\/[^/]*\/(.*)/,
+ '/raw/$1',
+ ),
+ });
+ } else {
+ allArtifacts.push({
+ name: `${filename}`,
+ url: gist.files[key]?.raw_url.replace(
+ /\/raw\/[^/]*\/(.*)/,
+ '/raw/$1',
+ ),
+ });
+ }
+ }
+ }
+ });
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ refreshArtifactCronJobs();
+ } catch (err) {
+ $.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
+ throw err;
+ }
+ success(res);
+ } catch (e) {
+ $.error(`恢复远程配置失败,原因:${e.message ?? e}`);
+ failed(
+ res,
+ new InternalServerError(
+ `FAILED_TO_RESTORE_ARTIFACTS`,
+ `Failed to restore artifacts`,
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+function getAllArtifacts(req, res) {
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ success(res, allArtifacts);
+}
+
+function replaceArtifact(req, res) {
+ try {
+ const allArtifacts = req.body;
+ allArtifacts.forEach(normalizeArtifactCron);
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ refreshArtifactCronJobs();
+ success(res);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+async function getArtifact(req, res) {
+ let { name } = req.params;
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const artifact = findByName(allArtifacts, name);
+
+ if (artifact) {
+ success(res, artifact);
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Artifact ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+function createArtifact(req, res) {
+ try {
+ const artifact = createArtifactItem(req.body);
+ success(res, artifact, 201);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function updateArtifact(req, res) {
+ let artifact = req.body;
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ let oldName = req.params.name;
+ const oldArtifact = findByName(allArtifacts, oldName);
+ if (oldArtifact) {
+ if (!artifact.name) artifact.name = oldArtifact.name;
+ $.info(`正在更新远程配置:${oldArtifact.name}`);
+ const newArtifact = {
+ ...oldArtifact,
+ ...artifact,
+ };
+ if (!validateArtifactName(newArtifact.name)) {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'INVALID_ARTIFACT_NAME',
+ `Artifact name ${newArtifact.name} is invalid.`,
+ ),
+ );
+ return;
+ }
+ try {
+ normalizeArtifactCron(newArtifact);
+ } catch (error) {
+ failed(res, error);
+ return;
+ }
+ updateByName(allArtifacts, oldName, newArtifact);
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ refreshArtifactCronJobs();
+ success(res, newArtifact);
+ } else {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ `Artifact ${oldName} already exists.`,
+ ),
+ );
+ }
+}
+
+async function deleteArtifact(req, res) {
+ try {
+ let { name } = req.params;
+ $.info(`正在删除远程配置:${name}`);
+ if (shouldArchiveDeletion(req.query.mode)) {
+ archiveArtifact(name);
+ }
+ const result = await deleteArtifactItem(name);
+ success(res, result);
+ } catch (err) {
+ $.error(`无法删除远程配置:${req.params.name},原因:${err}`);
+ failed(
+ res,
+ err instanceof InternalServerError ||
+ err instanceof RequestInvalidError ||
+ err instanceof ResourceNotFoundError
+ ? err
+ : new InternalServerError(
+ `FAILED_TO_DELETE_ARTIFACT`,
+ `Failed to delete artifact ${req.params.name}`,
+ `Reason: ${err}`,
+ ),
+ );
+ }
+}
+
+function validateArtifactName(name) {
+ return (
+ /^[a-zA-Z0-9._-]*$/.test(name) &&
+ !isArtifactGistPlaceholder(name)
+ );
+}
+
+function createArtifactItem(artifact) {
+ if (!validateArtifactName(artifact.name)) {
+ throw new RequestInvalidError(
+ 'INVALID_ARTIFACT_NAME',
+ `Artifact name ${artifact.name} is invalid.`,
+ );
+ }
+
+ $.info(`正在创建远程配置:${artifact.name}`);
+ normalizeArtifactCron(artifact);
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ if (findByName(allArtifacts, artifact.name)) {
+ throw new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ `Artifact ${artifact.name} already exists.`,
+ );
+ }
+ insertByPosition(allArtifacts, artifact, getCreateItemPosition());
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ refreshArtifactCronJobs();
+ return artifact;
+}
+
+async function deleteArtifactItem(name) {
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const artifact = findByName(allArtifacts, name);
+ if (!artifact) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Artifact ${name} does not exist!`,
+ );
+ }
+ const remote = {
+ attempted: false,
+ status: 'not_attempted',
+ };
+ if (artifact.url || (artifact.updated && artifact.upload !== false)) {
+ const files = {};
+ files[encodeURIComponent(artifact.name)] = {
+ content: '',
+ };
+ if (encodeURIComponent(artifact.name) !== artifact.name) {
+ files[artifact.name] = {
+ content: '',
+ };
+ }
+ remote.attempted = true;
+ try {
+ const resp = await syncToGist(files);
+ const fallback = resp.subStoreUploadMeta?.emptyFileFallback;
+ remote.status =
+ fallback?.status === 'created' ||
+ fallback?.status === 'retained'
+ ? 'placeholder_retained'
+ : 'deleted';
+ if (fallback?.filename) {
+ remote.placeholderFilename = fallback.filename;
+ }
+ } catch (error) {
+ remote.status = 'failed';
+ remote.message = `${error.message ?? error}`;
+ $.error(`Function syncToGist: ${name} : ${error}`);
+ }
+ }
+ deleteByName(allArtifacts, name);
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ refreshArtifactCronJobs();
+ return {
+ artifact,
+ remote,
+ };
+}
+
+function shouldArchiveDeletion(mode) {
+ if (mode == null || mode === '' || mode === 'permanent') {
+ return false;
+ }
+ if (mode === 'archive') {
+ return true;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_DELETE_MODE',
+ `Unsupported delete mode: ${mode}`,
+ );
+}
+
+function isArtifactGistPlaceholder(name) {
+ return name === ARTIFACT_GIST_PLACEHOLDER_FILENAME;
+}
+
+function getArtifactGistEmptyFileFallback() {
+ return {
+ filename: ARTIFACT_GIST_PLACEHOLDER_FILENAME,
+ content: ARTIFACT_GIST_PLACEHOLDER_CONTENT,
+ };
+}
+
+async function syncToGist(files, options = {}) {
+ const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
+ if (!gistToken) {
+ return Promise.reject('未设置 GitHub Token!');
+ }
+ const uploadSummary = summarizeGistUploadFiles(files);
+ $.info(
+ `准备同步 Gist: 文件数 ${uploadSummary.count}, 总大小 ${formatBytes(
+ uploadSummary.totalBytes,
+ )}, 最大文件 ${
+ uploadSummary.largestFilename || '-'
+ } (${formatBytes(uploadSummary.largestBytes)})`,
+ );
+ const manager = new Gist({
+ token: gistToken,
+ key: ARTIFACT_REPOSITORY_KEY,
+ syncPlatform,
+ });
+ let res;
+ try {
+ res = await manager.upload(files, {
+ ...options,
+ emptyFileFallback:
+ options.emptyFileFallback ?? getArtifactGistEmptyFileFallback(),
+ });
+ } catch (error) {
+ $.error(
+ `同步 Gist 请求失败: 文件数 ${uploadSummary.count}, 总大小 ${formatBytes(
+ uploadSummary.totalBytes,
+ )}, 最大文件 ${
+ uploadSummary.largestFilename || '-'
+ } (${formatBytes(uploadSummary.largestBytes)}), 原因: ${
+ error.message ?? error
+ }`,
+ );
+ throw error;
+ }
+ let body = {};
+ try {
+ body = JSON.parse(res.body);
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+
+ const url = body?.html_url ?? body?.web_url;
+ const settings = $.read(SETTINGS_KEY);
+ if (url) {
+ $.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`);
+ settings.artifactStore = url;
+ settings.artifactStoreStatus = 'VALID';
+ } else {
+ $.error(`同步 Gist 后, 找不到 Sub-Store Gist`);
+ settings.artifactStoreStatus = 'NOT FOUND';
+ }
+ $.write(settings, SETTINGS_KEY);
+ return res;
+}
+
+function summarizeGistUploadFiles(files) {
+ return Object.entries(files || {}).reduce(
+ (summary, [filename, file]) => {
+ const content = file?.content;
+ if (typeof content !== 'string') return summary;
+ const bytes = stringByteLength(content);
+ summary.count++;
+ summary.totalBytes += bytes;
+ if (bytes > summary.largestBytes) {
+ summary.largestBytes = bytes;
+ summary.largestFilename = filename;
+ }
+ return summary;
+ },
+ {
+ count: 0,
+ totalBytes: 0,
+ largestBytes: 0,
+ largestFilename: '',
+ },
+ );
+}
+
+function stringByteLength(value) {
+ if (typeof TextEncoder !== 'undefined') {
+ return new TextEncoder().encode(value).length;
+ }
+ return unescape(encodeURIComponent(value)).length;
+}
+
+function formatBytes(size) {
+ if (size < 1024) return `${size} B`;
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
+ return `${(size / 1024 / 1024).toFixed(1)} MB`;
+}
+
+export { syncToGist, normalizeArtifactSyncBatchSize };
+export { createArtifactItem, deleteArtifactItem };
diff --git a/backend/src/restful/collections.js b/backend/src/restful/collections.js
new file mode 100644
index 0000000000..a198f76374
--- /dev/null
+++ b/backend/src/restful/collections.js
@@ -0,0 +1,196 @@
+import {
+ deleteByName,
+ findByName,
+ insertByPosition,
+ updateByName,
+} from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+import { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants';
+import { archiveCollection } from '@/utils/archive';
+import { failed, success } from '@/restful/response';
+import $ from '@/core/app';
+import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
+import { formatDateTime } from '@/utils';
+
+export default function register($app) {
+ if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
+
+ $app.route('/api/collection/:name')
+ .get(getCollection)
+ .patch(updateCollection)
+ .delete(deleteCollection);
+
+ $app.route('/api/collections')
+ .get(getAllCollections)
+ .post(createCollection)
+ .put(replaceCollection);
+}
+
+// collection API
+function createCollection(req, res) {
+ try {
+ const collection = createCollectionItem(req.body);
+ success(res, collection, 201);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getCollection(req, res) {
+ let { name } = req.params;
+ let { raw } = req.query;
+ const allCols = $.read(COLLECTIONS_KEY);
+ const collection = findByName(allCols, name);
+ if (collection) {
+ if (raw) {
+ res.set('content-type', 'application/json')
+ .set(
+ 'content-disposition',
+ `attachment; filename="${encodeURIComponent(
+ `sub-store_collection_${name}_${formatDateTime(
+ new Date(),
+ )}.json`,
+ )}"`,
+ )
+ .send(JSON.stringify(collection));
+ } else {
+ success(res, collection);
+ }
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ `SUBSCRIPTION_NOT_FOUND`,
+ `Collection ${name} does not exist`,
+ 404,
+ ),
+ );
+ }
+}
+
+function updateCollection(req, res) {
+ let { name } = req.params;
+ let collection = req.body;
+ const allCols = $.read(COLLECTIONS_KEY);
+ const oldCol = findByName(allCols, name);
+ if (oldCol) {
+ if (!collection.name) collection.name = oldCol.name;
+ const newCol = {
+ ...oldCol,
+ ...collection,
+ };
+ $.info(`正在更新组合订阅:${name}...`);
+
+ if (name !== newCol.name) {
+ // update all artifacts referring this collection
+ const allArtifacts = $.read(ARTIFACTS_KEY) || [];
+ for (const artifact of allArtifacts) {
+ if (
+ artifact.type === 'collection' &&
+ artifact.source === oldCol.name
+ ) {
+ artifact.source = newCol.name;
+ }
+ }
+ // update all files referring this collection
+ const allFiles = $.read(FILES_KEY) || [];
+ for (const file of allFiles) {
+ if (
+ file.sourceType === 'collection' &&
+ file.sourceName === oldCol.name
+ ) {
+ file.sourceName = newCol.name;
+ }
+ }
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ $.write(allFiles, FILES_KEY);
+ }
+
+ updateByName(allCols, name, newCol);
+ $.write(allCols, COLLECTIONS_KEY);
+ success(res, newCol);
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Collection ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+function deleteCollection(req, res) {
+ try {
+ let { name } = req.params;
+ $.info(`正在删除组合订阅:${name}`);
+ if (shouldArchiveDeletion(req.query.mode)) {
+ archiveCollection(name);
+ }
+ deleteCollectionItem(name);
+ success(res);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getAllCollections(req, res) {
+ const allCols = $.read(COLLECTIONS_KEY);
+ success(res, allCols);
+}
+
+function replaceCollection(req, res) {
+ const allCols = req.body;
+ $.write(allCols, COLLECTIONS_KEY);
+ success(res);
+}
+
+function createCollectionItem(collection) {
+ $.info(`正在创建组合订阅:${collection.name}`);
+ if (/\//.test(collection.name)) {
+ throw new RequestInvalidError(
+ 'INVALID_NAME',
+ `Collection ${collection.name} is invalid`,
+ );
+ }
+ const allCols = $.read(COLLECTIONS_KEY);
+ if (findByName(allCols, collection.name)) {
+ throw new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ `Collection ${collection.name} already exists.`,
+ );
+ }
+ insertByPosition(allCols, collection, getCreateItemPosition());
+ $.write(allCols, COLLECTIONS_KEY);
+ return collection;
+}
+
+function deleteCollectionItem(name) {
+ const allCols = $.read(COLLECTIONS_KEY);
+ const collection = findByName(allCols, name);
+ if (!collection) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Collection ${name} does not exist!`,
+ );
+ }
+ deleteByName(allCols, name);
+ $.write(allCols, COLLECTIONS_KEY);
+ return collection;
+}
+
+function shouldArchiveDeletion(mode) {
+ if (mode == null || mode === '' || mode === 'permanent') {
+ return false;
+ }
+ if (mode === 'archive') {
+ return true;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_DELETE_MODE',
+ `Unsupported delete mode: ${mode}`,
+ );
+}
+
+export { createCollectionItem, deleteCollectionItem };
diff --git a/backend/src/restful/download.js b/backend/src/restful/download.js
new file mode 100644
index 0000000000..3f45e7a52c
--- /dev/null
+++ b/backend/src/restful/download.js
@@ -0,0 +1,932 @@
+import { getPlatformFromHeaders } from '@/utils/user-agent';
+import { ProxyUtils } from '@/core/proxy-utils';
+import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
+import { findByName } from '@/utils/database';
+import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
+import $ from '@/core/app';
+import { failed } from '@/restful/response';
+import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
+import { produceArtifact } from '@/restful/sync';
+// eslint-disable-next-line no-unused-vars
+import { isIPv4, isIPv6 } from '@/utils';
+import { getISO } from '@/utils/geo';
+import env from '@/utils/env';
+import { applyResponseTransformers } from '@/restful/response-transformer';
+
+function buildEmptyNezhaPayload() {
+ return JSON.stringify(
+ {
+ code: 0,
+ message: 'success',
+ result: [],
+ },
+ null,
+ 2,
+ );
+}
+
+function getMihomoExternalOptions(query) {
+ const useMihomoExternal = query.target === 'SurgeMac';
+ const mihomoExternal = useMihomoExternal ? query.mihomoExternal : undefined;
+ const mihomoMerge = useMihomoExternal ? query.mihomoMerge : undefined;
+ const mihomoMergeName = useMihomoExternal
+ ? query.mihomoMergeName
+ : undefined;
+
+ return {
+ useMihomoExternal,
+ mihomoExternal,
+ mihomoMerge,
+ mihomoMergeName,
+ };
+}
+
+export default function register($app) {
+ $app.get('/share/col/:name/:target', async (req, res) => {
+ const { target } = req.params;
+ if (target) {
+ req.query.target = target;
+ $.info(`使用路由指定目标: ${target}`);
+ }
+ await downloadCollection(req, res);
+ });
+ $app.get('/share/col/:name', downloadCollection);
+ $app.get('/share/sub/:name/:target', async (req, res) => {
+ const { target } = req.params;
+ if (target) {
+ req.query.target = target;
+ $.info(`使用路由指定目标: ${target}`);
+ }
+ await downloadSubscription(req, res);
+ });
+ $app.get('/share/sub/:name', downloadSubscription);
+
+ $app.get('/download/collection/:name/:target', async (req, res) => {
+ const { target } = req.params;
+ if (target) {
+ req.query.target = target;
+ $.info(`使用路由指定目标: ${target}`);
+ }
+ await downloadCollection(req, res);
+ });
+ $app.get('/download/collection/:name', downloadCollection);
+ $app.get('/download/:name/:target', async (req, res) => {
+ const { target } = req.params;
+ if (target) {
+ req.query.target = target;
+ $.info(`使用路由指定目标: ${target}`);
+ }
+ await downloadSubscription(req, res);
+ });
+ $app.get('/download/:name', downloadSubscription);
+
+ $app.get(
+ '/download/collection/:name/api/v1/server/details',
+ async (req, res) => {
+ req.query.platform = 'JSON';
+ req.query.produceType = 'internal';
+ req.query.resultFormat = 'nezha';
+ await downloadCollection(req, res);
+ },
+ );
+ $app.get('/download/:name/api/v1/server/details', async (req, res) => {
+ req.query.platform = 'JSON';
+ req.query.produceType = 'internal';
+ req.query.resultFormat = 'nezha';
+ await downloadSubscription(req, res);
+ });
+ $app.get(
+ '/download/collection/:name/api/v1/monitor/:nezhaIndex',
+ async (req, res) => {
+ req.query.platform = 'JSON';
+ req.query.produceType = 'internal';
+ req.query.resultFormat = 'nezha-monitor';
+ await downloadCollection(req, res);
+ },
+ );
+ $app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => {
+ req.query.platform = 'JSON';
+ req.query.produceType = 'internal';
+ req.query.resultFormat = 'nezha-monitor';
+ await downloadSubscription(req, res);
+ });
+}
+
+async function downloadSubscription(req, res) {
+ let { name, nezhaIndex } = req.params;
+
+ const { useMihomoExternal, mihomoMerge, mihomoMergeName, mihomoExternal } =
+ getMihomoExternalOptions(req.query);
+
+ const platform =
+ req.query.platform ||
+ req.query.target ||
+ getPlatformFromHeaders(req.headers) ||
+ 'JSON';
+ const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
+ $.info(
+ `正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
+ );
+ let {
+ url,
+ ua,
+ content,
+ mergeSources,
+ ignoreFailedRemoteSub,
+ produceType,
+ includeUnsupportedProxy,
+ resultFormat,
+ proxy,
+ noCache,
+ _fakeNode,
+ } = req.query;
+ const prettyYaml = req.query.prettyYaml ?? req.query['pretty-yaml'];
+
+ let $options = {
+ _req: {
+ method: req.method,
+ url: req.url,
+ path: req.path,
+ query: req.query,
+ params: req.params,
+ headers: req.headers,
+ body: req.body,
+ socket: {
+ remoteAddress: req.socket?.remoteAddress,
+ },
+ },
+ };
+ if (req.query.$options) {
+ let options = {};
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ options = JSON.parse(decodeURIComponent(req.query.$options));
+ } catch (e) {
+ for (const pair of req.query.$options.split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ options[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ $.info(`传入 $options: ${JSON.stringify(options)}`);
+ Object.assign($options, options);
+ }
+ if (url) {
+ $.info(`指定远程订阅 URL: ${url}`);
+ if (!/^https?:\/\//.test(url)) {
+ content = url;
+ $.info(`URL 不是链接,视为本地订阅`);
+ }
+ }
+ if (content) {
+ $.info(`指定本地订阅: ${content}`);
+ }
+ if (proxy) {
+ $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
+ }
+ if (ua) {
+ $.info(`指定远程订阅 User-Agent: ${ua}`);
+ }
+
+ if (mergeSources) {
+ $.info(`指定合并来源: ${mergeSources}`);
+ }
+ if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
+ $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
+ }
+ if (produceType) {
+ $.info(`指定生产类型: ${produceType}`);
+ }
+ if (includeUnsupportedProxy) {
+ $.info(`包含官方/商店版不支持的协议: ${includeUnsupportedProxy}`);
+ }
+ if (prettyYaml) {
+ $.info(`指定输出易读 YAML: ${prettyYaml}`);
+ }
+ if (mihomoMerge) {
+ $.info(`指定合并 Mihomo External: ${mihomoMerge}`);
+ }
+ if (mihomoMergeName) {
+ $.info(`指定合并 Mihomo External 名称: ${mihomoMergeName}`);
+ }
+
+ if (useMihomoExternal) {
+ $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
+ }
+ if (mihomoExternal) {
+ $.info(`手动指定了 Mihomo External 链接参数: ${mihomoExternal}`);
+ }
+
+ if (noCache) {
+ $.info(`指定不使用缓存: ${noCache}`);
+ }
+
+ const allSubs = $.read(SUBS_KEY);
+ const fakeSub = {
+ name: 'fakeNodeInfo',
+ source: 'local',
+ content:
+ 'invalid share = ss, 1.0.0.1, 80, encrypt-method=aes-128-gcm, password=password',
+ };
+ const sub = _fakeNode ? fakeSub : findByName(allSubs, name);
+ if (sub) {
+ try {
+ const passThroughUA = sub.passThroughUA;
+ if (passThroughUA) {
+ $.info(
+ `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
+ );
+ ua = reqUA;
+ }
+ const opt = {
+ type: 'subscription',
+ name,
+ platform,
+ url,
+ ua,
+ content,
+ mergeSources,
+ ignoreFailedRemoteSub,
+ produceType,
+ produceOpts: {
+ 'include-unsupported-proxy': includeUnsupportedProxy,
+ useMihomoExternal,
+ merge: mihomoMerge,
+ mergeName: mihomoMergeName,
+ mihomoExternal,
+ prettyYaml,
+ },
+ $options,
+ proxy,
+ noCache,
+ };
+ if (_fakeNode) {
+ $.info(`返回假节点信息`);
+ delete opt.name;
+ opt.subscription = fakeSub;
+ }
+ let output = await produceArtifact(opt);
+ let flowInfo;
+ if (
+ sub.source !== 'local' ||
+ ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
+ ) {
+ try {
+ url =
+ `${url || sub.url}`
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)?.[0] || '';
+
+ let $arguments = {};
+ const rawArgs = url.split('#');
+ url = url.split('#')[0];
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(
+ decodeURIComponent(rawArgs[1]),
+ );
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ if (!$arguments.noFlow && /^https?/.test(url)) {
+ // forward flow headers
+ flowInfo = await getFlowHeaders(
+ $arguments?.insecure ? `${url}#insecure` : url,
+ $arguments.flowUserAgent,
+ undefined,
+ proxy || sub.proxy,
+ $arguments.flowUrl,
+ $arguments.flowHeaders,
+ );
+ if (flowInfo) {
+ const headers = normalizeFlowHeader(flowInfo, true);
+ if (headers?.['subscription-userinfo']) {
+ res.set(
+ 'subscription-userinfo',
+ headers['subscription-userinfo'],
+ );
+ }
+ if (headers?.['profile-web-page-url']) {
+ res.set(
+ 'profile-web-page-url',
+ headers['profile-web-page-url'],
+ );
+ }
+ if (headers?.['plan-name']) {
+ res.set('plan-name', headers['plan-name']);
+ }
+ }
+ }
+ } catch (err) {
+ $.error(
+ `订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
+ err,
+ )}`,
+ );
+ }
+ }
+ if (sub.subUserinfo) {
+ let subUserInfo;
+ if (/^https?:\/\//.test(sub.subUserinfo)) {
+ try {
+ subUserInfo = await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ proxy || sub.proxy,
+ sub.subUserinfo,
+ );
+ } catch (e) {
+ $.error(
+ `订阅 ${name} 使用自定义流量链接 ${
+ sub.subUserinfo
+ } 获取流量信息时发生错误: ${JSON.stringify(e)}`,
+ );
+ }
+ } else {
+ subUserInfo = sub.subUserinfo;
+ }
+
+ const headers = normalizeFlowHeader(
+ [subUserInfo, flowInfo].filter((i) => i).join(';'),
+ true,
+ );
+ if (headers?.['subscription-userinfo']) {
+ res.set(
+ 'subscription-userinfo',
+ headers['subscription-userinfo'],
+ );
+ }
+ if (headers?.['profile-web-page-url']) {
+ res.set(
+ 'profile-web-page-url',
+ headers['profile-web-page-url'],
+ );
+ }
+ if (headers?.['plan-name']) {
+ res.set('plan-name', headers['plan-name']);
+ }
+ }
+
+ if (platform === 'JSON') {
+ if (resultFormat === 'nezha') {
+ output = nezhaTransform(output);
+ } else if (resultFormat === 'nezha-monitor') {
+ if (!Array.isArray(output) || output.length === 0) {
+ output = buildEmptyNezhaPayload();
+ } else {
+ nezhaIndex = /^\d+$/.test(nezhaIndex)
+ ? parseInt(nezhaIndex, 10)
+ : output.findIndex((i) => i.name === nezhaIndex);
+ output = await nezhaMonitor(
+ output[nezhaIndex],
+ nezhaIndex,
+ req.query,
+ );
+ }
+ }
+ res.set('Content-Type', 'application/json;charset=utf-8');
+ } else {
+ res.set('Content-Type', 'text/plain; charset=utf-8');
+ }
+ if ($options?._res?.headers) {
+ Object.entries($options._res.headers).forEach(
+ ([key, value]) => {
+ if (value == null) {
+ res.removeHeader(key);
+ } else {
+ res.set(key, value);
+ }
+ },
+ );
+ }
+ if ($options?._res?.status) {
+ res.status($options._res.status);
+ }
+ const body = await applyResponseTransformers({
+ res,
+ body: output,
+ process: sub.process,
+ targetPlatform: platform,
+ source: { [sub.name]: sub },
+ $options,
+ });
+ res.send(body);
+ } catch (err) {
+ $.notify(
+ `🌍 Sub-Store 下载订阅失败`,
+ `❌ 无法下载订阅:${name}!`,
+ `🤔 原因:${err.message ?? err}`,
+ );
+ $.error(err.message ?? err);
+ failed(
+ res,
+ new InternalServerError(
+ 'INTERNAL_SERVER_ERROR',
+ `Failed to download subscription: ${name}`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+ } else {
+ $.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}!`);
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Subscription ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+async function downloadCollection(req, res) {
+ let { name, nezhaIndex } = req.params;
+
+ const { useMihomoExternal, mihomoMerge, mihomoMergeName, mihomoExternal } =
+ getMihomoExternalOptions(req.query);
+
+ const platform =
+ req.query.platform ||
+ req.query.target ||
+ getPlatformFromHeaders(req.headers) ||
+ 'JSON';
+
+ const allCols = $.read(COLLECTIONS_KEY);
+ const collection = findByName(allCols, name);
+ const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
+ $.info(
+ `正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
+ );
+ let {
+ ignoreFailedRemoteSub,
+ produceType,
+ includeUnsupportedProxy,
+ resultFormat,
+ proxy,
+ noCache,
+ } = req.query;
+ const prettyYaml = req.query.prettyYaml ?? req.query['pretty-yaml'];
+
+ let $options = {
+ _req: {
+ method: req.method,
+ url: req.url,
+ path: req.path,
+ query: req.query,
+ params: req.params,
+ headers: req.headers,
+ body: req.body,
+ socket: {
+ remoteAddress: req.socket?.remoteAddress,
+ },
+ },
+ };
+ if (req.query.$options) {
+ let options = {};
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ options = JSON.parse(decodeURIComponent(req.query.$options));
+ } catch (e) {
+ for (const pair of req.query.$options.split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ options[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ $.info(`传入 $options: ${JSON.stringify(options)}`);
+ Object.assign($options, options);
+ }
+
+ if (proxy) {
+ $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
+ }
+
+ if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
+ $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
+ }
+ if (produceType) {
+ $.info(`指定生产类型: ${produceType}`);
+ }
+
+ if (includeUnsupportedProxy) {
+ $.info(`包含官方/商店版不支持的协议: ${includeUnsupportedProxy}`);
+ }
+ if (prettyYaml) {
+ $.info(`指定输出易读 YAML: ${prettyYaml}`);
+ }
+ if (mihomoMerge) {
+ $.info(`指定合并 Mihomo External: ${mihomoMerge}`);
+ }
+ if (mihomoMergeName) {
+ $.info(`指定合并 Mihomo External 名称: ${mihomoMergeName}`);
+ }
+
+ if (useMihomoExternal) {
+ $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
+ }
+ if (mihomoExternal) {
+ $.info(`手动指定了 Mihomo External 链接参数: ${mihomoExternal}`);
+ }
+ if (noCache) {
+ $.info(`指定不使用缓存: ${noCache}`);
+ }
+
+ if (collection) {
+ try {
+ let output = await produceArtifact({
+ type: 'collection',
+ name,
+ platform,
+ ignoreFailedRemoteSub,
+ produceType,
+ produceOpts: {
+ 'include-unsupported-proxy': includeUnsupportedProxy,
+ useMihomoExternal,
+ merge: mihomoMerge,
+ mergeName: mihomoMergeName,
+ mihomoExternal,
+ prettyYaml,
+ },
+ $options,
+ proxy,
+ noCache,
+ ua: reqUA,
+ });
+ let subUserInfoOfSub;
+ // 默认透传第一个子订阅的流量信息,除非 firstSubFlow 显式设置为 false
+ if (collection.firstSubFlow !== false) {
+ // forward flow header from the first subscription in this collection
+ const allSubs = $.read(SUBS_KEY);
+ const subnames = collection.subscriptions;
+ if (subnames.length > 0) {
+ const sub = findByName(allSubs, subnames[0]);
+ if (
+ sub.source !== 'local' ||
+ ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
+ ) {
+ try {
+ let url =
+ `${sub.url}`
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)?.[0] || '';
+
+ let $arguments = {};
+ const rawArgs = url.split('#');
+ url = url.split('#')[0];
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(
+ decodeURIComponent(rawArgs[1]),
+ );
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ if (!$arguments.noFlow && /^https?:/.test(url)) {
+ subUserInfoOfSub = await getFlowHeaders(
+ $arguments?.insecure
+ ? `${url}#insecure`
+ : url,
+ $arguments.flowUserAgent,
+ undefined,
+ proxy || sub.proxy || collection.proxy,
+ $arguments.flowUrl,
+ $arguments.flowHeaders,
+ );
+ }
+ } catch (err) {
+ $.error(
+ `组合订阅 ${name} 中的子订阅 ${
+ sub.name
+ } 获取流量信息时发生错误: ${
+ err.message ?? err
+ }`,
+ );
+ }
+ }
+ if (sub.subUserinfo) {
+ let subUserInfo;
+ if (/^https?:\/\//.test(sub.subUserinfo)) {
+ try {
+ subUserInfo = await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ proxy || sub.proxy,
+ sub.subUserinfo,
+ );
+ } catch (e) {
+ $.error(
+ `组合订阅 ${name} 使用自定义流量链接 ${
+ sub.subUserinfo
+ } 获取流量信息时发生错误: ${JSON.stringify(
+ e,
+ )}`,
+ );
+ }
+ } else {
+ subUserInfo = sub.subUserinfo;
+ }
+ subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
+ .filter((i) => i)
+ .join('; ');
+ }
+ }
+
+ $.info(
+ `组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`,
+ );
+ }
+
+ let subUserInfoOfCol;
+ if (/^https?:\/\//.test(collection.subUserinfo)) {
+ try {
+ subUserInfoOfCol = await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ proxy || collection.proxy,
+ collection.subUserinfo,
+ );
+ } catch (e) {
+ $.error(
+ `组合订阅 ${name} 使用自定义流量链接 ${
+ collection.subUserinfo
+ } 获取流量信息时发生错误: ${JSON.stringify(e)}`,
+ );
+ }
+ } else {
+ subUserInfoOfCol = collection.subUserinfo;
+ }
+ const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
+ .filter((i) => i)
+ .join('; ');
+ if (subUserInfo) {
+ const headers = normalizeFlowHeader(subUserInfo, true);
+ if (headers?.['subscription-userinfo']) {
+ res.set(
+ 'subscription-userinfo',
+ headers['subscription-userinfo'],
+ );
+ }
+ if (headers?.['profile-web-page-url']) {
+ res.set(
+ 'profile-web-page-url',
+ headers['profile-web-page-url'],
+ );
+ }
+ if (headers?.['plan-name']) {
+ res.set('plan-name', headers['plan-name']);
+ }
+ }
+ if (platform === 'JSON') {
+ if (resultFormat === 'nezha') {
+ output = nezhaTransform(output);
+ } else if (resultFormat === 'nezha-monitor') {
+ if (!Array.isArray(output) || output.length === 0) {
+ output = buildEmptyNezhaPayload();
+ } else {
+ nezhaIndex = /^\d+$/.test(nezhaIndex)
+ ? parseInt(nezhaIndex, 10)
+ : output.findIndex((i) => i.name === nezhaIndex);
+ output = await nezhaMonitor(
+ output[nezhaIndex],
+ nezhaIndex,
+ req.query,
+ );
+ }
+ }
+ res.set('Content-Type', 'application/json;charset=utf-8');
+ } else {
+ res.set('Content-Type', 'text/plain; charset=utf-8');
+ }
+ if ($options?._res?.headers) {
+ Object.entries($options._res.headers).forEach(
+ ([key, value]) => {
+ if (value == null) {
+ res.removeHeader(key);
+ } else {
+ res.set(key, value);
+ }
+ },
+ );
+ }
+ if ($options?._res?.status) {
+ res.status($options._res.status);
+ }
+ const body = await applyResponseTransformers({
+ res,
+ body: output,
+ process: collection.process,
+ targetPlatform: platform,
+ source: { _collection: collection },
+ $options,
+ });
+ res.send(body);
+ } catch (err) {
+ $.notify(
+ `🌍 Sub-Store 下载组合订阅失败`,
+ `❌ 下载组合订阅错误:${name}!`,
+ `🤔 原因:${err}`,
+ );
+ failed(
+ res,
+ new InternalServerError(
+ 'INTERNAL_SERVER_ERROR',
+ `Failed to download collection: ${name}`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+ } else {
+ $.error(
+ `🌍 Sub-Store 下载组合订阅失败`,
+ `❌ 未找到组合订阅:${name}!`,
+ );
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Collection ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+async function nezhaMonitor(proxy, index, query) {
+ const result = {
+ code: 0,
+ message: 'success',
+ result: [],
+ };
+
+ try {
+ const { isLoon, isSurge } = $.env;
+ if (!isLoon && !isSurge)
+ throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)');
+ const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge');
+ if (!node) throw new Error('当前客户端不兼容此节点');
+ const monitors = proxy._monitors || [
+ {
+ name: 'Cloudflare',
+ url: 'http://cp.cloudflare.com/generate_204',
+ method: 'HEAD',
+ number: 3,
+ timeout: 2000,
+ },
+ {
+ name: 'Google',
+ url: 'http://www.google.com/generate_204',
+ method: 'HEAD',
+ number: 3,
+ timeout: 2000,
+ },
+ ];
+ const number =
+ query.number || Math.max(...monitors.map((i) => i.number)) || 3;
+ for (const monitor of monitors) {
+ const interval = 10 * 60 * 1000;
+ const data = {
+ monitor_id: monitors.indexOf(monitor),
+ server_id: index,
+ monitor_name: monitor.name,
+ server_name: proxy.name,
+ created_at: [],
+ avg_delay: [],
+ };
+ for (let index = 0; index < number; index++) {
+ const startedAt = Date.now();
+ try {
+ await $.http[(monitor.method || 'HEAD').toLowerCase()]({
+ timeout: monitor.timeout || 2000,
+ url: monitor.url,
+ 'policy-descriptor': node,
+ node,
+ });
+ const latency = Date.now() - startedAt;
+ $.info(`${monitor.name} latency: ${latency}`);
+ data.avg_delay.push(latency);
+ } catch (e) {
+ $.error(e);
+ data.avg_delay.push(0);
+ }
+
+ data.created_at.push(
+ Date.now() - interval * (monitor.number - index - 1),
+ );
+ }
+
+ result.result.push(data);
+ }
+ } catch (e) {
+ $.error(e);
+ result.result.push({
+ monitor_id: 0,
+ server_id: 0,
+ monitor_name: `❌ ${e.message ?? e}`,
+ server_name: proxy.name,
+ created_at: [Date.now()],
+ avg_delay: [0],
+ });
+ }
+
+ return JSON.stringify(result, null, 2);
+}
+function nezhaTransform(output) {
+ const result = {
+ code: 0,
+ message: 'success',
+ result: [],
+ };
+ output.map((proxy, index) => {
+ // 如果节点上有数据 就取节点上的数据
+ let CountryCode = proxy._geo?.countryCode || proxy._geo?.country;
+ // 简单判断下
+ if (!/^[a-z]{2}$/i.test(CountryCode)) {
+ CountryCode = getISO(proxy.name);
+ }
+ // 简单判断下
+ if (/^[a-z]{2}$/i.test(CountryCode)) {
+ // 如果节点上有数据 就取节点上的数据
+ let now = Math.round(new Date().getTime() / 1000);
+ let time = proxy._unavailable ? 0 : now;
+
+ const uptime = parseInt(proxy._uptime || 0, 10);
+
+ result.result.push({
+ id: index,
+ name: proxy.name,
+ tag: `${proxy._tag ?? ''}`,
+ last_active: time,
+ // 暂时不用处理 现在 VPings App 端的接口支持域名查询
+ // 其他场景使用 自己在 Sub-Store 加一步域名解析
+ valid_ip: proxy._IP || proxy.server,
+ ipv4: proxy._IPv4 || proxy.server,
+ ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''),
+ host: {
+ Platform: 'Sub-Store',
+ PlatformVersion: env.version,
+ CPU: [],
+ MemTotal: 1024,
+ DiskTotal: 1024,
+ SwapTotal: 1024,
+ Arch: '',
+ Virtualization: '',
+ BootTime: now - uptime,
+ CountryCode, // 目前需要
+ Version: '0.0.1',
+ },
+ status: {
+ CPU: 0,
+ MemUsed: 0,
+ SwapUsed: 0,
+ DiskUsed: 0,
+ NetInTransfer: 0,
+ NetOutTransfer: 0,
+ NetInSpeed: 0,
+ NetOutSpeed: 0,
+ Uptime: uptime,
+ Load1: 0,
+ Load5: 0,
+ Load15: 0,
+ TcpConnCount: 0,
+ UdpConnCount: 0,
+ ProcessCount: 0,
+ },
+ });
+ }
+ });
+ return JSON.stringify(result, null, 2);
+}
diff --git a/backend/src/restful/errors/index.js b/backend/src/restful/errors/index.js
new file mode 100644
index 0000000000..a1946b4e2a
--- /dev/null
+++ b/backend/src/restful/errors/index.js
@@ -0,0 +1,35 @@
+class BaseError {
+ constructor(code, message, details) {
+ this.code = code;
+ this.message = message;
+ this.details = details;
+ }
+}
+
+export class InternalServerError extends BaseError {
+ constructor(code, message, details) {
+ super(code, message, details);
+ this.type = 'InternalServerError';
+ }
+}
+
+export class RequestInvalidError extends BaseError {
+ constructor(code, message, details) {
+ super(code, message, details);
+ this.type = 'RequestInvalidError';
+ }
+}
+
+export class ResourceNotFoundError extends BaseError {
+ constructor(code, message, details) {
+ super(code, message, details);
+ this.type = 'ResourceNotFoundError';
+ }
+}
+
+export class NetworkError extends BaseError {
+ constructor(code, message, details) {
+ super(code, message, details);
+ this.type = 'NetworkError';
+ }
+}
diff --git a/backend/src/restful/file.js b/backend/src/restful/file.js
new file mode 100644
index 0000000000..9fa84b7646
--- /dev/null
+++ b/backend/src/restful/file.js
@@ -0,0 +1,396 @@
+import {
+ deleteByName,
+ findByName,
+ insertByPosition,
+ updateByName,
+} from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
+import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
+import { failed, success } from '@/restful/response';
+import $ from '@/core/app';
+import {
+ RequestInvalidError,
+ ResourceNotFoundError,
+ InternalServerError,
+} from '@/restful/errors';
+import { produceArtifact } from '@/restful/sync';
+import { archiveFile } from '@/utils/archive';
+import { formatDateTime } from '@/utils';
+import { applyResponseTransformers } from '@/restful/response-transformer';
+
+export default function register($app) {
+ if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
+
+ $app.get('/share/file/:name', getFile);
+
+ $app.route('/api/file/:name')
+ .get(getFile)
+ .patch(updateFile)
+ .delete(deleteFile);
+
+ $app.route('/api/wholeFile/:name').get(getWholeFile);
+
+ $app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
+ $app.route('/api/wholeFiles').get(getAllWholeFiles);
+}
+
+// file API
+function createFile(req, res) {
+ try {
+ const file = createFileItem(req.body);
+ success(res, file, 201);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+async function getFile(req, res, next) {
+ let { name } = req.params;
+ const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
+ $.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
+ let {
+ url,
+ subInfoUrl,
+ subInfoUserAgent,
+ ua,
+ content,
+ mergeSources,
+ ignoreFailedRemoteFile,
+ proxy,
+ noCache,
+ produceType,
+ } = req.query;
+ let $options = {
+ _req: {
+ method: req.method,
+ url: req.url,
+ path: req.path,
+ query: req.query,
+ params: req.params,
+ headers: req.headers,
+ body: req.body,
+ socket: {
+ remoteAddress: req.socket?.remoteAddress,
+ },
+ },
+ };
+ if (req.query.$options) {
+ let options = {};
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ options = JSON.parse(decodeURIComponent(req.query.$options));
+ } catch (e) {
+ for (const pair of req.query.$options.split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ options[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ $.info(`传入 $options: ${JSON.stringify(options)}`);
+ Object.assign($options, options);
+ }
+ if (url) {
+ $.info(`指定远程文件 URL: ${url}`);
+ }
+ if (proxy) {
+ $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
+ }
+ if (ua) {
+ $.info(`指定远程文件 User-Agent: ${ua}`);
+ }
+ if (subInfoUrl) {
+ $.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
+ }
+ if (subInfoUserAgent) {
+ $.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
+ }
+ if (content) {
+ $.info(`指定本地文件: ${content}`);
+ }
+ if (mergeSources) {
+ $.info(`指定合并来源: ${mergeSources}`);
+ }
+ if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
+ $.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
+ }
+ if (noCache) {
+ $.info(`指定不使用缓存: ${noCache}`);
+ }
+ if (produceType) {
+ $.info(`指定生产类型: ${produceType}`);
+ }
+
+ const allFiles = $.read(FILES_KEY);
+ const file = findByName(allFiles, name);
+ if (file) {
+ try {
+ const output = await produceArtifact({
+ type: 'file',
+ name,
+ url,
+ ua,
+ content,
+ mergeSources,
+ ignoreFailedRemoteFile,
+ $options,
+ proxy,
+ noCache,
+ produceType,
+ all: true,
+ });
+
+ try {
+ subInfoUrl = subInfoUrl || file.subInfoUrl;
+ if (subInfoUrl) {
+ // forward flow headers
+ const flowInfo = await getFlowHeaders(
+ subInfoUrl,
+ subInfoUserAgent || file.subInfoUserAgent,
+ undefined,
+ proxy || file.proxy,
+ );
+ if (flowInfo) {
+ const headers = normalizeFlowHeader(flowInfo, true);
+ if (headers?.['subscription-userinfo']) {
+ res.set(
+ 'subscription-userinfo',
+ headers['subscription-userinfo'],
+ );
+ }
+ if (headers?.['profile-web-page-url']) {
+ res.set(
+ 'profile-web-page-url',
+ headers['profile-web-page-url'],
+ );
+ }
+ if (headers?.['plan-name']) {
+ res.set('plan-name', headers['plan-name']);
+ }
+ }
+ }
+ } catch (err) {
+ $.error(
+ `文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
+ err,
+ )}`,
+ );
+ }
+ if (file.download) {
+ res.set(
+ 'Content-Disposition',
+ `attachment; filename*=UTF-8''${encodeURIComponent(
+ file.displayName || file.name,
+ )}`,
+ );
+ }
+ res.set('Content-Type', 'text/plain; charset=utf-8');
+ if (output?.$options?._res?.headers) {
+ Object.entries(output.$options._res.headers).forEach(
+ ([key, value]) => {
+ if (value == null) {
+ res.removeHeader(key);
+ } else {
+ res.set(key, value);
+ }
+ },
+ );
+ }
+ if (output?.$options?._res?.status) {
+ res.status(output.$options._res.status);
+ }
+ const body = await applyResponseTransformers({
+ res,
+ body: output?.$content ?? '',
+ process: file.process,
+ source: { $file: file },
+ $options: output?.$options ?? $options,
+ });
+ res.send(body);
+ } catch (err) {
+ $.notify(
+ `🌍 Sub-Store 下载文件失败`,
+ `❌ 无法下载文件:${name}!`,
+ `🤔 原因:${err.message ?? err}`,
+ );
+ $.error(err.message ?? err);
+ failed(
+ res,
+ new InternalServerError(
+ 'INTERNAL_SERVER_ERROR',
+ `Failed to download file: ${name}`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+ } else {
+ $.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}!`);
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `File ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+function getWholeFile(req, res) {
+ let { name } = req.params;
+ let { raw } = req.query;
+ const allFiles = $.read(FILES_KEY);
+ const file = findByName(allFiles, name);
+ if (file) {
+ if (raw) {
+ res.set('content-type', 'application/json')
+ .set(
+ 'content-disposition',
+ `attachment; filename="${encodeURIComponent(
+ `sub-store_file_${name}_${formatDateTime(
+ new Date(),
+ )}.json`,
+ )}"`,
+ )
+ .send(JSON.stringify(file));
+ } else {
+ success(res, file);
+ }
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ `FILE_NOT_FOUND`,
+ `File ${name} does not exist`,
+ 404,
+ ),
+ );
+ }
+}
+
+function updateFile(req, res) {
+ let { name } = req.params;
+ let file = req.body;
+ const allFiles = $.read(FILES_KEY);
+ const oldFile = findByName(allFiles, name);
+ if (oldFile) {
+ if (!file.name) file.name = oldFile.name;
+ const newFile = {
+ ...oldFile,
+ ...file,
+ };
+ $.info(`正在更新文件:${name}...`);
+
+ if (name !== newFile.name) {
+ // update all artifacts referring this collection
+ const allArtifacts = $.read(ARTIFACTS_KEY) || [];
+ for (const artifact of allArtifacts) {
+ if (
+ artifact.type === 'file' &&
+ artifact.source === oldFile.name
+ ) {
+ artifact.source = newFile.name;
+ }
+ }
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ }
+
+ updateByName(allFiles, name, newFile);
+ $.write(allFiles, FILES_KEY);
+ success(res, newFile);
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `File ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+function deleteFile(req, res) {
+ try {
+ let { name } = req.params;
+ $.info(`正在删除文件:${name}`);
+ if (shouldArchiveDeletion(req.query.mode)) {
+ archiveFile(name);
+ }
+ deleteFileItem(name);
+ success(res);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getAllFiles(req, res) {
+ const allFiles = $.read(FILES_KEY);
+ success(
+ res, // eslint-disable-next-line no-unused-vars
+ allFiles.map(({ content, ...rest }) => rest),
+ );
+}
+
+function getAllWholeFiles(req, res) {
+ const allFiles = $.read(FILES_KEY);
+ success(res, allFiles);
+}
+
+function replaceFile(req, res) {
+ const allFiles = req.body;
+ $.write(allFiles, FILES_KEY);
+ success(res);
+}
+
+function createFileItem(rawFile) {
+ const file = {
+ ...rawFile,
+ };
+ file.name = `${file.name ?? Date.now()}`;
+ $.info(`正在创建文件:${file.name}`);
+ const allFiles = $.read(FILES_KEY);
+ if (findByName(allFiles, file.name)) {
+ throw new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ rawFile.name
+ ? `已存在 name 为 ${file.name} 的文件`
+ : `无法同时创建相同的文件 可稍后重试`,
+ );
+ }
+ insertByPosition(allFiles, file, getCreateItemPosition());
+ $.write(allFiles, FILES_KEY);
+ return file;
+}
+
+function deleteFileItem(name) {
+ const allFiles = $.read(FILES_KEY);
+ const file = findByName(allFiles, name);
+ if (!file) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `File ${name} does not exist!`,
+ );
+ }
+ deleteByName(allFiles, name);
+ $.write(allFiles, FILES_KEY);
+ return file;
+}
+
+function shouldArchiveDeletion(mode) {
+ if (mode == null || mode === '' || mode === 'permanent') {
+ return false;
+ }
+ if (mode === 'archive') {
+ return true;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_DELETE_MODE',
+ `Unsupported delete mode: ${mode}`,
+ );
+}
+
+export { createFileItem, deleteFileItem };
diff --git a/backend/src/restful/ignore-failed-remote-sub.js b/backend/src/restful/ignore-failed-remote-sub.js
new file mode 100644
index 0000000000..23f3b5c36c
--- /dev/null
+++ b/backend/src/restful/ignore-failed-remote-sub.js
@@ -0,0 +1,80 @@
+import { ProxyUtils } from '@/core/proxy-utils';
+
+const IGNORE_FAILED_REMOTE_SUB_NOTIFY_MODES = new Set([
+ 'enabled',
+ 'fallbackNotify',
+]);
+const IGNORE_FAILED_REMOTE_SUB_FALLBACK_MODES = new Set([
+ 'fallbackNotify',
+ 'fallbackQuiet',
+]);
+
+function normalizeIgnoreFailedRemoteSub(mode) {
+ if (mode === true) return 'quiet';
+ if (mode === false || mode == null || mode === '') return 'disabled';
+ return mode;
+}
+
+function resolveIgnoreFailedRemoteSubMode(...values) {
+ for (const value of values) {
+ if (value != null && value !== '') {
+ return normalizeIgnoreFailedRemoteSub(value);
+ }
+ }
+
+ return 'disabled';
+}
+
+function shouldNotifyIgnoreFailedRemoteSub(mode) {
+ return IGNORE_FAILED_REMOTE_SUB_NOTIFY_MODES.has(
+ normalizeIgnoreFailedRemoteSub(mode),
+ );
+}
+
+function shouldFallbackIgnoreFailedRemoteSub(mode) {
+ return IGNORE_FAILED_REMOTE_SUB_FALLBACK_MODES.has(
+ normalizeIgnoreFailedRemoteSub(mode),
+ );
+}
+
+function handleIgnoreFailedRemoteSubError({ mode, message, notify }) {
+ const resolvedMode = normalizeIgnoreFailedRemoteSub(mode);
+
+ if (
+ resolvedMode === 'disabled' ||
+ shouldFallbackIgnoreFailedRemoteSub(resolvedMode)
+ ) {
+ throw new Error(message);
+ }
+
+ if (shouldNotifyIgnoreFailedRemoteSub(resolvedMode)) {
+ notify?.(message);
+ }
+}
+
+function notifyIgnoreFailedRemoteSubFallback({ mode, notify, error }) {
+ if (!shouldNotifyIgnoreFailedRemoteSub(mode)) return;
+ notify?.(error);
+}
+
+function buildEmptySubscriptionOutput({
+ platform = 'JSON',
+ produceType,
+ produceOpts = {},
+}) {
+ if (produceType === 'raw') {
+ return JSON.stringify([]);
+ }
+
+ return ProxyUtils.produce([], platform, produceType, produceOpts);
+}
+
+export {
+ buildEmptySubscriptionOutput,
+ handleIgnoreFailedRemoteSubError,
+ normalizeIgnoreFailedRemoteSub,
+ notifyIgnoreFailedRemoteSubFallback,
+ resolveIgnoreFailedRemoteSubMode,
+ shouldFallbackIgnoreFailedRemoteSub,
+ shouldNotifyIgnoreFailedRemoteSub,
+};
diff --git a/backend/src/restful/index.js b/backend/src/restful/index.js
new file mode 100644
index 0000000000..cdd698acca
--- /dev/null
+++ b/backend/src/restful/index.js
@@ -0,0 +1,526 @@
+import { Base64 } from 'js-base64';
+import _ from 'lodash';
+import express from '@/vendor/express';
+import $ from '@/core/app';
+import migrate from '@/utils/migration';
+import download, { downloadFile } from '@/utils/download';
+import {
+ syncArtifacts,
+ produceArtifact,
+ syncArtifactItem,
+} from '@/restful/sync';
+import { gistBackupAction } from '@/restful/miscs';
+import { TOKENS_KEY, SETTINGS_KEY } from '@/constants';
+import { startArtifactCronJobs } from '@/utils/artifact-cron';
+
+import registerSubscriptionRoutes from './subscriptions';
+import registerCollectionRoutes from './collections';
+import registerArtifactRoutes from './artifacts';
+import registerFileRoutes from './file';
+import registerTokenRoutes from './token';
+import registerArchiveRoutes from './archives';
+import registerModuleRoutes from './module';
+import registerSyncRoutes from './sync';
+import registerDownloadRoutes from './download';
+import registerSettingRoutes from './settings';
+import registerPreviewRoutes from './preview';
+import registerSortingRoutes from './sort';
+import registerMiscRoutes from './miscs';
+import registerNodeInfoRoutes from './node-info';
+import registerParserRoutes from './parser';
+import registerLogRoutes from './logs';
+
+export default function serve() {
+ let port;
+ let host;
+ if ($.env.isNode) {
+ port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
+ host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
+ }
+ const $app = express({ substore: $, port, host });
+ if ($.env.isNode) {
+ const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
+ const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
+ const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
+ const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
+ if (be_prefix || be_merge) {
+ if (!fe_be_path.startsWith('/')) {
+ throw new Error(
+ 'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
+ );
+ }
+ if (be_merge) {
+ $.info(`[BACKEND] MERGE mode is [ON].`);
+ $.info(`[BACKEND && FRONTEND] ${host}:${port}`);
+ }
+ $.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
+ $app.use((req, res, next) => {
+ if (req.path.startsWith(fe_be_path)) {
+ req.url = req.url.replace(fe_be_path, '') || '/';
+ if (be_merge && req.url.startsWith('/api/')) {
+ req.query['share'] = 'true';
+ }
+ next();
+ return;
+ }
+ const pathname =
+ decodeURIComponent(req._parsedUrl.pathname) || '/';
+ if (
+ be_merge &&
+ req.path.startsWith('/share/') &&
+ req.query.token
+ ) {
+ if (req.method.toLowerCase() !== 'get') {
+ res.status(405).send('Method not allowed');
+ return;
+ }
+ const tokens = $.read(TOKENS_KEY) || [];
+ const token = tokens.find(
+ (t) =>
+ t.token === req.query.token &&
+ (`/share/${t.type}/${t.name}` === pathname ||
+ pathname.startsWith(
+ `/share/${t.type}/${t.name}/`,
+ )) &&
+ (t.exp == null || t.exp > Date.now()),
+ );
+ if (token) {
+ next();
+ return;
+ } else {
+ const settings = $.read(SETTINGS_KEY);
+ if (settings?.appearanceSetting?.invalidShareFakeNode) {
+ req.query._fakeNode = true;
+ req.url = req.url.replace(
+ /\/share\/.*?\//,
+ '/share/sub/',
+ );
+ next();
+ return;
+ }
+ }
+ }
+ const isBackendRoute = /^\/(api|download|share)(\/|$)/.test(
+ req.path,
+ );
+ if (be_merge && fe_path && !isBackendRoute) {
+ const express_ = eval(`require("express")`);
+ const mime_ = eval(`require("mime-types")`);
+ const path_ = eval(`require("path")`);
+ const fs_ = eval(`require("fs")`);
+ // 检查请求的文件是否真实存在,不存在则返回 index.html(SPA 路由)
+ const filePath = path_.join(fe_path, req.path);
+ if (!fs_.existsSync(filePath)) {
+ req.url = '/index.html';
+ }
+ const staticFileMiddleware = express_.static(fe_path, {
+ setHeaders: (res, path) => {
+ const type = mime_.contentType(path_.extname(path));
+ if (type) {
+ res.set('Content-Type', type);
+ }
+ },
+ });
+ staticFileMiddleware(req, res, next);
+ return;
+ }
+ res.status(404).end();
+ return;
+ });
+ }
+ }
+ // register routes
+ registerCollectionRoutes($app);
+ registerSubscriptionRoutes($app);
+ registerDownloadRoutes($app);
+ registerPreviewRoutes($app);
+ registerSortingRoutes($app);
+ registerSettingRoutes($app);
+ registerArtifactRoutes($app);
+ registerFileRoutes($app);
+ registerTokenRoutes($app);
+ registerArchiveRoutes($app);
+ registerModuleRoutes($app);
+ registerSyncRoutes($app);
+ registerNodeInfoRoutes($app);
+ registerMiscRoutes($app);
+ registerParserRoutes($app);
+ registerLogRoutes($app);
+
+ $app.start();
+
+ if ($.env.isNode) {
+ startArtifactCronJobs(syncArtifactItem);
+
+ // Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON
+ const backend_sync_cron = eval(
+ 'process.env.SUB_STORE_BACKEND_SYNC_CRON',
+ );
+
+ if (backend_sync_cron) {
+ $.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
+ const { CronJob } = eval(`require("cron")`);
+ new CronJob(
+ backend_sync_cron,
+ async function () {
+ try {
+ $.info(`[SYNC CRON] ${backend_sync_cron} started`);
+ await syncArtifacts({ skipCronArtifacts: true });
+ $.info(`[SYNC CRON] ${backend_sync_cron} finished`);
+ } catch (e) {
+ $.error(
+ `[SYNC CRON] ${backend_sync_cron} error: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ // 'Asia/Shanghai' // timeZone
+ );
+ } else {
+ if (eval('process.env.SUB_STORE_BACKEND_CRON')) {
+ $.error(
+ `[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
+ );
+ }
+ if (eval('process.env.SUB_STORE_CRON')) {
+ $.error(
+ `[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
+ );
+ }
+ }
+ // 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
+ // 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
+ const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
+ if (produce_cron) {
+ $.info(`[PRODUCE CRON] ${produce_cron} enabled`);
+ const { CronJob } = eval(`require("cron")`);
+ produce_cron
+ .split(/\s*;\s*/)
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0)
+ .forEach((item) => {
+ const [cron, type, name] = item.split(/\s*,\s*/);
+ $.info(`[PRODUCE CRON] ${type} ${name} ${cron} scheduled`);
+ new CronJob(
+ cron.trim(),
+ async function () {
+ try {
+ $.info(
+ `[PRODUCE CRON] ${type} ${name} ${cron} started`,
+ );
+ await produceArtifact({ type, name });
+ $.info(
+ `[PRODUCE CRON] ${type} ${name} ${cron} finished`,
+ );
+ } catch (e) {
+ $.error(
+ `[PRODUCE CRON] ${type} ${name} ${cron} error: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ // 'Asia/Shanghai' // timeZone
+ );
+ });
+ }
+ const backend_download_cron = eval(
+ 'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
+ );
+ if (backend_download_cron) {
+ $.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);
+ const { CronJob } = eval(`require("cron")`);
+ new CronJob(
+ backend_download_cron,
+ async function () {
+ try {
+ $.info(
+ `[DOWNLOAD CRON] ${backend_download_cron} started`,
+ );
+ await gistBackupAction('download');
+ $.info(
+ `[DOWNLOAD CRON] ${backend_download_cron} finished`,
+ );
+ } catch (e) {
+ $.error(
+ `[DOWNLOAD CRON] ${backend_download_cron} error: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ // 'Asia/Shanghai' // timeZone
+ );
+ }
+ const backend_upload_cron = eval(
+ 'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',
+ );
+ if (backend_upload_cron) {
+ $.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);
+ const { CronJob } = eval(`require("cron")`);
+ new CronJob(
+ backend_upload_cron,
+ async function () {
+ try {
+ $.info(`[UPLOAD CRON] ${backend_upload_cron} started`);
+ await gistBackupAction('upload');
+ $.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);
+ } catch (e) {
+ $.error(
+ `[UPLOAD CRON] ${backend_upload_cron} error: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ // 'Asia/Shanghai' // timeZone
+ );
+ }
+ const mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON');
+ const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
+ const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL');
+ const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH');
+ const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL');
+ if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) {
+ $.info(`[MMDB CRON] ${mmdb_cron} enabled`);
+ const { CronJob } = eval(`require("cron")`);
+ new CronJob(
+ mmdb_cron,
+ async function () {
+ try {
+ $.info(`[MMDB CRON] ${mmdb_cron} started`);
+ if (countryFile && countryUrl) {
+ try {
+ $.info(
+ `[MMDB CRON] downloading ${countryUrl} to ${countryFile}`,
+ );
+ await downloadFile(countryUrl, countryFile);
+ } catch (e) {
+ $.error(
+ `[MMDB CRON] ${countryUrl} download failed: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }
+ if (asnFile && asnUrl) {
+ try {
+ $.info(
+ `[MMDB CRON] downloading ${asnUrl} to ${asnFile}`,
+ );
+ await downloadFile(asnUrl, asnFile);
+ } catch (e) {
+ $.error(
+ `[MMDB CRON] ${asnUrl} download failed: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }
+
+ $.info(`[MMDB CRON] ${mmdb_cron} finished`);
+ } catch (e) {
+ $.error(
+ `[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`,
+ );
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ // 'Asia/Shanghai' // timeZone
+ );
+ }
+ const path = eval(`require("path")`);
+ const fs = eval(`require("fs")`);
+ const data_url = eval('process.env.SUB_STORE_DATA_URL');
+ const data_url_post = eval('process.env.SUB_STORE_DATA_URL_POST');
+ const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
+ const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
+ const fe_host =
+ eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
+ const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
+ const fe_abs_path = path.resolve(
+ fe_path || path.join(__dirname, 'frontend'),
+ );
+ const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
+ if (fe_path && !be_merge) {
+ try {
+ fs.accessSync(path.join(fe_abs_path, 'index.html'));
+ } catch (e) {
+ $.error(
+ `[FRONTEND] index.html file not found in ${fe_abs_path}`,
+ );
+ }
+
+ const express_ = eval(`require("express")`);
+ const history = eval(`require("connect-history-api-fallback")`);
+ const { createProxyMiddleware } = eval(
+ `require("http-proxy-middleware")`,
+ );
+
+ const app = express_();
+
+ const staticFileMiddleware = express_.static(fe_path);
+
+ let be_api = '/api/';
+ let be_download = '/download/';
+ let be_share = '/share/';
+ let be_download_rewrite = '';
+ let be_api_rewrite = '';
+ let be_share_rewrite = `${be_share}:type/:name`;
+ let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
+ ? fe_be_path
+ : '';
+ if (fe_be_path) {
+ if (!fe_be_path.startsWith('/')) {
+ throw new Error(
+ 'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
+ );
+ }
+ be_api_rewrite = `${
+ fe_be_path === '/' ? '' : fe_be_path
+ }${be_api}`;
+ be_download_rewrite = `${
+ fe_be_path === '/' ? '' : fe_be_path
+ }${be_download}`;
+
+ app.use(
+ be_share_rewrite,
+ createProxyMiddleware({
+ target: `http://127.0.0.1:${port}${prefix}`,
+ changeOrigin: true,
+ pathRewrite: async (path, req) => {
+ if (req.method.toLowerCase() !== 'get')
+ throw new Error('Method not allowed');
+ const tokens = $.read(TOKENS_KEY) || [];
+ const token = tokens.find(
+ (t) =>
+ t.token === req.query.token &&
+ t.type === req.params.type &&
+ t.name === req.params.name &&
+ (t.exp == null || t.exp > Date.now()),
+ );
+ if (!token) {
+ const settings = $.read(SETTINGS_KEY);
+ if (
+ settings?.appearanceSetting
+ ?.invalidShareFakeNode
+ ) {
+ return req.originalUrl
+ .replace(
+ /\/share\/.*?\//,
+ '/share/sub/',
+ )
+ .replace('?', '?_fakeNode=true&');
+ } else {
+ return '/404';
+ }
+ }
+ return req.originalUrl;
+ },
+ }),
+ );
+ app.use(
+ be_api_rewrite,
+ createProxyMiddleware({
+ target: `http://127.0.0.1:${port}${prefix}${be_api}`,
+ pathRewrite: async (path) => {
+ return path.includes('?')
+ ? `${path}&share=true`
+ : `${path}?share=true`;
+ },
+ }),
+ );
+ app.use(
+ be_download_rewrite,
+ createProxyMiddleware({
+ target: `http://127.0.0.1:${port}${prefix}${be_download}`,
+ changeOrigin: true,
+ }),
+ );
+ }
+
+ app.use(staticFileMiddleware);
+ app.use(
+ history({
+ disableDotRule: true,
+ verbose: false,
+ }),
+ );
+ app.use(staticFileMiddleware);
+
+ const listener = app.listen(fe_port, fe_host, () => {
+ const { address: fe_address, port: fe_port } =
+ listener.address();
+ $.info(`[FRONTEND] ${fe_address}:${fe_port}`);
+ if (fe_be_path) {
+ $.info(
+ `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
+ );
+ $.info(
+ `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`,
+ );
+ $.info(
+ `[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
+ );
+ }
+ });
+ }
+ if (data_url) {
+ $.info(`[BACKEND] downloading data from ${data_url}`);
+ download(data_url)
+ .then(async (content) => {
+ try {
+ content = JSON.parse(Base64.decode(content));
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error(
+ '备份文件应该至少包含 settings 字段',
+ );
+ }
+ } catch (err) {
+ try {
+ content = JSON.parse(content);
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error(
+ '备份文件应该至少包含 settings 字段',
+ );
+ }
+ } catch (err) {
+ $.error(
+ `Gist 备份文件校验失败, 无法还原\nReason: ${
+ err.message ?? err
+ }`,
+ );
+ throw new Error('Gist 备份文件校验失败, 无法还原');
+ }
+ }
+ if (data_url_post) {
+ $.info('[BACKEND] executing post-processing script');
+ eval(data_url_post);
+ }
+
+ $.write(JSON.stringify(content, null, ` `), '#sub-store');
+
+ $.cache = content;
+ $.persistCache();
+
+ migrate();
+ $.info(`[BACKEND] restored data from ${data_url}`);
+ })
+ .catch((e) => {
+ $.error(`[BACKEND] restore data failed`);
+ console.error(e);
+ throw e;
+ });
+ }
+ }
+}
diff --git a/backend/src/restful/logs.js b/backend/src/restful/logs.js
new file mode 100644
index 0000000000..409a2fd2a6
--- /dev/null
+++ b/backend/src/restful/logs.js
@@ -0,0 +1,52 @@
+import $ from '@/core/app';
+import { InternalServerError, RequestInvalidError } from '@/restful/errors';
+import { failed, success } from '@/restful/response';
+import { clearLogEntries, getLogEntries } from '@/utils/debug-logs';
+
+export default function register($app) {
+ $app.route('/api/logs').get(getLogs).delete(clearLogs);
+}
+
+function getLogs(req, res) {
+ try {
+ success(res, getLogEntries($, req.query || {}));
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'INVALID_LOG_KEYWORD_REGEX',
+ 'Invalid log keyword regular expression',
+ `Reason: ${e.message ?? e}`,
+ ),
+ 400,
+ );
+ return;
+ }
+
+ failed(
+ res,
+ new InternalServerError(
+ 'FAILED_TO_GET_LOGS',
+ 'Failed to get logs',
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+function clearLogs(req, res) {
+ try {
+ clearLogEntries($);
+ success(res);
+ } catch (e) {
+ failed(
+ res,
+ new InternalServerError(
+ 'FAILED_TO_CLEAR_LOGS',
+ 'Failed to clear logs',
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
diff --git a/backend/src/restful/miscs.js b/backend/src/restful/miscs.js
new file mode 100644
index 0000000000..89282d84c4
--- /dev/null
+++ b/backend/src/restful/miscs.js
@@ -0,0 +1,295 @@
+import { Base64 } from 'js-base64';
+import _ from 'lodash';
+import $ from '@/core/app';
+import { ENV } from '@/vendor/open-api';
+import { failed, success } from '@/restful/response';
+import { updateArtifactStore, updateAvatar } from '@/restful/settings';
+import resourceCache from '@/utils/resource-cache';
+import scriptResourceCache from '@/utils/script-resource-cache';
+import headersResourceCache from '@/utils/headers-resource-cache';
+import {
+ GIST_BACKUP_FILE_NAME,
+ GIST_BACKUP_KEY,
+ SETTINGS_KEY,
+} from '@/constants';
+import { InternalServerError, RequestInvalidError } from '@/restful/errors';
+import Gist from '@/utils/gist';
+import migrate from '@/utils/migration';
+import env from '@/utils/env';
+import { formatDateTime } from '@/utils';
+
+export default function register($app) {
+ // utils
+ $app.get('/api/utils/env', getEnv); // get runtime environment
+ $app.get('/api/utils/backup', gistBackup); // gist backup actions
+ $app.get('/api/utils/refresh', refresh);
+
+ // Storage management
+ $app.route('/api/storage')
+ .get((req, res) => {
+ res.set('content-type', 'application/json')
+ .set(
+ 'content-disposition',
+ `attachment; filename="${encodeURIComponent(
+ `sub-store_data_${formatDateTime(new Date())}.json`,
+ )}"`,
+ )
+ .send(
+ $.env.isNode
+ ? JSON.stringify($.cache)
+ : $.read('#sub-store'),
+ );
+ })
+ .post((req, res) => {
+ try {
+ let { content } = req.body;
+ try {
+ content = JSON.parse(Base64.decode(content));
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error('备份文件应该至少包含 settings 字段');
+ }
+ } catch (err) {
+ try {
+ content = JSON.parse(content);
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error(
+ '备份文件应该至少包含 settings 字段',
+ );
+ }
+ } catch (err) {
+ $.error(
+ `备份文件校验失败, 无法还原\nReason: ${
+ err.message ?? err
+ }`,
+ );
+ throw new Error('备份文件校验失败, 无法还原');
+ }
+ }
+ $.write(JSON.stringify(content, null, ` `), '#sub-store');
+ if ($.env.isNode) {
+ $.cache = content;
+ $.persistCache();
+ }
+ migrate();
+ success(res);
+ } catch (e) {
+ $.error(
+ `Failed to restore backup data.\nReason: ${e.message ?? e}`,
+ );
+ failed(
+ res,
+ new RequestInvalidError(
+ 'INVALID_BACKUP_DATA',
+ 'Invalid backup data, failed to restore!',
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+ });
+
+ if (ENV().isNode) {
+ $app.get('/', getEnv);
+ } else {
+ // Redirect sub.store to vercel webpage
+ $app.get('/', async (req, res) => {
+ // 302 redirect
+ res.set('location', 'https://sub-store.vercel.app/')
+ .status(302)
+ .end();
+ });
+ }
+
+ // handle preflight request for QX
+ if (ENV().isQX) {
+ $app.options('/', async (req, res) => {
+ res.status(200).end();
+ });
+ }
+
+ $app.all('/', (_, res) => {
+ res.send('Hello from sub-store, made with ❤️ by Peng-YM');
+ });
+}
+
+function getEnv(req, res) {
+ env.feature = env.feature || {};
+ env.feature.archive = true;
+ if (req.query.share) {
+ env.feature.share = true;
+ }
+ res.set('Content-Type', 'application/json;charset=UTF-8').send(
+ JSON.stringify(
+ {
+ status: 'success',
+ data: {
+ guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
+ ...env,
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+}
+
+async function refresh(_, res) {
+ // 1. get artifact store
+ // await updateAvatar();
+ await updateArtifactStore();
+
+ // 2. clear resource cache
+ resourceCache.revokeAll();
+ scriptResourceCache.revokeAll();
+ headersResourceCache.revokeAll();
+ success(res);
+}
+
+async function gistBackupAction(action, keep, encode) {
+ // read token
+ const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
+ if (!gistToken) throw new Error('GitHub Token is required for backup!');
+
+ const gist = new Gist({
+ token: gistToken,
+ key: GIST_BACKUP_KEY,
+ syncPlatform,
+ });
+ let currentContent = $.read('#sub-store');
+ currentContent = currentContent ? JSON.parse(currentContent) : {};
+ if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));
+ let content;
+ const settings = $.read(SETTINGS_KEY);
+ const updated = settings.syncTime;
+
+ const encoding = encode || settings.gistUpload || 'base64';
+ $.info(
+ `Gist backup action: ${action}, keep: ${keep}, encode: ${encode}, settings encode: ${settings.gistUpload}, final encoding: ${encoding}`,
+ );
+ switch (action) {
+ case 'upload':
+ try {
+ content = $.read('#sub-store');
+ content = content ? JSON.parse(content) : {};
+ if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
+ if (encoding === 'plaintext') {
+ content.settings.gistToken =
+ '恢复后请重新设置 GitHub Token';
+ content = JSON.stringify(content, null, ` `);
+ } else {
+ content = Base64.encode(
+ JSON.stringify(content, null, ` `),
+ );
+ }
+
+ $.info(`下载备份, 与本地内容对比...`);
+ const onlineContent = await gist.download(
+ GIST_BACKUP_FILE_NAME,
+ );
+ if (onlineContent === content) {
+ $.info(`内容一致, 无需上传备份`);
+ return;
+ }
+ } catch (error) {
+ $.error(`${error.message ?? error}`);
+ }
+
+ // update syncTime
+ settings.syncTime = new Date().getTime();
+ $.write(settings, SETTINGS_KEY);
+ content = $.read('#sub-store');
+ content = content ? JSON.parse(content) : {};
+ if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
+ if (encoding === 'plaintext') {
+ content.settings.gistToken = '恢复后请重新设置 GitHub Token';
+ content = JSON.stringify(content, null, ` `);
+ } else {
+ content = Base64.encode(JSON.stringify(content, null, ` `));
+ }
+ $.info(`上传备份中...`);
+ try {
+ await gist.upload({
+ [GIST_BACKUP_FILE_NAME]: { content },
+ });
+ $.info(`上传备份完成`);
+ } catch (err) {
+ // restore syncTime if upload failed
+ settings.syncTime = updated;
+ $.write(settings, SETTINGS_KEY);
+ throw err;
+ }
+ break;
+ case 'download':
+ $.info(`还原备份中...`);
+ content = await gist.download(GIST_BACKUP_FILE_NAME);
+ try {
+ content = JSON.parse(Base64.decode(content));
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error('备份文件应该至少包含 settings 字段');
+ }
+ } catch (err) {
+ try {
+ content = JSON.parse(content);
+ if (!(Object.keys(content.settings).length >= 0)) {
+ throw new Error('备份文件应该至少包含 settings 字段');
+ }
+ } catch (err) {
+ $.error(
+ `Gist 备份文件校验失败, 无法还原\nReason: ${
+ err.message ?? err
+ }`,
+ );
+ throw new Error('Gist 备份文件校验失败, 无法还原');
+ }
+ }
+ if (keep) {
+ $.info(`保留原有设置 ${keep}`);
+ keep.split(',').forEach((path) => {
+ _.set(content, path, _.get(currentContent, path));
+ });
+ }
+ // restore settings
+ $.write(JSON.stringify(content, null, ` `), '#sub-store');
+ if ($.env.isNode) {
+ $.cache = content;
+ $.persistCache();
+ }
+ $.info(`perform migration after restoring from gist...`);
+ migrate();
+ $.info(`migration completed`);
+ $.info(`还原备份完成`);
+ break;
+ }
+}
+async function gistBackup(req, res) {
+ const { action, keep, encode } = req.query;
+ // read token
+ const { gistToken } = $.read(SETTINGS_KEY);
+ if (!gistToken) {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'GIST_TOKEN_NOT_FOUND',
+ `GitHub Token is required for backup!`,
+ ),
+ );
+ } else {
+ try {
+ await gistBackupAction(action, keep, encode);
+ success(res);
+ } catch (err) {
+ $.error(
+ `Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
+ );
+ failed(
+ res,
+ new InternalServerError(
+ 'BACKUP_FAILED',
+ `Failed to ${action} gist data!`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+ }
+}
+
+export { gistBackupAction };
diff --git a/backend/src/restful/module.js b/backend/src/restful/module.js
new file mode 100644
index 0000000000..c7421940d5
--- /dev/null
+++ b/backend/src/restful/module.js
@@ -0,0 +1,113 @@
+import { deleteByName, findByName, updateByName } from '@/utils/database';
+import { MODULES_KEY } from '@/constants';
+import { failed, success } from '@/restful/response';
+import $ from '@/core/app';
+import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
+import { hex_md5 } from '@/vendor/md5';
+
+export default function register($app) {
+ if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
+
+ $app.route('/api/module/:name')
+ .get(getModule)
+ .patch(updateModule)
+ .delete(deleteModule);
+
+ $app.route('/api/modules')
+ .get(getAllModules)
+ .post(createModule)
+ .put(replaceModule);
+}
+
+// module API
+function createModule(req, res) {
+ const module = req.body;
+ module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
+ $.info(`正在创建模块:${module.name}`);
+ const allModules = $.read(MODULES_KEY);
+ if (findByName(allModules, module.name)) {
+ return failed(
+ res,
+ new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ req.body.name
+ ? `已存在 name 为 ${module.name} 的模块`
+ : `已存在相同的模块 请勿重复添加`,
+ ),
+ );
+ }
+ allModules.push(module);
+ $.write(allModules, MODULES_KEY);
+ success(res, module, 201);
+}
+
+function getModule(req, res) {
+ let { name } = req.params;
+ const allModules = $.read(MODULES_KEY);
+ const module = findByName(allModules, name);
+ if (module) {
+ res.set('Content-Type', 'text/plain; charset=utf-8').send(
+ module.content,
+ );
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ `MODULE_NOT_FOUND`,
+ `Module ${name} does not exist`,
+ 404,
+ ),
+ );
+ }
+}
+
+function updateModule(req, res) {
+ let { name } = req.params;
+ let module = req.body;
+ const allModules = $.read(MODULES_KEY);
+ const oldModule = findByName(allModules, name);
+ if (oldModule) {
+ const newModule = {
+ ...oldModule,
+ ...module,
+ };
+ $.info(`正在更新模块:${name}...`);
+
+ updateByName(allModules, name, newModule);
+ $.write(allModules, MODULES_KEY);
+ success(res, newModule);
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Module ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+function deleteModule(req, res) {
+ let { name } = req.params;
+ $.info(`正在删除模块:${name}`);
+ let allModules = $.read(MODULES_KEY);
+ deleteByName(allModules, name);
+ $.write(allModules, MODULES_KEY);
+ success(res);
+}
+
+function getAllModules(req, res) {
+ const allModules = $.read(MODULES_KEY);
+ success(
+ res,
+ // eslint-disable-next-line no-unused-vars
+ allModules.map(({ content, ...rest }) => rest),
+ );
+}
+
+function replaceModule(req, res) {
+ const allModules = req.body;
+ $.write(allModules, MODULES_KEY);
+ success(res);
+}
diff --git a/backend/src/restful/node-info.js b/backend/src/restful/node-info.js
new file mode 100644
index 0000000000..17ddc77446
--- /dev/null
+++ b/backend/src/restful/node-info.js
@@ -0,0 +1,59 @@
+import producer from '@/core/proxy-utils/producers';
+import { HTTP } from '@/vendor/open-api';
+import { failed, success } from '@/restful/response';
+import { NetworkError } from '@/restful/errors';
+
+export default function register($app) {
+ $app.post('/api/utils/node-info', getNodeInfo);
+}
+
+async function getNodeInfo(req, res) {
+ const proxy = req.body;
+ const lang = req.query.lang || 'zh-CN';
+ let shareUrl;
+ try {
+ shareUrl = producer.URI.produce(proxy);
+ } catch (err) {
+ // do nothing
+ }
+
+ try {
+ const $http = HTTP();
+ const info = await $http
+ .get({
+ url: `http://ip-api.com/json/${encodeURIComponent(
+ `${proxy.server}`
+ .trim()
+ .replace(/^\[/, '')
+ .replace(/\]$/, ''),
+ )}?lang=${lang}`,
+ headers: {
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
+ },
+ })
+ .then((resp) => {
+ const data = JSON.parse(resp.body);
+ if (data.status !== 'success') {
+ throw new Error(data.message);
+ }
+
+ // remove unnecessary fields
+ delete data.status;
+ return data;
+ });
+ success(res, {
+ shareUrl,
+ info,
+ });
+ } catch (err) {
+ failed(
+ res,
+ new NetworkError(
+ 'FAILED_TO_GET_NODE_INFO',
+ `Failed to get node info`,
+ `Reason: ${err}`,
+ ),
+ );
+ }
+}
diff --git a/backend/src/restful/parser.js b/backend/src/restful/parser.js
new file mode 100644
index 0000000000..d5b1888b18
--- /dev/null
+++ b/backend/src/restful/parser.js
@@ -0,0 +1,54 @@
+import { success, failed } from '@/restful/response';
+import { ProxyUtils } from '@/core/proxy-utils';
+import { RuleUtils } from '@/core/rule-utils';
+
+export default function register($app) {
+ $app.route('/api/proxy/parse').post(proxy_parser);
+ $app.route('/api/rule/parse').post(rule_parser);
+}
+
+/***
+ * 感谢 izhangxm 的 PR!
+ * 目前没有节点操作, 没有支持完整参数, 以后再完善一下
+ */
+
+/***
+ * 代理服务器协议转换接口。
+ * 请求方法为POST,数据为json。需要提供data和client字段。
+ * data: string, 协议数据,每行一个或者是clash
+ * client: string, 目标平台名称,见backend/src/core/proxy-utils/producers/index.js
+ *
+ */
+function proxy_parser(req, res) {
+ const { data, client, content, platform } = req.body;
+ var result = {};
+ try {
+ var proxies = ProxyUtils.parse(data ?? content);
+ var par_res = ProxyUtils.produce(proxies, client ?? platform);
+ result['par_res'] = par_res;
+ } catch (err) {
+ failed(res, err);
+ return;
+ }
+ success(res, result);
+}
+/**
+ * 规则转换接口。
+ * 请求方法为POST,数据为json。需要提供data和client字段。
+ * data: string, 多行规则字符串
+ * client: string, 目标平台名称,具体见backend/src/core/rule-utils/producers.js
+ */
+function rule_parser(req, res) {
+ const { data, client, content, platform } = req.body;
+ var result = {};
+ try {
+ const rules = RuleUtils.parse(data ?? content);
+ var par_res = RuleUtils.produce(rules, client ?? platform);
+ result['par_res'] = par_res;
+ } catch (err) {
+ failed(res, err);
+ return;
+ }
+
+ success(res, result);
+}
diff --git a/backend/src/restful/preview.js b/backend/src/restful/preview.js
new file mode 100644
index 0000000000..755527928d
--- /dev/null
+++ b/backend/src/restful/preview.js
@@ -0,0 +1,442 @@
+import { InternalServerError } from './errors';
+import { ProxyUtils } from '@/core/proxy-utils';
+import { findByName } from '@/utils/database';
+import { success, failed } from './response';
+import download from '@/utils/download';
+import { SUBS_KEY } from '@/constants';
+import $ from '@/core/app';
+import {
+ handleIgnoreFailedRemoteSubError,
+ notifyIgnoreFailedRemoteSubFallback,
+ resolveIgnoreFailedRemoteSubMode,
+ shouldFallbackIgnoreFailedRemoteSub,
+} from '@/restful/ignore-failed-remote-sub';
+import { normalizeClashYaml } from '@/core/proxy-utils/preprocessors';
+
+export default function register($app) {
+ $app.post('/api/preview/sub', compareSub);
+ $app.post('/api/preview/collection', compareCollection);
+ $app.post('/api/preview/file', previewFile);
+}
+
+async function previewFile(req, res) {
+ try {
+ const file = req.body;
+ let content = '';
+ if (file.type !== 'mihomoProfile') {
+ if (
+ file.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(file.mergeSources)
+ ) {
+ content = file.content;
+ } else {
+ const errors = {};
+ content = await Promise.all(
+ file.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ file.ua,
+ undefined,
+ file.proxy,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ if (!file.ignoreFailedRemoteFile) {
+ throw new Error(
+ `文件 ${file.name} 的远程文件 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`,
+ );
+ } else if (file.ignoreFailedRemoteFile === 'enabled') {
+ $.notify(
+ `🌍 Sub-Store 预览文件失败`,
+ `❌ ${file.name}`,
+ `远程文件 ${Object.keys(errors).join(
+ ', ',
+ )} 发生错误, 请查看日志`,
+ );
+ }
+ }
+ if (file.mergeSources === 'localFirst') {
+ content.unshift(file.content);
+ } else if (file.mergeSources === 'remoteFirst') {
+ content.push(file.content);
+ }
+ }
+ }
+ // parse proxies
+ const files = (Array.isArray(content) ? content : [content]).flat();
+ let filesContent = files
+ .filter((i) => i != null && i !== '')
+ .join('\n');
+
+ // apply processors
+ const processed =
+ Array.isArray(file.process) && file.process.length > 0
+ ? await ProxyUtils.process(
+ { $files: files, $content: filesContent, $file: file },
+ file.process,
+ )
+ : { $content: filesContent, $files: files };
+
+ // produce
+ success(res, {
+ original: filesContent,
+ processed: normalizeClashYaml(processed?.$content ?? ''),
+ });
+ } catch (err) {
+ $.error(err.message ?? err);
+ failed(
+ res,
+ new InternalServerError(
+ `INTERNAL_SERVER_ERROR`,
+ `Failed to preview file`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+}
+
+async function compareSub(req, res) {
+ const sub = req.body;
+ const mode = resolveIgnoreFailedRemoteSubMode(sub.ignoreFailedRemoteSub);
+
+ try {
+ const target = req.query.target || 'JSON';
+ let content;
+ if (
+ sub.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
+ ) {
+ content = sub.content;
+ } else {
+ const errors = {};
+ content = await Promise.all(
+ sub.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ sub.ua,
+ undefined,
+ sub.proxy,
+ undefined,
+ undefined,
+ undefined,
+ true,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `订阅 ${sub.name} 的远程订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 预览订阅失败`,
+ `❌ ${sub.name}`,
+ message,
+ );
+ },
+ });
+ }
+ if (sub.mergeSources === 'localFirst') {
+ content.unshift(sub.content);
+ } else if (sub.mergeSources === 'remoteFirst') {
+ content.push(sub.content);
+ }
+ }
+ // parse proxies
+ const original = (Array.isArray(content) ? content : [content])
+ .map((i) => ProxyUtils.parse(i))
+ .flat();
+
+ // add id
+ original.forEach((proxy, i) => {
+ proxy.id = i;
+ proxy._subName = sub.name;
+ proxy._subDisplayName = sub.displayName;
+ });
+
+ // apply processors
+ const processed = await ProxyUtils.process(
+ original,
+ sub.process || [],
+ target,
+ { [sub.name]: sub },
+ );
+
+ // produce
+ success(res, { original, processed });
+ } catch (err) {
+ if (shouldFallbackIgnoreFailedRemoteSub(mode)) {
+ notifyIgnoreFailedRemoteSubFallback({
+ mode,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 预览订阅失败`,
+ `❌ ${sub.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `订阅 ${sub.name} 预览启用兜底后返回空结果: ${
+ err.message ?? err
+ }`,
+ );
+ success(res, { original: [], processed: [] });
+ return;
+ }
+
+ $.error(err.message ?? err);
+ failed(
+ res,
+ new InternalServerError(
+ `INTERNAL_SERVER_ERROR`,
+ `Failed to preview subscription`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+}
+
+async function compareCollection(req, res) {
+ const collection = req.body;
+ const collectionMode = resolveIgnoreFailedRemoteSubMode(
+ collection.ignoreFailedRemoteSub,
+ );
+
+ try {
+ const allSubs = $.read(SUBS_KEY);
+ const subnames = [...collection.subscriptions];
+ let subscriptionTags = collection.subscriptionTags;
+ if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
+ allSubs.forEach((sub) => {
+ if (
+ Array.isArray(sub.tag) &&
+ sub.tag.length > 0 &&
+ !subnames.includes(sub.name) &&
+ sub.tag.some((tag) => subscriptionTags.includes(tag))
+ ) {
+ subnames.push(sub.name);
+ }
+ });
+ }
+ const results = {};
+ const errors = {};
+ await Promise.all(
+ subnames.map(async (name) => {
+ const sub = findByName(allSubs, name);
+ const subMode = resolveIgnoreFailedRemoteSubMode(
+ sub.ignoreFailedRemoteSub,
+ );
+ try {
+ let raw;
+ if (
+ sub.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(
+ sub.mergeSources,
+ )
+ ) {
+ raw = sub.content;
+ } else {
+ const errors = {};
+ raw = await Promise.all(
+ sub.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ sub.ua,
+ undefined,
+ sub.proxy,
+ undefined,
+ undefined,
+ undefined,
+ true,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `订阅 ${sub.name} 的远程订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: subMode,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 预览订阅失败`,
+ `❌ ${sub.name}`,
+ message,
+ );
+ },
+ });
+ }
+ if (sub.mergeSources === 'localFirst') {
+ raw.unshift(sub.content);
+ } else if (sub.mergeSources === 'remoteFirst') {
+ raw.push(sub.content);
+ }
+ }
+ // parse proxies
+ let currentProxies = (Array.isArray(raw) ? raw : [raw])
+ .map((i) => ProxyUtils.parse(i))
+ .flat();
+
+ currentProxies.forEach((proxy) => {
+ proxy._subName = sub.name;
+ proxy._subDisplayName = sub.displayName;
+ proxy._collectionName = collection.name;
+ proxy._collectionDisplayName = collection.displayName;
+ });
+
+ // apply processors
+ currentProxies = await ProxyUtils.process(
+ currentProxies,
+ sub.process || [],
+ 'JSON',
+ { [sub.name]: sub, _collection: collection },
+ );
+ results[name] = currentProxies;
+ } catch (err) {
+ if (shouldFallbackIgnoreFailedRemoteSub(subMode)) {
+ notifyIgnoreFailedRemoteSubFallback({
+ mode: subMode,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 预览订阅失败`,
+ `❌ ${sub.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `订阅 ${sub.name} 在组合订阅预览中启用兜底后返回空结果: ${
+ err.message ?? err
+ }`,
+ );
+ results[name] = [];
+ return;
+ }
+
+ errors[name] = err;
+
+ $.error(
+ `❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name} 时出现错误:${err}!`,
+ );
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `组合订阅 ${collection.name} 的子订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: collectionMode,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 预览组合订阅失败`,
+ `❌ ${collection.name}`,
+ message,
+ );
+ },
+ });
+ }
+ // merge proxies with the original order
+ const original = Array.prototype.concat.apply(
+ [],
+ subnames.map((name) => results[name] || []),
+ );
+
+ original.forEach((proxy, i) => {
+ proxy.id = i;
+ proxy._collectionName = collection.name;
+ proxy._collectionDisplayName = collection.displayName;
+ });
+
+ const processed = await ProxyUtils.process(
+ original,
+ collection.process || [],
+ 'JSON',
+ { _collection: collection },
+ );
+
+ success(res, { original, processed });
+ } catch (err) {
+ if (shouldFallbackIgnoreFailedRemoteSub(collectionMode)) {
+ notifyIgnoreFailedRemoteSubFallback({
+ mode: collectionMode,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 预览组合订阅失败`,
+ `❌ ${collection.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `组合订阅 ${collection.name} 预览启用兜底后返回空结果: ${
+ err.message ?? err
+ }`,
+ );
+ success(res, { original: [], processed: [] });
+ return;
+ }
+
+ $.error(err.message ?? err);
+ failed(
+ res,
+ new InternalServerError(
+ `INTERNAL_SERVER_ERROR`,
+ `Failed to preview collection`,
+ `Reason: ${err.message ?? err}`,
+ ),
+ );
+ }
+}
diff --git a/backend/src/restful/response-transformer.js b/backend/src/restful/response-transformer.js
new file mode 100644
index 0000000000..f2b75057eb
--- /dev/null
+++ b/backend/src/restful/response-transformer.js
@@ -0,0 +1,56 @@
+import { ProxyUtils } from '@/core/proxy-utils';
+
+function getCurrentHeaders(res) {
+ return typeof res.getHeaders === 'function' ? { ...res.getHeaders() } : {};
+}
+
+function setHeaders(res, currentHeaders, nextHeaders = {}) {
+ for (const key of Object.keys(currentHeaders)) {
+ if (!Object.prototype.hasOwnProperty.call(nextHeaders, key)) {
+ res.removeHeader(key);
+ }
+ }
+ for (const [key, value] of Object.entries(nextHeaders)) {
+ if (value == null) {
+ res.removeHeader(key);
+ } else {
+ res.set(key, value);
+ }
+ }
+}
+
+function setStatus(res, status) {
+ const statusCode = Number(status);
+ if (
+ Number.isInteger(statusCode) &&
+ statusCode >= 100 &&
+ statusCode <= 599
+ ) {
+ res.status(statusCode);
+ }
+}
+
+export async function applyResponseTransformers({
+ res,
+ body,
+ process,
+ targetPlatform,
+ source,
+ $options,
+}) {
+ const currentHeaders = getCurrentHeaders(res);
+ const transformed = await ProxyUtils.processResponse(
+ {
+ status: res.statusCode || 200,
+ headers: currentHeaders,
+ body,
+ },
+ process || [],
+ targetPlatform,
+ source,
+ $options,
+ );
+ setHeaders(res, currentHeaders, transformed.header || transformed.headers);
+ setStatus(res, transformed.status || transformed.statusCode);
+ return transformed.body;
+}
diff --git a/backend/src/restful/response.js b/backend/src/restful/response.js
new file mode 100644
index 0000000000..fee9c03550
--- /dev/null
+++ b/backend/src/restful/response.js
@@ -0,0 +1,20 @@
+export function success(resp, data, statusCode) {
+ resp.status(statusCode || 200).json({
+ status: 'success',
+ data,
+ });
+}
+
+export function failed(resp, error, statusCode) {
+ resp.status(statusCode || 500).json({
+ status: 'failed',
+ error: {
+ code: error.code,
+ type: error.type,
+ message: error.message,
+ details: resp.req?.route?.path?.startsWith('/share/')
+ ? '详情请查看日志'
+ : error.details,
+ },
+ });
+}
diff --git a/backend/src/restful/settings.js b/backend/src/restful/settings.js
new file mode 100644
index 0000000000..52e0469d7f
--- /dev/null
+++ b/backend/src/restful/settings.js
@@ -0,0 +1,213 @@
+import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
+import { success, failed } from './response';
+import { InternalServerError } from '@/restful/errors';
+import $ from '@/core/app';
+import Gist, { getGithubGistBaseURL } from '@/utils/gist';
+import { clearLogSettingsCache } from '@/utils/debug-logs';
+
+const ARTIFACT_STORE_SETTING_KEYS = [
+ 'gistToken',
+ 'githubProxy',
+ 'githubApiUrl',
+ 'defaultProxy',
+];
+
+export function shouldRefreshArtifactStoreForSettingsPatch(body = {}) {
+ return ARTIFACT_STORE_SETTING_KEYS.some((key) =>
+ Object.prototype.hasOwnProperty.call(body, key),
+ );
+}
+
+export function getGithubAvatarApiUrl({ username, githubApiUrl, githubProxy }) {
+ const githubApiBaseURL = getGithubGistBaseURL({
+ githubApiUrl,
+ githubProxy,
+ });
+
+ return `${githubApiBaseURL}/users/${encodeURIComponent(username)}`;
+}
+
+export default function register($app) {
+ const settings = $.read(SETTINGS_KEY);
+ if (!settings) $.write({}, SETTINGS_KEY);
+ $app.route('/api/settings').get(getSettings).patch(updateSettings);
+}
+
+async function getSettings(req, res) {
+ try {
+ let settings = $.read(SETTINGS_KEY);
+ if (!settings) {
+ settings = {};
+ $.write(settings, SETTINGS_KEY);
+ }
+
+ // await updateAvatar();
+ if (!settings.artifactStore) await updateArtifactStore();
+
+ success(res, settings);
+ } catch (e) {
+ $.error(`Failed to get settings: ${e.message ?? e}`);
+ failed(
+ res,
+ new InternalServerError(
+ `FAILED_TO_GET_SETTINGS`,
+ `Failed to get settings`,
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+async function updateSettings(req, res) {
+ try {
+ const settings = $.read(SETTINGS_KEY);
+ const newSettings = {
+ ...settings,
+ ...req.body,
+ };
+ [
+ 'defaultTimeout',
+ 'githubApiTimeout',
+ 'artifactSyncBatchSize',
+ 'cacheThreshold',
+ 'resourceCacheTtl',
+ 'headersCacheTtl',
+ 'scriptCacheTtl',
+ ].map((key) => {
+ let value = Number(newSettings[key]);
+ if (!isFinite(value) || value <= 0) {
+ delete newSettings[key];
+ }
+ });
+ if ('logsMaxCount' in newSettings) {
+ const rawLogsMaxCount = newSettings.logsMaxCount;
+ const value = Number(rawLogsMaxCount);
+ if (
+ rawLogsMaxCount === null ||
+ rawLogsMaxCount === undefined ||
+ rawLogsMaxCount === '' ||
+ !isFinite(value) ||
+ value < 0
+ ) {
+ delete newSettings.logsMaxCount;
+ }
+ }
+ $.write(newSettings, SETTINGS_KEY);
+ clearLogSettingsCache();
+ if (shouldRefreshArtifactStoreForSettingsPatch(req.body)) {
+ // await updateAvatar();
+ await updateArtifactStore();
+ }
+ success(res, newSettings);
+ } catch (e) {
+ $.error(`Failed to update settings: ${e.message ?? e}`);
+ failed(
+ res,
+ new InternalServerError(
+ `FAILED_TO_UPDATE_SETTINGS`,
+ `Failed to update settings`,
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+export async function updateAvatar() {
+ const settings = $.read(SETTINGS_KEY);
+ const {
+ githubUser: username,
+ syncPlatform,
+ githubProxy,
+ githubApiUrl,
+ githubApiTimeout,
+ } = settings;
+ if (username) {
+ if (syncPlatform === 'gitlab') {
+ try {
+ const data = await $.http
+ .get({
+ url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
+ username,
+ )}`,
+ headers: {
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
+ },
+ timeout: githubApiTimeout || 10000,
+ })
+ .then((resp) => JSON.parse(resp.body));
+ settings.avatarUrl = data[0]['avatar_url'].replace(
+ /(\?|&)s=\d+(&|$)/,
+ '$1s=160$2',
+ );
+ $.write(settings, SETTINGS_KEY);
+ } catch (err) {
+ $.error(
+ `Failed to fetch GitLab avatar for User: ${username}. Reason: ${
+ err.message ?? err
+ }`,
+ );
+ }
+ } else {
+ try {
+ const data = await $.http
+ .get({
+ url: getGithubAvatarApiUrl({
+ username,
+ githubApiUrl,
+ githubProxy,
+ }),
+ headers: {
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
+ },
+ timeout: githubApiTimeout || 10000,
+ })
+ .then((resp) => JSON.parse(resp.body));
+ settings.avatarUrl = data['avatar_url'];
+ $.write(settings, SETTINGS_KEY);
+ } catch (err) {
+ $.error(
+ `Failed to fetch GitHub avatar for User: ${username}. Reason: ${
+ err.message ?? err
+ }`,
+ );
+ }
+ }
+ }
+}
+
+export async function updateArtifactStore() {
+ $.log('Updating artifact store');
+ const settings = $.read(SETTINGS_KEY);
+ const { gistToken, syncPlatform } = settings;
+ if (gistToken) {
+ const manager = new Gist({
+ token: gistToken,
+ key: ARTIFACT_REPOSITORY_KEY,
+ syncPlatform,
+ });
+
+ try {
+ const gist = await manager.locate();
+ const url = gist?.html_url ?? gist?.web_url;
+ if (url) {
+ $.log(`找到 Sub-Store Gist: ${url}`);
+ // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
+ settings.artifactStore = url;
+ settings.artifactStoreStatus = 'VALID';
+ } else {
+ $.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
+ settings.artifactStoreStatus = 'NOT FOUND';
+ }
+ } catch (err) {
+ $.error(
+ `查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
+ err.message ?? err
+ }`,
+ );
+ settings.artifactStoreStatus = 'ERROR';
+ }
+ $.write(settings, SETTINGS_KEY);
+ }
+}
diff --git a/backend/src/restful/sort.js b/backend/src/restful/sort.js
new file mode 100644
index 0000000000..d01773820d
--- /dev/null
+++ b/backend/src/restful/sort.js
@@ -0,0 +1,70 @@
+import {
+ ARTIFACTS_KEY,
+ COLLECTIONS_KEY,
+ SUBS_KEY,
+ FILES_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+import $ from '@/core/app';
+import { success } from '@/restful/response';
+import { sortArchiveEntries } from '@/utils/archive';
+
+export default function register($app) {
+ $app.post('/api/sort/subs', sortSubs);
+ $app.post('/api/sort/collections', sortCollections);
+ $app.post('/api/sort/artifacts', sortArtifacts);
+ $app.post('/api/sort/files', sortFiles);
+ $app.post('/api/sort/tokens', sortTokens);
+ $app.post('/api/sort/archives', sortArchive);
+}
+
+function sortSubs(req, res) {
+ const orders = req.body;
+ const allSubs = $.read(SUBS_KEY);
+ allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
+ $.write(allSubs, SUBS_KEY);
+ success(res, allSubs);
+}
+
+function sortCollections(req, res) {
+ const orders = req.body;
+ const allCols = $.read(COLLECTIONS_KEY);
+ allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
+ $.write(allCols, COLLECTIONS_KEY);
+ success(res, allCols);
+}
+
+function sortArtifacts(req, res) {
+ const orders = req.body;
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ allArtifacts.sort(
+ (a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
+ );
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ success(res, allArtifacts);
+}
+
+function sortFiles(req, res) {
+ const orders = req.body;
+ const allFiles = $.read(FILES_KEY);
+ allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
+ $.write(allFiles, FILES_KEY);
+ success(res, allFiles);
+}
+
+function sortTokens(req, res) {
+ const orders = req.body;
+ const allTokens = $.read(TOKENS_KEY);
+ allTokens.sort(
+ (a, b) =>
+ orders.indexOf(`${a.type}-${a.name}-${a.token}`) -
+ orders.indexOf(`${b.type}-${b.name}-${b.token}`),
+ );
+ $.write(allTokens, TOKENS_KEY);
+ success(res, allTokens);
+}
+
+function sortArchive(req, res) {
+ const entries = sortArchiveEntries(req.body);
+ success(res, entries);
+}
diff --git a/backend/src/restful/subscriptions.js b/backend/src/restful/subscriptions.js
new file mode 100644
index 0000000000..f43121be91
--- /dev/null
+++ b/backend/src/restful/subscriptions.js
@@ -0,0 +1,429 @@
+import {
+ NetworkError,
+ InternalServerError,
+ ResourceNotFoundError,
+ RequestInvalidError,
+} from './errors';
+import {
+ deleteByName,
+ findByName,
+ insertByPosition,
+ updateByName,
+} from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+import {
+ SUBS_KEY,
+ COLLECTIONS_KEY,
+ ARTIFACTS_KEY,
+ FILES_KEY,
+} from '@/constants';
+import {
+ getFlowHeaders,
+ parseFlowHeaders,
+ getRmainingDays,
+} from '@/utils/flow';
+import { archiveSubscription } from '@/utils/archive';
+import { success, failed } from './response';
+import $ from '@/core/app';
+import { formatDateTime } from '@/utils';
+
+if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
+
+export default function register($app) {
+ $app.get('/api/sub/flow/:name', getFlowInfo);
+
+ $app.route('/api/sub/:name')
+ .get(getSubscription)
+ .patch(updateSubscription)
+ .delete(deleteSubscription);
+
+ $app.route('/api/subs')
+ .get(getAllSubscriptions)
+ .post(createSubscription)
+ .put(replaceSubscriptions);
+}
+
+// subscriptions API
+async function getFlowInfo(req, res) {
+ let { name } = req.params;
+ let { url } = req.query;
+ if (url) {
+ $.info(`指定远程订阅 URL: ${url}`);
+ }
+ const allSubs = $.read(SUBS_KEY);
+ const sub = findByName(allSubs, name);
+ if (!sub) {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Subscription ${name} does not exist!`,
+ ),
+ 404,
+ );
+ return;
+ }
+ if (
+ sub.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
+ ) {
+ if (sub.subUserinfo) {
+ let subUserInfo;
+ if (/^https?:\/\//.test(sub.subUserinfo)) {
+ try {
+ subUserInfo = await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ sub.proxy,
+ sub.subUserinfo,
+ );
+ } catch (e) {
+ $.error(
+ `订阅 ${name} 使用自定义流量链接 ${
+ sub.subUserinfo
+ } 获取流量信息时发生错误: ${JSON.stringify(e)}`,
+ );
+ }
+ } else {
+ subUserInfo = sub.subUserinfo;
+ }
+ try {
+ success(res, {
+ ...parseFlowHeaders(subUserInfo),
+ });
+ } catch (e) {
+ $.error(
+ `Failed to parse flow info for local subscription ${name}: ${
+ e.message ?? e
+ }`,
+ );
+ failed(
+ res,
+ new RequestInvalidError(
+ 'NO_FLOW_INFO',
+ 'N/A',
+ `Failed to parse flow info`,
+ ),
+ );
+ }
+ } else {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'NO_FLOW_INFO',
+ 'N/A',
+ `Local subscription ${name} has no flow information!`,
+ ),
+ );
+ }
+ return;
+ }
+ try {
+ url =
+ `${url || sub.url}`
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)?.[0] || '';
+
+ let $arguments = {};
+ const rawArgs = url.split('#');
+ url = url.split('#')[0];
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ if ($arguments.noFlow || !/^https?/.test(url)) {
+ failed(
+ res,
+ new RequestInvalidError(
+ 'NO_FLOW_INFO',
+ 'N/A',
+ `Subscription ${name}: noFlow`,
+ ),
+ );
+ return;
+ }
+ const flowHeaders = await getFlowHeaders(
+ $arguments?.insecure ? `${url}#insecure` : url,
+ $arguments.flowUserAgent,
+ undefined,
+ sub.proxy,
+ $arguments.flowUrl,
+ $arguments.flowHeaders,
+ );
+ if (!flowHeaders && !sub.subUserinfo) {
+ failed(
+ res,
+ new InternalServerError(
+ 'NO_FLOW_INFO',
+ 'No flow info',
+ `Failed to fetch flow headers`,
+ ),
+ );
+ return;
+ }
+ try {
+ const remainingDays = getRmainingDays({
+ resetDay: $arguments.resetDay,
+ startDate: $arguments.startDate,
+ cycleDays: $arguments.cycleDays,
+ });
+ let subUserInfo;
+ if (/^https?:\/\//.test(sub.subUserinfo)) {
+ try {
+ subUserInfo = await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ sub.proxy,
+ sub.subUserinfo,
+ );
+ } catch (e) {
+ $.error(
+ `订阅 ${name} 使用自定义流量链接 ${
+ sub.subUserinfo
+ } 获取流量信息时发生错误: ${JSON.stringify(e)}`,
+ );
+ }
+ } else {
+ subUserInfo = sub.subUserinfo;
+ }
+ const result = {
+ ...parseFlowHeaders(
+ [subUserInfo, flowHeaders].filter((i) => i).join('; '),
+ ),
+ };
+ if (remainingDays != null) {
+ result.remainingDays = remainingDays;
+ }
+ success(res, result);
+ } catch (e) {
+ $.error(
+ `Failed to parse flow info for local subscription ${name}: ${
+ e.message ?? e
+ }`,
+ );
+ failed(
+ res,
+ new RequestInvalidError(
+ 'NO_FLOW_INFO',
+ 'N/A',
+ `Failed to parse flow info`,
+ ),
+ );
+ }
+ } catch (err) {
+ failed(
+ res,
+ new NetworkError(
+ `URL_NOT_ACCESSIBLE`,
+ `The URL for subscription ${name} is inaccessible.`,
+ ),
+ );
+ }
+}
+
+function createSubscription(req, res) {
+ try {
+ const sub = createSubscriptionItem(req.body);
+ success(res, sub, 201);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getSubscription(req, res) {
+ let { name } = req.params;
+ let { raw } = req.query;
+ const allSubs = $.read(SUBS_KEY);
+ const sub = findByName(allSubs, name);
+ delete sub.subscriptions;
+ if (sub) {
+ if (raw) {
+ res.set('content-type', 'application/json')
+ .set(
+ 'content-disposition',
+ `attachment; filename="${encodeURIComponent(
+ `sub-store_subscription_${name}_${formatDateTime(
+ new Date(),
+ )}.json`,
+ )}"`,
+ )
+ .send(JSON.stringify(sub));
+ } else {
+ success(res, sub);
+ }
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ `SUBSCRIPTION_NOT_FOUND`,
+ `Subscription ${name} does not exist`,
+ 404,
+ ),
+ );
+ }
+}
+
+function updateSubscription(req, res) {
+ let { name } = req.params;
+ let sub = req.body;
+ delete sub.subscriptions;
+ const allSubs = $.read(SUBS_KEY);
+ const oldSub = findByName(allSubs, name);
+ if (oldSub) {
+ if (!sub.name) sub.name = oldSub.name;
+ const newSub = {
+ ...oldSub,
+ ...sub,
+ };
+ $.info(`正在更新订阅: ${name}`);
+ // allow users to update the subscription name
+ if (name !== sub.name) {
+ // update all collections refer to this name
+ const allCols = $.read(COLLECTIONS_KEY) || [];
+ for (const collection of allCols) {
+ const idx = collection.subscriptions.indexOf(name);
+ if (idx !== -1) {
+ collection.subscriptions[idx] = sub.name;
+ }
+ }
+
+ // update all artifacts referring this subscription
+ const allArtifacts = $.read(ARTIFACTS_KEY) || [];
+ for (const artifact of allArtifacts) {
+ if (
+ artifact.type === 'subscription' &&
+ artifact.source == name
+ ) {
+ artifact.source = sub.name;
+ }
+ }
+ // update all files referring this subscription
+ const allFiles = $.read(FILES_KEY) || [];
+ for (const file of allFiles) {
+ if (
+ file.sourceType === 'subscription' &&
+ file.sourceName == name
+ ) {
+ file.sourceName = sub.name;
+ }
+ }
+
+ $.write(allCols, COLLECTIONS_KEY);
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ $.write(allFiles, FILES_KEY);
+ }
+ updateByName(allSubs, name, newSub);
+ $.write(allSubs, SUBS_KEY);
+ success(res, newSub);
+ } else {
+ failed(
+ res,
+ new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Subscription ${name} does not exist!`,
+ ),
+ 404,
+ );
+ }
+}
+
+function deleteSubscription(req, res) {
+ try {
+ const { name } = req.params;
+ $.info(`删除订阅:${name}...`);
+ if (shouldArchiveDeletion(req.query.mode)) {
+ archiveSubscription(name);
+ }
+ deleteSubscriptionItem(name);
+ success(res);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getAllSubscriptions(req, res) {
+ const allSubs = $.read(SUBS_KEY);
+ success(res, allSubs);
+}
+
+function replaceSubscriptions(req, res) {
+ const allSubs = req.body;
+ $.write(allSubs, SUBS_KEY);
+ success(res);
+}
+
+function createSubscriptionItem(rawSub) {
+ const sub = {
+ ...rawSub,
+ };
+ delete sub.subscriptions;
+ $.info(`正在创建订阅: ${sub.name}`);
+ if (/\//.test(sub.name)) {
+ throw new RequestInvalidError(
+ 'INVALID_NAME',
+ `Subscription ${sub.name} is invalid`,
+ );
+ }
+ const allSubs = $.read(SUBS_KEY);
+ if (findByName(allSubs, sub.name)) {
+ throw new RequestInvalidError(
+ 'DUPLICATE_KEY',
+ `Subscription ${sub.name} already exists.`,
+ );
+ }
+ insertByPosition(allSubs, sub, getCreateItemPosition());
+ $.write(allSubs, SUBS_KEY);
+ return sub;
+}
+
+function deleteSubscriptionItem(name) {
+ const allSubs = $.read(SUBS_KEY);
+ const sub = findByName(allSubs, name);
+ if (!sub) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Subscription ${name} does not exist!`,
+ );
+ }
+ deleteByName(allSubs, name);
+ $.write(allSubs, SUBS_KEY);
+
+ const allCols = $.read(COLLECTIONS_KEY) || [];
+ for (const collection of allCols) {
+ collection.subscriptions = collection.subscriptions.filter(
+ (subscriptionName) => subscriptionName !== name,
+ );
+ }
+ $.write(allCols, COLLECTIONS_KEY);
+ return sub;
+}
+
+function shouldArchiveDeletion(mode) {
+ if (mode == null || mode === '' || mode === 'permanent') {
+ return false;
+ }
+ if (mode === 'archive') {
+ return true;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_DELETE_MODE',
+ `Unsupported delete mode: ${mode}`,
+ );
+}
+
+export { createSubscriptionItem, deleteSubscriptionItem };
diff --git a/backend/src/restful/sync.js b/backend/src/restful/sync.js
new file mode 100644
index 0000000000..b9ed1107d7
--- /dev/null
+++ b/backend/src/restful/sync.js
@@ -0,0 +1,1181 @@
+import $ from '@/core/app';
+import {
+ ARTIFACTS_KEY,
+ COLLECTIONS_KEY,
+ RULES_KEY,
+ SUBS_KEY,
+ FILES_KEY,
+ SETTINGS_KEY,
+} from '@/constants';
+import { failed, success } from '@/restful/response';
+import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
+import {
+ formatArtifactLogName,
+ shouldSyncArtifactInGlobalCron,
+} from '@/utils/artifact-cron';
+import { findByName, updateByName } from '@/utils/database';
+import download from '@/utils/download';
+import { ProxyUtils } from '@/core/proxy-utils';
+import { RuleUtils } from '@/core/rule-utils';
+import {
+ normalizeArtifactSyncBatchSize,
+ syncToGist,
+} from '@/restful/artifacts';
+import {
+ buildEmptySubscriptionOutput,
+ handleIgnoreFailedRemoteSubError,
+ notifyIgnoreFailedRemoteSubFallback,
+ resolveIgnoreFailedRemoteSubMode,
+ shouldFallbackIgnoreFailedRemoteSub,
+} from '@/restful/ignore-failed-remote-sub';
+import { normalizeClashYaml } from '@/core/proxy-utils/preprocessors';
+
+export default function register($app) {
+ // Initialization
+ if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
+
+ // sync all artifacts
+ $app.get('/api/sync/artifacts', syncAllArtifacts);
+ $app.get('/api/sync/artifact/:name', syncArtifact);
+}
+
+async function produceArtifact({
+ type,
+ name,
+ platform,
+ url,
+ ua,
+ content,
+ mergeSources,
+ ignoreFailedRemoteSub,
+ ignoreFailedRemoteFile,
+ produceType,
+ produceOpts = {},
+ subscription,
+ awaitCustomCache,
+ $options,
+ proxy,
+ noCache,
+ all,
+}) {
+ platform = platform || 'JSON';
+
+ if (['subscription', 'sub'].includes(type)) {
+ let sub;
+ if (name) {
+ const allSubs = $.read(SUBS_KEY);
+ sub = findByName(allSubs, name);
+ if (!sub) throw new Error(`找不到订阅 ${name}`);
+ } else if (subscription) {
+ sub = subscription;
+ } else {
+ throw new Error('未提供订阅名称或订阅数据');
+ }
+ const subIgnoreFailedRemoteSub = resolveIgnoreFailedRemoteSubMode(
+ ignoreFailedRemoteSub,
+ sub.ignoreFailedRemoteSub,
+ );
+
+ try {
+ let raw;
+ if (
+ content &&
+ !['localFirst', 'remoteFirst'].includes(mergeSources)
+ ) {
+ raw = content;
+ } else if (url) {
+ const errors = {};
+ raw = await Promise.all(
+ url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ ua || sub.ua,
+ undefined,
+ proxy || sub.proxy,
+ undefined,
+ awaitCustomCache,
+ noCache || sub.noCache,
+ true,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `订阅 ${sub.name} 的远程订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: subIgnoreFailedRemoteSub,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 处理订阅失败`,
+ `❌ ${sub.name}`,
+ message,
+ );
+ },
+ });
+ }
+ if (mergeSources === 'localFirst') {
+ raw.unshift(content);
+ } else if (mergeSources === 'remoteFirst') {
+ raw.push(content);
+ }
+ } else if (
+ sub.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
+ ) {
+ raw = sub.content;
+ } else {
+ const errors = {};
+ raw = await Promise.all(
+ sub.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ ua || sub.ua,
+ undefined,
+ proxy || sub.proxy,
+ undefined,
+ awaitCustomCache,
+ noCache || sub.noCache,
+ true,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `订阅 ${sub.name} 的远程订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: subIgnoreFailedRemoteSub,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 处理订阅失败`,
+ `❌ ${sub.name}`,
+ message,
+ );
+ },
+ });
+ }
+ if (sub.mergeSources === 'localFirst') {
+ raw.unshift(sub.content);
+ } else if (sub.mergeSources === 'remoteFirst') {
+ raw.push(sub.content);
+ }
+ }
+ if (produceType === 'raw') {
+ return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
+ }
+ // parse proxies
+ let proxies = (Array.isArray(raw) ? raw : [raw])
+ .map((i) => ProxyUtils.parse(i))
+ .flat();
+
+ proxies.forEach((proxy) => {
+ proxy._subName = sub.name;
+ proxy._subDisplayName = sub.displayName;
+ });
+ // apply processors
+ proxies = await ProxyUtils.process(
+ proxies,
+ sub.process || [],
+ platform,
+ { [sub.name]: sub },
+ $options,
+ );
+ if (proxies.length === 0) {
+ throw new Error(`订阅 ${name} 中不含有效节点`);
+ }
+ // check duplicate
+ const exist = {};
+ for (const proxy of proxies) {
+ if (exist[proxy.name]) {
+ $.notify(
+ '🌍 Sub-Store',
+ `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}!`,
+ '请仔细检测配置!',
+ {
+ 'media-url':
+ 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
+ },
+ );
+ break;
+ }
+ exist[proxy.name] = true;
+ }
+ // produce
+ return ProxyUtils.produce(
+ proxies,
+ platform,
+ produceType,
+ produceOpts,
+ );
+ } catch (err) {
+ if (!shouldFallbackIgnoreFailedRemoteSub(subIgnoreFailedRemoteSub)) {
+ throw err;
+ }
+
+ notifyIgnoreFailedRemoteSubFallback({
+ mode: subIgnoreFailedRemoteSub,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 处理订阅失败`,
+ `❌ ${sub.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `订阅 ${sub.name} 启用兜底后返回空结果: ${err.message ?? err}`,
+ );
+
+ return buildEmptySubscriptionOutput({
+ platform,
+ produceType,
+ produceOpts,
+ });
+ }
+ } else if (['collection', 'col'].includes(type)) {
+ const allSubs = $.read(SUBS_KEY);
+ const allCols = $.read(COLLECTIONS_KEY);
+ const collection = findByName(allCols, name);
+ if (!collection) throw new Error(`找不到组合订阅 ${name}`);
+ const subnames = [...collection.subscriptions];
+ let subscriptionTags = collection.subscriptionTags;
+ if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
+ allSubs.forEach((sub) => {
+ if (
+ Array.isArray(sub.tag) &&
+ sub.tag.length > 0 &&
+ !subnames.includes(sub.name) &&
+ sub.tag.some((tag) => subscriptionTags.includes(tag))
+ ) {
+ subnames.push(sub.name);
+ }
+ });
+ }
+ const collectionIgnoreFailedRemoteSub = resolveIgnoreFailedRemoteSubMode(
+ ignoreFailedRemoteSub,
+ collection.ignoreFailedRemoteSub,
+ );
+
+ try {
+ const results = {};
+ const errors = {};
+ let processed = 0;
+
+ await Promise.all(
+ subnames.map(async (name) => {
+ const sub = findByName(allSubs, name);
+ const subMode = resolveIgnoreFailedRemoteSubMode(
+ sub.ignoreFailedRemoteSub,
+ );
+ const passThroughUA = sub.passThroughUA;
+ let reqUA = sub.ua;
+ if (passThroughUA) {
+ $.info(
+ `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
+ );
+ reqUA = ua;
+ }
+ try {
+ $.info(`正在处理子订阅:${sub.name}...`);
+ let raw;
+ if (
+ sub.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(
+ sub.mergeSources,
+ )
+ ) {
+ raw = sub.content;
+ } else {
+ const errors = {};
+ raw = await Promise.all(
+ sub.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ reqUA,
+ undefined,
+ proxy ||
+ sub.proxy ||
+ collection.proxy,
+ undefined,
+ undefined,
+ noCache || sub.noCache,
+ true,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `订阅 ${sub.name} 的远程订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: subMode,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 处理订阅失败`,
+ `❌ ${sub.name}`,
+ message,
+ );
+ },
+ });
+ }
+ if (sub.mergeSources === 'localFirst') {
+ raw.unshift(sub.content);
+ } else if (sub.mergeSources === 'remoteFirst') {
+ raw.push(sub.content);
+ }
+ }
+ // parse proxies
+ let currentProxies = (Array.isArray(raw) ? raw : [raw])
+ .map((i) => ProxyUtils.parse(i))
+ .flat();
+
+ currentProxies.forEach((proxy) => {
+ proxy._subName = sub.name;
+ proxy._subDisplayName = sub.displayName;
+ proxy._collectionName = collection.name;
+ proxy._collectionDisplayName =
+ collection.displayName;
+ });
+
+ // apply processors
+ currentProxies = await ProxyUtils.process(
+ currentProxies,
+ sub.process || [],
+ platform,
+ {
+ [sub.name]: sub,
+ _collection: collection,
+ $options,
+ },
+ );
+ results[name] = currentProxies;
+ processed++;
+ $.info(
+ `✅ 子订阅:${sub.name}加载成功,进度--${
+ 100 * (processed / subnames.length).toFixed(1)
+ }% `,
+ );
+ } catch (err) {
+ processed++;
+
+ if (shouldFallbackIgnoreFailedRemoteSub(subMode)) {
+ notifyIgnoreFailedRemoteSubFallback({
+ mode: subMode,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 处理订阅失败`,
+ `❌ ${sub.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `订阅 ${sub.name} 在组合订阅处理中启用兜底后返回空结果: ${
+ err.message ?? err
+ }`,
+ );
+ results[name] = [];
+ return;
+ }
+
+ errors[name] = err;
+ $.error(
+ `❌ 处理组合订阅中的子订阅: ${
+ sub.name
+ }时出现错误:${err}!进度--${
+ 100 * (processed / subnames.length).toFixed(1)
+ }%`,
+ );
+ }
+ }),
+ );
+
+ if (Object.keys(errors).length > 0) {
+ const message = `组合订阅 ${collection.name} 的子订阅 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`;
+ handleIgnoreFailedRemoteSubError({
+ mode: collectionIgnoreFailedRemoteSub,
+ message,
+ notify: () => {
+ $.notify(
+ `🌍 Sub-Store 处理组合订阅失败`,
+ `❌ ${collection.name}`,
+ message,
+ );
+ },
+ });
+ }
+
+ // merge proxies with the original order
+ let proxies = Array.prototype.concat.apply(
+ [],
+ subnames.map((name) => results[name] || []),
+ );
+
+ proxies.forEach((proxy) => {
+ proxy._collectionName = collection.name;
+ proxy._collectionDisplayName = collection.displayName;
+ });
+
+ // apply own processors
+ proxies = await ProxyUtils.process(
+ proxies,
+ collection.process || [],
+ platform,
+ { _collection: collection },
+ $options,
+ );
+ if (proxies.length === 0) {
+ throw new Error(`组合订阅 ${name} 中不含有效节点`);
+ }
+ // check duplicate
+ const exist = {};
+ for (const proxy of proxies) {
+ if (exist[proxy.name]) {
+ $.notify(
+ '🌍 Sub-Store',
+ `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}!`,
+ '请仔细检测配置!',
+ {
+ 'media-url':
+ 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
+ },
+ );
+ break;
+ }
+ exist[proxy.name] = true;
+ }
+ return ProxyUtils.produce(
+ proxies,
+ platform,
+ produceType,
+ produceOpts,
+ );
+ } catch (err) {
+ if (
+ !shouldFallbackIgnoreFailedRemoteSub(
+ collectionIgnoreFailedRemoteSub,
+ )
+ ) {
+ throw err;
+ }
+
+ notifyIgnoreFailedRemoteSubFallback({
+ mode: collectionIgnoreFailedRemoteSub,
+ error: err,
+ notify: (error) => {
+ $.notify(
+ `🌍 Sub-Store 处理组合订阅失败`,
+ `❌ ${collection.name}`,
+ `🤔 原因:${error.message ?? error}`,
+ );
+ },
+ });
+ $.error(
+ `组合订阅 ${collection.name} 启用兜底后返回空结果: ${
+ err.message ?? err
+ }`,
+ );
+
+ return buildEmptySubscriptionOutput({
+ platform,
+ produceType,
+ produceOpts,
+ });
+ }
+ } else if (type === 'rule') {
+ const allRules = $.read(RULES_KEY);
+ const rule = findByName(allRules, name);
+ if (!rule) throw new Error(`找不到规则 ${name}`);
+ let rules = [];
+ for (let i = 0; i < rule.urls.length; i++) {
+ const url = rule.urls[i];
+ $.info(
+ `正在处理URL:${url},进度--${
+ 100 * ((i + 1) / rule.urls.length).toFixed(1)
+ }% `,
+ );
+ try {
+ const { body } = await download(url);
+ const currentRules = RuleUtils.parse(body);
+ rules = rules.concat(currentRules);
+ } catch (err) {
+ $.error(
+ `处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`,
+ );
+ }
+ }
+ // remove duplicates
+ rules = await RuleUtils.process(rules, [
+ { type: 'Remove Duplicate Filter' },
+ ]);
+ // produce output
+ return RuleUtils.produce(rules, platform);
+ } else if (type === 'file') {
+ const allFiles = $.read(FILES_KEY);
+ const file = findByName(allFiles, name);
+ if (!file) throw new Error(`找不到文件 ${name}`);
+ let raw = '';
+ if (file.type !== 'mihomoProfile') {
+ if (
+ content &&
+ !['localFirst', 'remoteFirst'].includes(mergeSources)
+ ) {
+ raw = content;
+ } else if (url) {
+ const errors = {};
+ raw = await Promise.all(
+ url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ ua || file.ua,
+ undefined,
+ file.proxy || proxy,
+ undefined,
+ undefined,
+ noCache,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+ let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
+ if (
+ ignoreFailedRemoteFile != null &&
+ ignoreFailedRemoteFile !== ''
+ ) {
+ fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
+ }
+ if (
+ !fileIgnoreFailedRemoteFile &&
+ Object.keys(errors).length > 0
+ ) {
+ throw new Error(
+ `文件 ${file.name} 的远程文件 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`,
+ );
+ }
+ if (mergeSources === 'localFirst') {
+ raw.unshift(content);
+ } else if (mergeSources === 'remoteFirst') {
+ raw.push(content);
+ }
+ } else if (
+ file.source === 'local' &&
+ !['localFirst', 'remoteFirst'].includes(file.mergeSources)
+ ) {
+ raw = file.content;
+ } else {
+ const errors = {};
+ raw = await Promise.all(
+ file.url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)
+ .map(async (url) => {
+ try {
+ return await download(
+ url,
+ ua || file.ua,
+ undefined,
+ file.proxy || proxy,
+ undefined,
+ undefined,
+ noCache,
+ );
+ } catch (err) {
+ errors[url] = err;
+ $.error(
+ `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
+ );
+ return '';
+ }
+ }),
+ );
+ let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
+ if (
+ ignoreFailedRemoteFile != null &&
+ ignoreFailedRemoteFile !== ''
+ ) {
+ fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
+ }
+
+ if (Object.keys(errors).length > 0) {
+ if (!fileIgnoreFailedRemoteFile) {
+ throw new Error(
+ `文件 ${file.name} 的远程文件 ${Object.keys(
+ errors,
+ ).join(', ')} 发生错误, 请查看日志`,
+ );
+ } else if (fileIgnoreFailedRemoteFile === 'enabled') {
+ $.notify(
+ `🌍 Sub-Store 处理文件失败`,
+ `❌ ${file.name}`,
+ `远程文件 ${Object.keys(errors).join(
+ ', ',
+ )} 发生错误, 请查看日志`,
+ );
+ }
+ }
+ if (file.mergeSources === 'localFirst') {
+ raw.unshift(file.content);
+ } else if (file.mergeSources === 'remoteFirst') {
+ raw.push(file.content);
+ }
+ }
+ }
+ if (produceType === 'raw') {
+ return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
+ }
+ const files = (Array.isArray(raw) ? raw : [raw]).flat();
+ let filesContent = files
+ .filter((i) => i != null && i !== '')
+ .join('\n');
+
+ // apply processors
+ const processed =
+ Array.isArray(file.process) && file.process.length > 0
+ ? await ProxyUtils.process(
+ {
+ $files: files,
+ $content: filesContent,
+ $options,
+ $file: file,
+ },
+ file.process,
+ )
+ : { $content: filesContent, $files: files, $options };
+
+ processed.$content = normalizeClashYaml(processed?.$content ?? '');
+
+ return (all ? processed : processed?.$content) ?? '';
+ }
+}
+
+function createArtifactUploadBatches(names, batchSize) {
+ const batches = [];
+ for (let index = 0; index < names.length; index += batchSize) {
+ batches.push(names.slice(index, index + batchSize));
+ }
+ return batches;
+}
+
+function normalizeUploadResponseFiles(files) {
+ if (Array.isArray(files)) {
+ return {
+ isGitLab: true,
+ files: Object.fromEntries(files.map((item) => [item.path, item])),
+ };
+ }
+
+ return {
+ isGitLab: false,
+ files: files || {},
+ };
+}
+
+function logUploadResponse(body) {
+ delete body.history;
+ delete body.forks;
+ delete body.owner;
+ if (body.files) {
+ Object.values(body.files).forEach((file) => {
+ delete file.content;
+ });
+ }
+ $.info('上传配置响应:');
+ $.info(JSON.stringify(body, null, 2));
+}
+
+function resolveArtifactUploadUrl(body, artifactName) {
+ const { files, isGitLab } = normalizeUploadResponseFiles(body.files);
+ const encodedName = encodeURIComponent(artifactName);
+ const raw_url = files[encodedName]?.raw_url;
+ const new_url = isGitLab
+ ? raw_url
+ : raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
+ $.info(
+ `上传配置完成\n文件列表: ${Object.keys(files).join(
+ ', ',
+ )}\n当前文件: ${encodedName}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
+ );
+ return new_url;
+}
+
+function shouldUploadArtifact(artifact) {
+ return artifact?.upload !== false;
+}
+
+function markArtifactProducedWithoutUpload(artifact) {
+ artifact.updated = new Date().getTime();
+ delete artifact.url;
+}
+
+function patchArtifactSyncResult(name, patcher) {
+ const latestArtifacts = $.read(ARTIFACTS_KEY);
+ const currentArtifact = findByName(latestArtifacts, name);
+ if (!currentArtifact) {
+ $.info(`远程配置 ${name} 已不存在, 跳过同步结果写入`);
+ return null;
+ }
+
+ const nextArtifact = patcher({ ...currentArtifact });
+ updateByName(latestArtifacts, name, nextArtifact);
+ $.write(latestArtifacts, ARTIFACTS_KEY);
+ return nextArtifact;
+}
+
+async function uploadArtifactBatches({ allArtifacts, files, valid, invalid }) {
+ const settings = $.read(SETTINGS_KEY) || {};
+ const batchSize = normalizeArtifactSyncBatchSize(
+ settings.artifactSyncBatchSize,
+ );
+ const uploadNames = valid.filter((name) => {
+ const artifact = findByName(allArtifacts, name);
+ return artifact && shouldUploadArtifact(artifact);
+ });
+ const batches = createArtifactUploadBatches(uploadNames, batchSize);
+ const uploaded = [];
+
+ if (uploadNames.length === 0) {
+ $.info('没有需要上传的同步配置');
+ return uploaded;
+ }
+
+ $.info(
+ `准备分批上传同步配置: 共 ${uploadNames.length} 个, 每批 ${batchSize} 个, 批次数 ${batches.length}`,
+ );
+
+ for (let index = 0; index < batches.length; index++) {
+ const batchNames = batches[index];
+ const batchFiles = Object.fromEntries(
+ batchNames.map((name) => [
+ encodeURIComponent(name),
+ files[encodeURIComponent(name)],
+ ]),
+ );
+
+ try {
+ $.info(
+ `正在上传第 ${index + 1}/${
+ batches.length
+ } 批同步配置: ${batchNames.join(', ')}`,
+ );
+ const resp = await syncToGist(batchFiles);
+ const body = JSON.parse(resp.body);
+ logUploadResponse(body);
+
+ for (const artifact of allArtifacts) {
+ if (
+ artifact.sync &&
+ artifact.source &&
+ batchNames.includes(artifact.name)
+ ) {
+ const newUrl = resolveArtifactUploadUrl(
+ body,
+ artifact.name,
+ );
+ if (newUrl) {
+ artifact.updated = new Date().getTime();
+ artifact.url = newUrl;
+ uploaded.push(artifact.name);
+ } else {
+ $.error(
+ `同步配置 ${artifact.name} 上传成功但响应中未找到文件链接`,
+ );
+ invalid.push(artifact.name);
+ }
+ }
+ }
+ } catch (e) {
+ $.error(
+ `第 ${index + 1}/${
+ batches.length
+ } 批同步配置上传失败: ${batchNames.join(', ')}, 原因: ${
+ e.message ?? e
+ }`,
+ );
+ invalid.push(...batchNames);
+ }
+ }
+
+ return uploaded;
+}
+
+function shouldSyncArtifact(artifact, { skipCronArtifacts = false } = {}) {
+ return (
+ artifact.sync &&
+ artifact.source &&
+ (!skipCronArtifacts || shouldSyncArtifactInGlobalCron(artifact))
+ );
+}
+
+async function syncArtifacts(options = {}) {
+ $.info('开始同步所有远程配置...');
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const files = {};
+
+ try {
+ const valid = [];
+ const invalid = [];
+ const producedWithoutUpload = [];
+ const allSubs = $.read(SUBS_KEY);
+ const allCols = $.read(COLLECTIONS_KEY);
+ const subNames = [];
+ let enabledCount = 0;
+ allArtifacts.map((artifact) => {
+ if (shouldSyncArtifact(artifact, options)) {
+ enabledCount++;
+ if (artifact.type === 'subscription') {
+ const subName = artifact.source;
+ const sub = findByName(allSubs, subName);
+ if (sub && sub.url && !subNames.includes(subName)) {
+ subNames.push(subName);
+ }
+ } else if (artifact.type === 'collection') {
+ const collection = findByName(allCols, artifact.source);
+ if (collection && Array.isArray(collection.subscriptions)) {
+ collection.subscriptions.map((subName) => {
+ const sub = findByName(allSubs, subName);
+ if (sub && sub.url && !subNames.includes(subName)) {
+ subNames.push(subName);
+ }
+ });
+ }
+ }
+ }
+ });
+
+ if (enabledCount === 0) {
+ $.info(
+ `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
+ );
+ return;
+ }
+
+ if (subNames.length > 0) {
+ await Promise.all(
+ subNames.map(async (subName) => {
+ try {
+ await produceArtifact({
+ type: 'subscription',
+ name: subName,
+ awaitCustomCache: true,
+ });
+ } catch (e) {
+ // $.error(`${e.message ?? e}`);
+ }
+ }),
+ );
+ }
+
+ await Promise.all(
+ allArtifacts.map(async (artifact) => {
+ try {
+ if (shouldSyncArtifact(artifact, options)) {
+ $.info(
+ `正在同步云配置:${formatArtifactLogName(
+ artifact,
+ )}...`,
+ );
+
+ const useMihomoExternal =
+ artifact.platform === 'SurgeMac';
+
+ if (useMihomoExternal) {
+ $.info(
+ `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
+ );
+ }
+
+ const output = await produceArtifact({
+ type: artifact.type,
+ name: artifact.source,
+ platform: artifact.platform,
+ produceOpts: {
+ 'include-unsupported-proxy':
+ artifact.includeUnsupportedProxy,
+ useMihomoExternal,
+ prettyYaml: artifact.prettyYaml,
+ },
+ });
+
+ // if (!output || output.length === 0)
+ // throw new Error('该配置的结果为空 不进行上传');
+
+ if (shouldUploadArtifact(artifact)) {
+ files[encodeURIComponent(artifact.name)] = {
+ content: output,
+ };
+ valid.push(artifact.name);
+ } else {
+ markArtifactProducedWithoutUpload(artifact);
+ producedWithoutUpload.push(artifact.name);
+ }
+ }
+ } catch (e) {
+ $.error(
+ `生成同步配置 ${formatArtifactLogName(
+ artifact,
+ )} 发生错误: ${
+ e.message ?? e
+ }`,
+ );
+ invalid.push(artifact.name);
+ }
+ }),
+ );
+
+ const producedCount = valid.length + producedWithoutUpload.length;
+ $.info(
+ `${producedCount} 个同步配置生成成功: ${valid
+ .concat(producedWithoutUpload)
+ .join(', ')}`,
+ );
+ $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
+ if (producedWithoutUpload.length > 0) {
+ $.info(
+ `${
+ producedWithoutUpload.length
+ } 个同步配置仅生成未上传: ${producedWithoutUpload.join(', ')}`,
+ );
+ }
+
+ if (producedCount === 0) {
+ throw new Error(
+ `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
+ );
+ }
+
+ const uploaded = await uploadArtifactBatches({
+ allArtifacts,
+ files,
+ valid,
+ invalid,
+ });
+
+ $.write(allArtifacts, ARTIFACTS_KEY);
+ $.info('同步配置执行完成');
+
+ if (invalid.length > 0) {
+ throw new Error(
+ `同步配置成功 ${
+ uploaded.length + producedWithoutUpload.length
+ } 个, 失败 ${invalid.length} 个, 详情请查看日志`,
+ );
+ } else {
+ $.info(
+ `同步配置成功 ${
+ uploaded.length + producedWithoutUpload.length
+ } 个`,
+ );
+ }
+ } catch (e) {
+ $.error(`同步配置失败,原因:${e.message ?? e}`);
+ throw e;
+ }
+}
+async function syncAllArtifacts(_, res) {
+ $.info('开始同步所有远程配置...');
+ try {
+ await syncArtifacts();
+ success(res);
+ } catch (e) {
+ $.error(`同步配置失败,原因:${e.message ?? e}`);
+ failed(
+ res,
+ new InternalServerError(
+ `FAILED_TO_SYNC_ARTIFACTS`,
+ `Failed to sync all artifacts`,
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+async function syncArtifactItem(name) {
+ const allArtifacts = $.read(ARTIFACTS_KEY);
+ const artifact = findByName(allArtifacts, name);
+
+ if (!artifact) {
+ $.error(`找不到远程配置 ${name}`);
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `找不到远程配置 ${name}`,
+ );
+ }
+
+ if (!artifact.source) {
+ $.error(`远程配置 ${formatArtifactLogName(artifact)} 未设置来源`);
+ throw new ResourceNotFoundError(
+ 'RESOURCE_HAS_NO_SOURCE',
+ `远程配置 ${formatArtifactLogName(artifact)} 未设置来源`,
+ );
+ }
+
+ $.info(`开始同步远程配置 ${formatArtifactLogName(artifact)}...`);
+
+ const useMihomoExternal = artifact.platform === 'SurgeMac';
+
+ if (useMihomoExternal) {
+ $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
+ }
+ const output = await produceArtifact({
+ type: artifact.type,
+ name: artifact.source,
+ platform: artifact.platform,
+ produceOpts: {
+ 'include-unsupported-proxy': artifact.includeUnsupportedProxy,
+ useMihomoExternal,
+ prettyYaml: artifact.prettyYaml,
+ },
+ });
+
+ // if (!output || output.length === 0)
+ // throw new Error('该配置的结果为空 不进行上传');
+ if (!shouldUploadArtifact(artifact)) {
+ $.info(
+ `配置 ${formatArtifactLogName(
+ artifact,
+ )} 已关闭上传, 仅更新执行时间`,
+ );
+ const updated = new Date().getTime();
+ const patchedArtifact = patchArtifactSyncResult(
+ name,
+ (currentArtifact) => {
+ currentArtifact.updated = updated;
+ delete currentArtifact.url;
+ return currentArtifact;
+ },
+ );
+ if (patchedArtifact) return patchedArtifact;
+
+ const fallbackArtifact = {
+ ...artifact,
+ updated,
+ };
+ delete fallbackArtifact.url;
+ return fallbackArtifact;
+ }
+
+ $.info(
+ `正在上传配置:${formatArtifactLogName(artifact)}\n>>>${JSON.stringify(
+ artifact,
+ null,
+ 2,
+ )}`,
+ );
+ const resp = await syncToGist({
+ [encodeURIComponent(artifact.name)]: {
+ content: output,
+ },
+ });
+ const updated = new Date().getTime();
+ const body = JSON.parse(resp.body);
+
+ logUploadResponse(body);
+ const new_url = resolveArtifactUploadUrl(body, artifact.name);
+ const patchedArtifact = patchArtifactSyncResult(
+ name,
+ (currentArtifact) => ({
+ ...currentArtifact,
+ updated,
+ url: new_url,
+ }),
+ );
+ return (
+ patchedArtifact || {
+ ...artifact,
+ updated,
+ url: new_url,
+ }
+ );
+}
+
+async function syncArtifact(req, res) {
+ let { name } = req.params;
+
+ try {
+ const artifact = await syncArtifactItem(name);
+ success(res, artifact);
+ } catch (err) {
+ $.error(`远程配置 ${name} 发生错误: ${err.message ?? err}`);
+ failed(
+ res,
+ err instanceof ResourceNotFoundError
+ ? err
+ : new InternalServerError(
+ `FAILED_TO_SYNC_ARTIFACT`,
+ `Failed to sync artifact ${name}`,
+ `Reason: ${err}`,
+ ),
+ err instanceof ResourceNotFoundError ? 404 : undefined,
+ );
+ }
+}
+
+export {
+ markArtifactProducedWithoutUpload,
+ produceArtifact,
+ shouldUploadArtifact,
+ syncArtifactItem,
+ syncArtifacts,
+ uploadArtifactBatches,
+};
diff --git a/backend/src/restful/token.js b/backend/src/restful/token.js
new file mode 100644
index 0000000000..ca23b704c3
--- /dev/null
+++ b/backend/src/restful/token.js
@@ -0,0 +1,330 @@
+import { ENV } from '@/vendor/open-api';
+import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants';
+import { failed, success } from '@/restful/response';
+import $ from '@/core/app';
+import {
+ RequestInvalidError,
+ InternalServerError,
+ ResourceNotFoundError,
+} from '@/restful/errors';
+import { insertByPosition } from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+import { archiveShare } from '@/utils/archive';
+
+export default function register($app) {
+ if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY);
+
+ $app.post('/api/token', signToken);
+
+ $app.route('/api/token/:token').delete(deleteToken);
+
+ $app.route('/api/tokens').get(getAllTokens);
+}
+
+function deleteToken(req, res) {
+ try {
+ let { token } = req.params;
+ const { type, name } = req.query;
+ if (!type || !name) {
+ throw new RequestInvalidError(
+ 'INVALID_PAYLOAD',
+ `Payload type and name are required. Please update your front-end(version >= 2.15.76)`,
+ );
+ }
+ $.info(`正在删除...\ntoken: ${token}, 类型:${type}, 名称:${name}`);
+ if (shouldArchiveDeletion(req.query.mode)) {
+ archiveShare(token, type, name);
+ }
+ deleteTokenItem(token, type, name);
+ success(res);
+ } catch (error) {
+ failed(res, error);
+ }
+}
+
+function getAllTokens(req, res) {
+ const { type, name } = req.query;
+ const allTokens = $.read(TOKENS_KEY) || [];
+ success(
+ res,
+ type || name
+ ? allTokens.filter(
+ (item) =>
+ (type ? item.type === type : true) &&
+ (name ? item.name === name : true),
+ )
+ : allTokens,
+ );
+}
+
+async function signToken(req, res) {
+ if (!ENV().isNode) {
+ return failed(
+ res,
+ new RequestInvalidError(
+ 'INVALID_ENV',
+ `This endpoint is only available in Node.js environment`,
+ ),
+ );
+ }
+ try {
+ const tokenData = createTokenItem(req.body.payload, req.body.options);
+ return success(res, {
+ token: tokenData.token,
+ // secret,
+ });
+ } catch (e) {
+ return failed(
+ res,
+ e instanceof RequestInvalidError
+ ? e
+ : new InternalServerError(
+ 'TOKEN_SIGN_FAILED',
+ `Failed to sign token`,
+ `Reason: ${e.message ?? e}`,
+ ),
+ );
+ }
+}
+
+function normalizeExpirationMode(mode) {
+ if (mode == null || mode === '') {
+ return undefined;
+ }
+ if (mode === 'duration' || mode === 'datetime') {
+ return mode;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRATION_MODE',
+ `Unsupported expiration mode: ${mode}`,
+ );
+}
+
+function inferLegacyExpirationMode(options = {}) {
+ if (options?.expiresIn != null && options.expiresIn !== '') {
+ return 'duration';
+ }
+ if (options?.exp != null && options.exp !== '') {
+ return 'datetime';
+ }
+ return undefined;
+}
+
+function resolveExactExpiration(options = {}) {
+ const rawExp = options?.exp;
+ if (typeof rawExp !== 'number' && typeof rawExp !== 'string') {
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRATION_DATETIME',
+ `Invalid exp option: ${rawExp}`,
+ );
+ }
+
+ const normalizedRawExp =
+ typeof rawExp === 'string' ? rawExp.trim() : rawExp;
+ if (normalizedRawExp === '') {
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRATION_DATETIME',
+ `Invalid exp option: ${rawExp}`,
+ );
+ }
+
+ const exp = Number(normalizedRawExp);
+ if (
+ !Number.isSafeInteger(exp) ||
+ exp <= 0 ||
+ // Require an explicit millisecond Unix timestamp to avoid
+ // silently accepting second-based values from non-frontend callers.
+ exp < 1000000000000
+ ) {
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRATION_DATETIME',
+ `Invalid exp option: ${rawExp}`,
+ );
+ }
+ return exp;
+}
+
+function resolveDurationExpiration(options = {}, { required = false } = {}) {
+ const rawExpiresIn = options?.expiresIn;
+ if (rawExpiresIn == null || rawExpiresIn === '') {
+ if (required) {
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRES_IN',
+ `Invalid expiresIn option: ${rawExpiresIn}`,
+ );
+ }
+ return null;
+ }
+
+ const ms = eval(`require("ms")`);
+ const expiresIn = ms(rawExpiresIn);
+ if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
+ throw new RequestInvalidError(
+ 'INVALID_EXPIRES_IN',
+ `Invalid expiresIn option: ${rawExpiresIn}`,
+ );
+ }
+
+ return {
+ rawExpiresIn,
+ exp: Date.now() + expiresIn,
+ };
+}
+
+function createTokenItem(payload, options = {}) {
+ const type = payload?.type;
+ const name = payload?.name;
+ if (!type || !name) {
+ throw new RequestInvalidError(
+ 'INVALID_PAYLOAD',
+ `payload type and name are required`,
+ );
+ }
+ let token = payload?.token;
+ if (token != null) {
+ if (typeof token !== 'string' || token.length < 1) {
+ throw new RequestInvalidError(
+ 'INVALID_CUSTOM_TOKEN',
+ `Invalid custom token: ${token}`,
+ );
+ }
+ const tokens = $.read(TOKENS_KEY) || [];
+ if (
+ tokens.find(
+ (item) =>
+ item.token === token &&
+ item.type === type &&
+ item.name === name,
+ )
+ ) {
+ throw new RequestInvalidError(
+ 'DUPLICATE_TOKEN',
+ `Token ${token} already exists`,
+ );
+ }
+ }
+
+ if (type === 'col') {
+ const collections = $.read(COLLECTIONS_KEY) || [];
+ const collection = collections.find((item) => item.name === name);
+ if (!collection) {
+ throw new RequestInvalidError(
+ 'INVALID_COLLECTION',
+ `collection ${name} not found`,
+ );
+ }
+ } else if (type === 'file') {
+ const files = $.read(FILES_KEY) || [];
+ const file = files.find((item) => item.name === name);
+ if (!file) {
+ throw new RequestInvalidError(
+ 'INVALID_FILE',
+ `file ${name} not found`,
+ );
+ }
+ } else if (type === 'sub') {
+ const subs = $.read(SUBS_KEY) || [];
+ const sub = subs.find((item) => item.name === name);
+ if (!sub) {
+ throw new RequestInvalidError(
+ 'INVALID_SUB',
+ `sub ${name} not found`,
+ );
+ }
+ } else {
+ throw new RequestInvalidError(
+ 'INVALID_TYPE',
+ `type ${name} not supported`,
+ );
+ }
+
+ const expirationMode =
+ normalizeExpirationMode(options?.mode) ??
+ inferLegacyExpirationMode(options);
+ let durationExpiration = null;
+ let exp;
+ if (expirationMode === 'datetime') {
+ exp = resolveExactExpiration(options);
+ } else {
+ durationExpiration = resolveDurationExpiration(options, {
+ required: expirationMode === 'duration',
+ });
+ exp = durationExpiration?.exp;
+ }
+
+ const nanoid = eval(`require("nanoid")`);
+ const tokens = $.read(TOKENS_KEY) || [];
+ if (!token) {
+ do {
+ token = nanoid.customAlphabet(nanoid.urlAlphabet)();
+ } while (
+ tokens.find(
+ (item) =>
+ item.token === token &&
+ item.type === type &&
+ item.name === name,
+ )
+ );
+ }
+ const normalizedMode =
+ expirationMode === 'datetime'
+ ? 'datetime'
+ : durationExpiration
+ ? 'duration'
+ : undefined;
+ const safePayload = { ...payload };
+ delete safePayload.mode;
+ delete safePayload.exp;
+ delete safePayload.expiresIn;
+ const tokenData = {
+ ...safePayload,
+ token,
+ createdAt: Date.now(),
+ ...(normalizedMode ? { mode: normalizedMode } : {}),
+ ...(normalizedMode === 'datetime'
+ ? { exp }
+ : durationExpiration
+ ? {
+ expiresIn: durationExpiration.rawExpiresIn,
+ exp,
+ }
+ : {}),
+ };
+ insertByPosition(tokens, tokenData, getCreateItemPosition());
+ $.write(tokens, TOKENS_KEY);
+ return tokenData;
+}
+
+function deleteTokenItem(token, type, name) {
+ const allTokens = $.read(TOKENS_KEY) || [];
+ const match = allTokens.find(
+ (item) => item.token === token && item.type === type && item.name === name,
+ );
+ if (!match) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Share ${type}/${name}/${token} does not exist!`,
+ );
+ }
+ const filtered = allTokens.filter(
+ (item) =>
+ !(item.token === token && item.type === type && item.name === name),
+ );
+ $.write(filtered, TOKENS_KEY);
+ return match;
+}
+
+function shouldArchiveDeletion(mode) {
+ if (mode == null || mode === '' || mode === 'permanent') {
+ return false;
+ }
+ if (mode === 'archive') {
+ return true;
+ }
+ throw new RequestInvalidError(
+ 'INVALID_DELETE_MODE',
+ `Unsupported delete mode: ${mode}`,
+ );
+}
+
+export { createTokenItem, deleteTokenItem };
diff --git a/backend/src/test/products/resource-parser.loon.spec.js b/backend/src/test/products/resource-parser.loon.spec.js
new file mode 100644
index 0000000000..41dfcde681
--- /dev/null
+++ b/backend/src/test/products/resource-parser.loon.spec.js
@@ -0,0 +1,62 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { ProxyUtils } from '@/core/proxy-utils';
+
+describe('Loon resource parser', function () {
+ const modulePath = require.resolve('../../products/resource-parser.loon.js');
+
+ function cleanupGlobals() {
+ delete global.$argument;
+ delete global.$done;
+ delete global.$loon;
+ delete global.$resource;
+ delete global.$resourceType;
+ delete global.$resourceUrl;
+ }
+
+ function resetModule() {
+ delete require.cache[modulePath];
+ }
+
+ it('does not auto-enable includeUnsupportedProxy from build number', async function () {
+ const originalParse = ProxyUtils.parse;
+ const originalProduce = ProxyUtils.produce;
+ let capturedOpts;
+
+ try {
+ ProxyUtils.parse = () => [{ type: 'ss' }];
+ ProxyUtils.produce = (proxies, platform, type, opts = {}) => {
+ capturedOpts = opts;
+ return 'stub-output';
+ };
+
+ global.$loon = 'Loon(842)';
+ global.$resource = 'stub-resource';
+ global.$resourceType = 1;
+ global.$resourceUrl = '';
+
+ const result = await new Promise((resolve, reject) => {
+ global.$done = resolve;
+
+ try {
+ resetModule();
+ require(modulePath);
+ } catch (error) {
+ reject(error);
+ }
+ });
+
+ expect(result).to.equal('stub-output');
+ expect(capturedOpts).to.be.an('object');
+ expect(capturedOpts['include-unsupported-proxy']).to.equal(
+ undefined,
+ );
+ } finally {
+ ProxyUtils.parse = originalParse;
+ ProxyUtils.produce = originalProduce;
+ resetModule();
+ cleanupGlobals();
+ }
+ });
+});
diff --git a/backend/src/test/proxy-parsers/helpers.js b/backend/src/test/proxy-parsers/helpers.js
new file mode 100644
index 0000000000..a203fd2815
--- /dev/null
+++ b/backend/src/test/proxy-parsers/helpers.js
@@ -0,0 +1,36 @@
+import { expect } from 'chai';
+
+import { ProxyUtils } from '@/core/proxy-utils';
+
+export const UUID = '11111111-1111-4111-8111-111111111111';
+
+export function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+export function parseAll(raw) {
+ return ProxyUtils.parse(raw);
+}
+
+export function parseOne(raw) {
+ const proxies = parseAll(raw);
+ expect(proxies, raw).to.have.length(1);
+ return proxies[0];
+}
+
+export function expectSubset(actual, expected, path = 'proxy') {
+ if (Array.isArray(expected)) {
+ expect(actual, path).to.deep.equal(expected);
+ return;
+ }
+
+ if (expected && typeof expected === 'object') {
+ expect(actual, path).to.be.an('object');
+ for (const [key, value] of Object.entries(expected)) {
+ expectSubset(actual[key], value, `${path}.${key}`);
+ }
+ return;
+ }
+
+ expect(actual, path).to.deep.equal(expected);
+}
diff --git a/backend/src/test/proxy-parsers/pipeline.spec.js b/backend/src/test/proxy-parsers/pipeline.spec.js
new file mode 100644
index 0000000000..2039ac3d25
--- /dev/null
+++ b/backend/src/test/proxy-parsers/pipeline.spec.js
@@ -0,0 +1,247 @@
+import { Base64 } from 'js-base64';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { parseAll, parseOne, expectSubset, UUID } from './helpers';
+
+describe('Proxy parser pipeline coverage', function () {
+ it('parses base64-encoded subscriptions', function () {
+ const raw = Base64.encode(
+ [
+ 'https://alice:pa%24%24@https.example.com#HTTPS%20Default',
+ `socks://${encodeURIComponent(
+ Base64.encode('bob:secret'),
+ )}@socks.example.com:1080#SOCKS`,
+ ].join('\n'),
+ );
+
+ const proxies = parseAll(raw);
+
+ expect(proxies).to.have.length(2);
+ expectSubset(proxies[0], {
+ type: 'http',
+ name: 'HTTPS Default',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ username: 'alice',
+ password: 'pa$$',
+ });
+ expectSubset(proxies[1], {
+ type: 'socks5',
+ name: 'SOCKS',
+ server: 'socks.example.com',
+ port: 1080,
+ username: 'bob',
+ password: 'secret',
+ });
+ });
+
+ it('parses SSD subscriptions into shadowsocks nodes', function () {
+ const payload = {
+ port: 8388,
+ encryption: 'aes-128-gcm',
+ password: 'shared-secret',
+ servers: [
+ {
+ server: 'ssd.example.com',
+ remarks: 'SSD Node',
+ plugin: 'obfs-local',
+ plugin_options: 'obfs=http;obfs-host=cdn.example.com',
+ },
+ ],
+ };
+
+ const proxy = parseOne(`ssd://${Base64.encode(JSON.stringify(payload))}`);
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SSD Node',
+ server: 'ssd.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'shared-secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'cdn.example.com',
+ },
+ });
+ });
+
+ it('extracts [Proxy] blocks from full config documents before parsing', function () {
+ const raw = `[General]
+skip-proxy = 192.168.0.0/16
+
+[Proxy]
+Direct = direct
+HTTP = http,full-config.example.com,8080,username=user,password=pass
+
+[Rule]
+FINAL,DIRECT
+`;
+
+ const proxies = parseAll(raw);
+
+ expect(proxies).to.have.length(2);
+ expectSubset(proxies[0], {
+ type: 'direct',
+ name: 'Direct',
+ });
+ expectSubset(proxies[1], {
+ type: 'http',
+ name: 'HTTP',
+ server: 'full-config.example.com',
+ port: 8080,
+ username: 'user',
+ password: 'pass',
+ });
+ });
+
+ it('parses full Clash YAML documents end-to-end', function () {
+ const raw = `proxies:
+ - name: clash-vless
+ type: vless
+ server: clash.example.com
+ port: 443
+ uuid: ${UUID}
+ servername: sni.example.com
+ reality-opts:
+ public-key: pubkey
+ short-id: 08
+ - name: clash-http
+ type: http
+ server: http.clash.example.com
+ port: 8080
+ benchmark-url: https://check.example.com
+ benchmark-timeout: 9
+`;
+
+ const proxies = parseAll(raw);
+
+ expect(proxies).to.have.length(2);
+ expectSubset(proxies[0], {
+ type: 'vless',
+ name: 'clash-vless',
+ server: 'clash.example.com',
+ port: 443,
+ uuid: UUID,
+ sni: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ expectSubset(proxies[1], {
+ type: 'http',
+ name: 'clash-http',
+ server: 'http.clash.example.com',
+ port: 8080,
+ 'test-url': 'https://check.example.com',
+ 'test-timeout': 9,
+ });
+ });
+
+ it('splits mihomo hop-interval ranges for Clash-style object inputs', function () {
+ const proxy = parseOne(
+ JSON.stringify({
+ name: 'hy2-range-inline',
+ type: 'hysteria2',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ 'hop-interval': '15-30',
+ }),
+ );
+
+ expectSubset(proxy, {
+ name: 'hy2-range-inline',
+ type: 'hysteria2',
+ 'hop-interval': 15,
+ 'hop-interval-max': 30,
+ });
+ });
+
+ it('drops invalid mihomo hop-interval values for Clash-style object inputs', function () {
+ const invalidHopIntervals = [
+ { title: 'zero string', value: '0' },
+ { title: 'zero number', value: 0 },
+ { title: 'negative string', value: '-5' },
+ { title: 'negative number', value: -5 },
+ { title: 'decimal string', value: '15.5' },
+ { title: 'decimal number', value: 15.5 },
+ { title: 'comma list', value: '15,30' },
+ { title: 'plain text', value: 'abc' },
+ { title: 'empty string', value: '' },
+ { title: 'blank string', value: ' ' },
+ { title: 'zero-start range', value: '0-15' },
+ { title: 'zero-end range', value: '15-0' },
+ { title: 'reverse range', value: '30-15' },
+ { title: 'multi-range', value: '15-30-45' },
+ { title: 'double hyphen range', value: '15--30' },
+ { title: 'boolean', value: true },
+ { title: 'array', value: [15, 30] },
+ { title: 'object', value: { min: 15, max: 30 } },
+ ];
+
+ for (const { title, value } of invalidHopIntervals) {
+ const proxy = parseOne(
+ JSON.stringify({
+ name: `hy2-invalid-${title}`,
+ type: 'hysteria2',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ 'hop-interval': value,
+ 'hop-interval-max': 999,
+ }),
+ );
+
+ expect(proxy).to.not.have.property('hop-interval');
+ expect(proxy).to.not.have.property('hop-interval-max');
+ }
+ });
+
+ it('accepts every Clash-supported proxy type as inline objects', function () {
+ const supportedTypes = [
+ 'tailscale',
+ 'trusttunnel',
+ 'naive',
+ 'anytls',
+ 'mieru',
+ 'masque',
+ 'sudoku',
+ 'juicity',
+ 'ss',
+ 'ssr',
+ 'vmess',
+ 'socks5',
+ 'http',
+ 'snell',
+ 'trojan',
+ 'tuic',
+ 'vless',
+ 'hysteria',
+ 'hysteria2',
+ 'wireguard',
+ 'ssh',
+ 'direct',
+ ];
+
+ for (const type of supportedTypes) {
+ const proxy = parseOne(
+ JSON.stringify({
+ name: `${type}-inline`,
+ type,
+ server: `${type}.example.com`,
+ port: 443,
+ }),
+ );
+
+ expectSubset(proxy, {
+ name: `${type}-inline`,
+ type,
+ });
+ }
+ });
+});
diff --git a/backend/src/test/proxy-parsers/uri.spec.js b/backend/src/test/proxy-parsers/uri.spec.js
new file mode 100644
index 0000000000..90a73c4ee0
--- /dev/null
+++ b/backend/src/test/proxy-parsers/uri.spec.js
@@ -0,0 +1,718 @@
+import { Base64 } from 'js-base64';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { UUID, parseAll, parseOne, expectSubset } from './helpers';
+
+describe('Proxy URI parser coverage', function () {
+ describe('generic URIs', function () {
+ it('parses HTTPS URIs with implicit default port', function () {
+ const proxy = parseOne(
+ 'https://alice:pa%24%24@https.example.com#HTTPS%20Default',
+ );
+
+ expectSubset(proxy, {
+ type: 'http',
+ name: 'HTTPS Default',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ username: 'alice',
+ password: 'pa$$',
+ });
+ });
+
+ it('keeps the full https fragment comment after the first hash', function () {
+ const proxy = parseOne(
+ 'https://alice:pa%24%24@https.example.com#HTTPS%20Outer#Remark',
+ );
+
+ expectSubset(proxy, {
+ type: 'http',
+ name: 'HTTPS Outer#Remark',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ username: 'alice',
+ password: 'pa$$',
+ });
+ });
+
+ it('parses legacy socks:// URIs with base64 auth', function () {
+ const proxy = parseOne(
+ `socks://${encodeURIComponent(
+ Base64.encode('bob:secret'),
+ )}@socks.example.com:1080#SOCKS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'socks5',
+ name: 'SOCKS',
+ server: 'socks.example.com',
+ port: 1080,
+ username: 'bob',
+ password: 'secret',
+ });
+ });
+
+ it('keeps the full socks fragment comment after the first hash', function () {
+ const proxy = parseOne(
+ `socks://${encodeURIComponent(
+ Base64.encode('bob:secret'),
+ )}@socks.example.com:1080#SOCKS#Remark`,
+ );
+
+ expectSubset(proxy, {
+ type: 'socks5',
+ name: 'SOCKS#Remark',
+ server: 'socks.example.com',
+ port: 1080,
+ username: 'bob',
+ password: 'secret',
+ });
+ });
+ });
+
+ describe('shadowsocks family', function () {
+ it('parses SIP002 shadowsocks URIs with obfs and transport flags', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const plugin = encodeURIComponent(
+ 'obfs-local;obfs=http;obfs-host=obfs.example.com',
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss.example.com:8388/?plugin=${plugin}&uot=1&tfo=1#SS%20Obfs`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Obfs',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'obfs.example.com',
+ },
+ 'udp-over-tcp': true,
+ tfo: true,
+ });
+ });
+
+ it('parses SIP002 Base64URL shadowsocks userinfo', function () {
+ const proxy = parseOne(
+ `ss://${Base64.encodeURI(
+ 'aes-128-gcm:aa>',
+ )}@ss.example.com:8388#SS%20Base64URL`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Base64URL',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'aa>',
+ });
+ });
+
+ it('parses SIP002 plain shadowsocks userinfo with percent-encoded credentials', function () {
+ const password = 'sec:ret@/plus+';
+ const proxy = parseOne(
+ `ss://aes-128-gcm:${encodeURIComponent(
+ password,
+ )}@ss.example.com:8388#SS%20Plain`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Plain',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password,
+ });
+ });
+
+ it('parses SIP002 AEAD-2022 plain userinfo without Base64URL decoding', function () {
+ const password = 'YctPZ6U7xPPcU+gp3u+0tx/tRizJN9K8y+uKlW2qjlI=';
+ const proxy = parseOne(
+ `ss://2022-blake3-aes-256-gcm:${encodeURIComponent(
+ password,
+ )}@192.168.100.1:8888#SS%202022`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS 2022',
+ server: '192.168.100.1',
+ port: 8888,
+ cipher: '2022-blake3-aes-256-gcm',
+ password,
+ });
+ });
+
+ it('parses legacy shadowsocks URIs with v2ray-plugin options', function () {
+ const legacy = Base64.encode(
+ 'chacha20-ietf-poly1305:legacy-pass@legacy.example.com:443',
+ );
+ const plugin = encodeURIComponent(
+ 'v2ray-plugin;tls;host=cdn.example.com;path=/socket',
+ );
+ const proxy = parseOne(
+ `ss://${legacy}?plugin=${plugin}#SS%20V2ray`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS V2ray',
+ server: 'legacy.example.com',
+ port: 443,
+ cipher: 'chacha20-ietf-poly1305',
+ password: 'legacy-pass',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ },
+ });
+ });
+
+ it('keeps the full shadowsocks fragment comment after the first hash', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss.example.com:8388#SS%20Outer#Remark`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Outer#Remark',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ });
+ });
+
+ it('parses shadowsocks v2ray-plugin URI flags for sni, skip-cert-verify, and mux', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const plugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;host=cdn.example.com;path=/socket;tls;sni=sni.example.com;skip-cert-verify=1;mux=1',
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss.example.com:443/?plugin=${plugin}#SS%20V2ray%20Flags`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS V2ray Flags',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ mux: 1,
+ },
+ });
+ });
+
+ it('parses v2ray-plugin paths containing escaped equals signs', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const plugin = encodeURIComponent(
+ 'v2ray-plugin;mode=websocket;host=cdn.example.com;path=/?enc\\=aes-128-gcm',
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss.example.com:8080?plugin=${plugin}#SS%20Escaped%20Path`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Escaped Path',
+ server: 'ss.example.com',
+ port: 8080,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/?enc=aes-128-gcm',
+ },
+ });
+ });
+
+ it('parses shadowsocks websocket path early data without keeping it in path', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss-ws.example.com:443?type=ws&path=${encodeURIComponent(
+ '/ws?a=1&ed=2048&b=2',
+ )}&host=cdn.example.com&security=tls#SS%20WS%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS WS Early',
+ server: 'ss-ws.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('v2ray-http-upgrade');
+ });
+
+ it('does not double-decode shadowsocks path query values before extracting early data', function () {
+ const userInfo = encodeURIComponent(
+ Base64.encode('aes-128-gcm:secret'),
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss-upgrade.example.com:443?type=httpupgrade&path=${encodeURIComponent(
+ '/upgrade?redirect=%26ed%3D2048',
+ )}&host=cdn.example.com&security=tls#SS%20Upgrade%20Escaped`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS Upgrade Escaped',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?redirect=%26ed%3D2048',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'v2ray-http-upgrade-fast-open',
+ );
+ expect(proxy['ws-opts']).to.not.have.property(
+ '_v2ray-http-upgrade-ed',
+ );
+ });
+
+ it('parses shadowsocks shadow-tls compatibility payloads', function () {
+ const userInfo = Base64.encode('aes-256-gcm:shadow-pass');
+ const payload = Base64.encode(
+ JSON.stringify({
+ host: 'mask.example.com',
+ password: 'tls-pass',
+ version: '3',
+ address: '1.1.1.1',
+ port: '9443',
+ }),
+ );
+ const proxy = parseOne(
+ `ss://${userInfo}@ss.example.com:8388?shadow-tls=${payload}#SS%20ShadowTLS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: 'SS ShadowTLS',
+ server: '1.1.1.1',
+ port: 9443,
+ cipher: 'aes-256-gcm',
+ password: 'shadow-pass',
+ plugin: 'shadow-tls',
+ 'plugin-opts': {
+ host: 'mask.example.com',
+ password: 'tls-pass',
+ version: 3,
+ },
+ });
+ });
+
+ it('parses Shadowrocket shadowsocks gost-plugin payloads', function () {
+ const proxy = parseOne(
+ 'ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206WVRFMVpXVTRaVEV5WmpjM1ltRXpaQT09OlkySmhaVFUzT0RZdFpqZzNNQzAwTkE9PUBvcGVuYWkuY29tOjEx?gost=eyJwYXRoIjoiXC93cyIsInBvcnQiOiIxMSIsImhvc3QiOiJhIiwicm91dGUiOiJ3cyIsImFkZHJlc3MiOiJhIn0#%F0%9F%87%AF%F0%9F%87%B5%20%E6%97%A5%E6%9C%AC-A77ACD92',
+ );
+
+ expectSubset(proxy, {
+ type: 'ss',
+ name: '🇯🇵 日本-A77ACD92',
+ server: 'a',
+ port: 11,
+ cipher: '2022-blake3-aes-128-gcm',
+ password: 'YTE1ZWU4ZTEyZjc3YmEzZA==:Y2JhZTU3ODYtZjg3MC00NA==',
+ plugin: 'gost-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'a',
+ path: '/ws',
+ },
+ });
+ });
+
+ it('parses SSR URIs with protocol and obfs parameters', function () {
+ const encoded = Base64.encode(
+ [
+ 'ssr.example.com:8899:origin:aes-256-cfb:http_simple:',
+ Base64.encode('ssr-secret'),
+ '/?remarks=',
+ Base64.encode('SSR Node'),
+ '&obfsparam=',
+ Base64.encode('cdn.example.com'),
+ '&protoparam=',
+ Base64.encode('user:pass'),
+ ].join(''),
+ );
+ const proxy = parseOne(`ssr://${encoded}`);
+
+ expectSubset(proxy, {
+ type: 'ssr',
+ name: 'SSR Node',
+ server: 'ssr.example.com',
+ port: 8899,
+ protocol: 'origin',
+ cipher: 'aes-256-cfb',
+ obfs: 'http_simple',
+ password: 'ssr-secret',
+ 'protocol-param': 'user:pass',
+ 'obfs-param': 'cdn.example.com',
+ });
+ });
+ });
+
+ describe('other modern URI formats', function () {
+ it('parses AnyTLS URIs and drops plain tcp transport metadata', function () {
+ const proxy = parseOne(
+ 'anytls://top-secret@anytls.example.com:443?type=tcp&alpn=h2,http/1.1&insecure=1&udp=1#AnyTLS',
+ );
+
+ expectSubset(proxy, {
+ type: 'anytls',
+ name: 'AnyTLS',
+ server: 'anytls.example.com',
+ port: 443,
+ password: 'top-secret',
+ tls: true,
+ sni: 'anytls.example.com',
+ alpn: ['h2', 'http/1.1'],
+ udp: true,
+ 'skip-cert-verify': true,
+ });
+ expect(proxy.network).to.equal(undefined);
+ });
+
+ it('parses Hysteria URIs with fallback SNI and throughput fields', function () {
+ const proxy = parseOne(
+ 'hy://hysteria.example.com:8443?auth=token&peer=sni.example.com&alpn=h3,h2&mport=2000,3000&obfsParam=mask&upmbps=10&downmbps=20&insecure=1#Hysteria',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria',
+ name: 'Hysteria',
+ server: 'hysteria.example.com',
+ port: 8443,
+ protocol: 'udp',
+ 'auth-str': 'token',
+ sni: 'sni.example.com',
+ alpn: ['h3', 'h2'],
+ ports: '2000,3000',
+ obfs: 'mask',
+ up: '10',
+ down: '20',
+ 'skip-cert-verify': true,
+ });
+ });
+
+ it('parses Hysteria2 URIs with port hopping ranges', function () {
+ const proxy = parseOne(
+ 'hy2://hy2-secret@hy2.example.com:8443-8445?peer=peer.example.com&obfs=salamander&obfs-password=mask&insecure=1&fastopen=1&pinSHA256=fingerprint&hop-interval=30&keepalive=15#Hy2%20Range',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria2',
+ name: 'Hy2 Range',
+ server: 'hy2.example.com',
+ ports: '8443-8445',
+ password: 'hy2-secret',
+ sni: 'peer.example.com',
+ obfs: 'salamander',
+ 'obfs-password': 'mask',
+ 'skip-cert-verify': true,
+ tfo: true,
+ 'tls-fingerprint': 'fingerprint',
+ 'hop-interval': 30,
+ keepalive: 15,
+ });
+ expect(proxy.port).to.be.within(8443, 8445);
+ });
+
+ it('splits Hysteria2 URI hop-interval ranges during normalization', function () {
+ const proxy = parseOne(
+ 'hy2://hy2-secret@hy2.example.com:443?hop-interval=15-30#Hy2%20Hop%20Range',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria2',
+ name: 'Hy2 Hop Range',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'hy2-secret',
+ 'hop-interval': 15,
+ 'hop-interval-max': 30,
+ });
+ });
+
+ it('drops invalid Hysteria2 URI hop-interval values during normalization', function () {
+ const proxy = parseOne(
+ 'hy2://hy2-secret@hy2.example.com:443?hop-interval=30-15#Hy2%20Invalid%20Hop',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria2',
+ name: 'Hy2 Invalid Hop',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'hy2-secret',
+ });
+ expect(proxy).to.not.have.property('hop-interval');
+ expect(proxy).to.not.have.property('hop-interval-max');
+ });
+
+ it('parses Hysteria2 URIs with mport overrides', function () {
+ const proxy = parseOne(
+ 'hy2://hy2-secret@hy2.example.com:443?mport=9000,9002-9004#Hy2%20Mport',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria2',
+ name: 'Hy2 Mport',
+ server: 'hy2.example.com',
+ port: 443,
+ ports: '9000,9002-9004',
+ password: 'hy2-secret',
+ });
+ });
+
+ it('parses Hysteria2 URI throughput fields', function () {
+ const proxy = parseOne(
+ 'hy2://hy2-secret@hy2.example.com:443?upmbps=50&downmbps=100#Hy2%20Throughput',
+ );
+
+ expectSubset(proxy, {
+ type: 'hysteria2',
+ name: 'Hy2 Throughput',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'hy2-secret',
+ up: '50',
+ down: '100',
+ });
+ });
+
+ it('rejects Hysteria2 salamander obfs without obfs-password', function () {
+ const proxies = parseAll(
+ 'hy2://hy2-secret@hy2.example.com:443?obfs=salamander#Hy2%20Missing%20Password',
+ );
+
+ expect(proxies).to.deep.equal([]);
+ });
+
+ it('parses TUIC URIs with colon-containing passwords and booleans', function () {
+ const proxy = parseOne(
+ `tuic://${UUID}:pass%3Aword@tuic.example.com?alpn=h3,hq-29&allow-insecure=1&fast-open=1&disable-sni=1&reduce-rtt=1&congestion-control=bbr#TUIC`,
+ );
+
+ expectSubset(proxy, {
+ type: 'tuic',
+ name: 'TUIC',
+ server: 'tuic.example.com',
+ port: 443,
+ uuid: UUID,
+ password: 'pass:word',
+ alpn: ['h3', 'hq-29'],
+ 'skip-cert-verify': true,
+ tfo: true,
+ 'disable-sni': true,
+ 'reduce-rtt': true,
+ 'congestion-controller': 'bbr',
+ });
+ });
+
+ it('parses WireGuard URIs with address lists and reserved bytes', function () {
+ const proxy = parseOne(
+ 'wg://private-key@wg.example.com?publickey=public-key&privatekey=override-key&address=10.0.0.2/32,[fd00::2]/128&reserved=1,2,3&mtu=1280&udp=0#WG',
+ );
+
+ expectSubset(proxy, {
+ type: 'wireguard',
+ name: 'WG',
+ server: 'wg.example.com',
+ port: 51820,
+ 'private-key': 'override-key',
+ 'public-key': 'public-key',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 32,
+ 'ipv6-cidr': 128,
+ reserved: [1, 2, 3],
+ mtu: 1280,
+ udp: false,
+ });
+ });
+
+ it('defaults WireGuard CIDR suffixes when address entries omit them', function () {
+ const proxy = parseOne(
+ 'wg://private-key@wg.example.com?publickey=public-key&address=10.0.0.2,[fd00::2]#WG%20Default%20CIDR',
+ );
+
+ expectSubset(proxy, {
+ type: 'wireguard',
+ name: 'WG Default CIDR',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 32,
+ 'ipv6-cidr': 128,
+ });
+ });
+
+ it('preserves trailing base64 padding in WireGuard key query params', function () {
+ const proxy = parseOne(
+ 'wg://120.233.41.77:19368?publicKey=N+K9fXobvy0vy3VFbn8a7tPRgUNcQbGRwjlyOMx4WHc=&privateKey=QJjrFqqqpbIqfI5qhbYWrPXhaBFmFq71jCj8mMaQE04=&ip=10.0.20.45/16,fd10:10:10:0:10:0:20:45/64&mtu=1420&udp=1#HK-%E5%A4%A7%E5%B8%A6%E5%AE%BD4',
+ );
+
+ expectSubset(proxy, {
+ type: 'wireguard',
+ name: 'HK-大带宽4',
+ server: '120.233.41.77',
+ port: 19368,
+ 'public-key': 'N+K9fXobvy0vy3VFbn8a7tPRgUNcQbGRwjlyOMx4WHc=',
+ 'private-key': 'QJjrFqqqpbIqfI5qhbYWrPXhaBFmFq71jCj8mMaQE04=',
+ ip: '10.0.20.45',
+ 'ip-cidr': 16,
+ ipv6: 'fd10:10:10:0:10:0:20:45',
+ 'ipv6-cidr': 64,
+ mtu: 1420,
+ udp: true,
+ });
+ });
+
+ it('parses Trojan URIs with websocket transport', function () {
+ const proxy = parseOne(
+ 'trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=%2Fws%3Fa%3D1%26ed%3D1024%26b%3D2#Trojan%20WS',
+ );
+
+ expectSubset(proxy, {
+ type: 'trojan',
+ name: 'Trojan WS',
+ server: 'trojan-ws.example.com',
+ port: 443,
+ password: 'trojan-pass',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'ws.example.com',
+ },
+ 'max-early-data': 1024,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ });
+
+ it('does not double-decode Trojan path query values before extracting early data', function () {
+ const proxy = parseOne(
+ `trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=${encodeURIComponent(
+ '/ws?redirect=%26ed%3D2048',
+ )}#Trojan%20WS%20Escaped`,
+ );
+
+ expectSubset(proxy, {
+ type: 'trojan',
+ name: 'Trojan WS Escaped',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?redirect=%26ed%3D2048',
+ headers: {
+ Host: 'ws.example.com',
+ },
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('max-early-data');
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('parses Trojan URIs with pcs as tls fingerprint', function () {
+ const proxy = parseOne(
+ 'trojan://trojan-pass@trojan-ws.example.com?type=ws&host=ws.example.com&path=%2Fws&pcs=fingerprint#Trojan%20WS%20PCS',
+ );
+
+ expectSubset(proxy, {
+ type: 'trojan',
+ name: 'Trojan WS PCS',
+ server: 'trojan-ws.example.com',
+ port: 443,
+ password: 'trojan-pass',
+ 'tls-fingerprint': 'fingerprint',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'ws.example.com',
+ },
+ },
+ });
+ });
+
+ it('parses Trojan URIs with grpc reality metadata', function () {
+ const proxy = parseOne(
+ 'trojan://trojan-pass@trojan-grpc.example.com?type=grpc&serviceName=grpc-service&authority=grpc.example.com&mode=multi&security=reality&pbk=pubkey&sid=08&spx=%2Fspider&udp=1&tfo=1#Trojan%20Reality',
+ );
+
+ expectSubset(proxy, {
+ type: 'trojan',
+ name: 'Trojan Reality',
+ server: 'trojan-grpc.example.com',
+ port: 443,
+ password: 'trojan-pass',
+ network: 'grpc',
+ udp: true,
+ tfo: true,
+ 'grpc-opts': {
+ 'grpc-service-name': 'grpc-service',
+ '_grpc-type': 'multi',
+ '_grpc-authority': 'grpc.example.com',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ '_spider-x': '/spider',
+ },
+ _mode: 'multi',
+ });
+ });
+ });
+});
diff --git a/backend/src/test/proxy-parsers/v2ray-and-platforms.spec.js b/backend/src/test/proxy-parsers/v2ray-and-platforms.spec.js
new file mode 100644
index 0000000000..e01d4f4e32
--- /dev/null
+++ b/backend/src/test/proxy-parsers/v2ray-and-platforms.spec.js
@@ -0,0 +1,3239 @@
+import { Base64 } from 'js-base64';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { UUID, expectSubset, parseAll, parseOne } from './helpers';
+
+describe('VMess and VLESS parser coverage', function () {
+ describe('VMess URIs', function () {
+ it('parses Quantumult VMess shares', function () {
+ const share = Base64.encode(
+ `QX VMess = vmess,vmess-qx.example.com,443,auto,"${UUID}",udp-relay=true,fast-open=true,tls-verification=false`,
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'QX VMess',
+ server: 'vmess-qx.example.com',
+ port: 443,
+ cipher: 'auto',
+ uuid: UUID,
+ udp: 'true',
+ tfo: 'true',
+ 'skip-cert-verify': false,
+ });
+ });
+
+ it('parses V2rayN ws shares', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ v: '2',
+ ps: 'VMess WS',
+ add: 'vmess-ws.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'ws',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: 'tls',
+ sni: 'sni.example.com',
+ allowInsecure: '1',
+ fp: 'chrome',
+ alpn: 'h2',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess WS',
+ server: 'vmess-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ alterId: 0,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2'],
+ network: 'ws',
+ 'ws-opts': {
+ path: '/socket',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+ });
+
+ it('prefers fragment comments over internal V2rayN vmess names', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ v: '2',
+ ps: 'VMess WS Fragment',
+ add: 'vmess-fragment.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'none',
+ net: 'ws',
+ host: 'fragment.example.com',
+ path: '/fragment?ed=2560',
+ tls: 'tls',
+ sni: 'sni.fragment.example.com',
+ fp: 'chrome',
+ }),
+ );
+ const proxy = parseOne(
+ `vmess://${share}#Outer%20Fragment%20Remark`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'Outer Fragment Remark',
+ server: 'vmess-fragment.example.com',
+ port: 443,
+ uuid: UUID,
+ alterId: 0,
+ cipher: 'none',
+ tls: true,
+ sni: 'sni.fragment.example.com',
+ 'client-fingerprint': 'chrome',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/fragment',
+ headers: {
+ Host: 'fragment.example.com',
+ },
+ 'max-early-data': 2560,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ });
+
+ it('keeps the full vmess fragment comment after the first hash', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ v: '2',
+ ps: 'VMess WS Fragment Full',
+ add: 'vmess-fragment-full.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'ws',
+ host: 'fragment-full.example.com',
+ path: '/fragment-full',
+ tls: 'tls',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}#Outer%20Fragment#Remark`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'Outer Fragment#Remark',
+ server: 'vmess-fragment-full.example.com',
+ port: 443,
+ uuid: UUID,
+ alterId: 0,
+ cipher: 'auto',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/fragment-full',
+ headers: {
+ Host: 'fragment-full.example.com',
+ },
+ },
+ });
+ });
+
+ it('parses V2rayN http shares and normalizes http paths', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess HTTP',
+ add: 'vmess-http.example.com',
+ port: '80',
+ id: UUID,
+ aid: '1',
+ scy: 'unknown-cipher',
+ net: 'http',
+ host: 'h1.example.com,h2.example.com',
+ path: '/a,/b',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess HTTP',
+ server: 'vmess-http.example.com',
+ port: 80,
+ uuid: UUID,
+ alterId: 1,
+ cipher: 'auto',
+ network: 'http',
+ 'http-opts': {
+ headers: {
+ Host: ['h1.example.com'],
+ },
+ path: ['/a,/b'],
+ },
+ });
+ });
+
+ it('parses V2rayN grpc shares', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess gRPC',
+ add: 'vmess-grpc.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'grpc',
+ path: 'grpc-service',
+ type: 'multi',
+ authority: 'grpc.example.com',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess gRPC',
+ server: 'vmess-grpc.example.com',
+ port: 443,
+ network: 'grpc',
+ 'grpc-opts': {
+ 'grpc-service-name': 'grpc-service',
+ '_grpc-type': 'multi',
+ '_grpc-authority': 'grpc.example.com',
+ },
+ });
+ });
+
+ it('parses V2rayN quic shares', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess QUIC',
+ add: 'vmess-quic.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'quic',
+ host: 'quic-host.example.com',
+ path: '/quic',
+ type: 'wireguard',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess QUIC',
+ server: 'vmess-quic.example.com',
+ port: 443,
+ network: 'quic',
+ 'quic-opts': {
+ '_quic-type': 'wireguard',
+ '_quic-host': 'quic-host.example.com',
+ '_quic-path': '/quic',
+ },
+ });
+ });
+
+ it('parses V2rayN httpupgrade shares as websocket upgrades', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess Upgrade',
+ add: 'vmess-upgrade.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'httpupgrade',
+ host: 'upgrade.example.com',
+ path: '/upgrade',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess Upgrade',
+ server: 'vmess-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'v2ray-http-upgrade-fast-open',
+ );
+ });
+
+ it('parses V2rayN httpupgrade path early data from multiple query params', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess Upgrade Early',
+ add: 'vmess-upgrade.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'httpupgrade',
+ host: 'upgrade.example.com',
+ path: '/upgrade?a=1&ed=1024&b=2',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess Upgrade Early',
+ server: 'vmess-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('max-early-data');
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('parses V2rayN httpupgrade top-level early data', function () {
+ const share = Base64.encode(
+ JSON.stringify({
+ ps: 'VMess Upgrade Top Early',
+ add: 'vmess-upgrade.example.com',
+ port: '443',
+ id: UUID,
+ aid: '0',
+ scy: 'auto',
+ net: 'httpupgrade',
+ host: 'upgrade.example.com',
+ path: '/upgrade',
+ ed: '1024',
+ }),
+ );
+ const proxy = parseOne(`vmess://${share}`);
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'VMess Upgrade Top Early',
+ server: 'vmess-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('max-early-data');
+ });
+
+ it('parses Shadowrocket VMess shares', function () {
+ const base = Base64.encode(
+ `auto:${UUID}@shadowrocket-vmess.example.com:443`,
+ );
+ const proxy = parseOne(
+ `vmess://${base}?remarks=Shadowrocket%20VMess&obfs=websocket&path=%2Fshadow&obfsParam=ws.shadow.example.com&tls=1&peer=sni.shadow.example.com&allowInsecure=1&fp=safari&alpn=h2`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vmess',
+ name: 'Shadowrocket VMess',
+ server: 'shadowrocket-vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.shadow.example.com',
+ 'skip-cert-verify': true,
+ 'client-fingerprint': 'safari',
+ alpn: ['h2'],
+ network: 'ws',
+ 'ws-opts': {
+ path: '/shadow',
+ headers: {
+ Host: 'ws.shadow.example.com',
+ },
+ },
+ });
+ });
+ });
+
+ describe('VLESS URIs', function () {
+ it('parses websocket VLESS shares', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&sni=sni.example.com&host=cdn.example.com&path=%2Fws&allowInsecure=1&fp=chrome&alpn=h2#VLESS%20WS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS',
+ server: 'vless-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2'],
+ udp: true,
+ xudp: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+ });
+
+ it('keeps the full vless fragment comment after the first hash', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&sni=sni.example.com&host=cdn.example.com&path=%2Fws&allowInsecure=1&fp=chrome&alpn=h2#VLESS%20Outer#Remark`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Outer#Remark',
+ server: 'vless-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2'],
+ udp: true,
+ xudp: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+ });
+
+ it('parses websocket VLESS shares with packet encoding and early data', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&packetEncoding=packet&ed=2048&eh=X-Data#VLESS%20WS%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS Early',
+ server: 'vless-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ udp: true,
+ 'packet-addr': true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Data',
+ },
+ });
+ expect(proxy).to.not.have.property('xudp');
+ });
+
+ it('parses websocket VLESS shares with pcs as tls fingerprint', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&pcs=fingerprint#VLESS%20WS%20PCS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS PCS',
+ server: 'vless-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ udp: true,
+ xudp: true,
+ 'tls-fingerprint': 'fingerprint',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+ });
+
+ it('parses VLESS share ECH config into mihomo ech opts', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&ech=${encodeURIComponent(
+ 'ECHCONFIG',
+ )}#VLESS%20WS%20ECH`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS ECH',
+ _echConfigList: 'ECHCONFIG',
+ 'ech-opts': {
+ enable: true,
+ config: 'ECHCONFIG',
+ },
+ });
+ });
+
+ it('parses VLESS share ECH DNS into mihomo sidecar fields', function () {
+ const echConfigList = 'ech.example.com+https://1.1.1.1/dns-query';
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&ech=${encodeURIComponent(
+ echConfigList,
+ )}#VLESS%20WS%20ECH%20DNS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS ECH DNS',
+ _echConfigList: echConfigList,
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'ech.example.com',
+ },
+ });
+ });
+
+ it('rejects websocket VLESS shares with malformed early data size', function () {
+ expect(
+ parseAll(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&ed=2048foo#VLESS%20WS%20Broken`,
+ ),
+ ).to.have.length(0);
+ });
+
+ it('rejects websocket VLESS shares with oversized early data size', function () {
+ expect(
+ parseAll(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&ed=999999999999999999999#VLESS%20WS%20Too%20Large`,
+ ),
+ ).to.have.length(0);
+ });
+
+ it('parses grpc reality VLESS shares', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-grpc.example.com:443?type=grpc&security=reality&serviceName=grpc-service&authority=grpc.example.com&mode=multi&pbk=pubkey&sid=08&spx=%2Fspider&flow=xtls-rprx-vision&encryption=none&pqv=1&alpn=h2#VLESS%20Reality`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Reality',
+ server: 'vless-grpc.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ encryption: 'none',
+ _pqv: '1',
+ alpn: ['h2'],
+ network: 'grpc',
+ 'grpc-opts': {
+ 'grpc-service-name': 'grpc-service',
+ '_grpc-authority': 'grpc.example.com',
+ '_grpc-type': 'multi',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ '_spider-x': '/spider',
+ },
+ _mode: 'multi',
+ });
+ });
+
+ it('keeps reality opts when share links carry pbk without security=reality', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-grpc.example.com:443?type=grpc&security=tls&serviceName=grpc-service&pbk=pubkey&sid=08#VLESS%20PBK`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS PBK',
+ server: 'vless-grpc.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'grpc',
+ 'grpc-opts': {
+ 'grpc-service-name': 'grpc-service',
+ '_grpc-type': 'gun',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ });
+
+ it('keeps pbk-derived reality opts without tls defaults when security is none', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-grpc.example.com:443?type=grpc&security=none&serviceName=grpc-service&pbk=pubkey&sid=08#VLESS%20PBK%20No%20TLS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS PBK No TLS',
+ server: 'vless-grpc.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: false,
+ network: 'grpc',
+ 'grpc-opts': {
+ 'grpc-service-name': 'grpc-service',
+ '_grpc-type': 'gun',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ });
+
+ it('parses tcp http-header VLESS shares', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-http.example.com:80?type=tcp&headerType=http&host=http.example.com&path=%2Fedge&method=GET#VLESS%20HTTP`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS HTTP',
+ server: 'vless-http.example.com',
+ port: 80,
+ network: 'http',
+ 'http-opts': {
+ headers: {
+ Host: ['http.example.com'],
+ },
+ method: 'GET',
+ path: ['/edge'],
+ },
+ });
+ });
+
+ it('parses httpupgrade VLESS shares as websocket upgrades', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-upgrade.example.com:443?type=httpupgrade&host=upgrade.example.com&path=%2Fupgrade#VLESS%20Upgrade`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Upgrade',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'v2ray-http-upgrade-fast-open',
+ );
+ });
+
+ it('parses httpupgrade VLESS shares with early data metadata', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-upgrade.example.com:443?type=httpupgrade&host=upgrade.example.com&path=%2Fupgrade&ed=1024&eh=X-Upgrade#VLESS%20Upgrade%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Upgrade Early',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ 'early-data-header-name': 'X-Upgrade',
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('max-early-data');
+ });
+
+ it('parses httpupgrade VLESS path early data from multiple query params', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-upgrade.example.com:443?type=httpupgrade&host=upgrade.example.com&path=%2Fupgrade%3Fa%3D1%26ed%3D1024%26b%3D2#VLESS%20Upgrade%20Path%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Upgrade Path Early',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+ expect(proxy['ws-opts']).to.not.have.property('max-early-data');
+ expect(proxy['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('prefers VLESS path early data over duplicate top-level early data', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-upgrade.example.com:443?type=httpupgrade&host=upgrade.example.com&path=%2Fupgrade%3Fed%3D1024&ed=2048#VLESS%20Upgrade%20Duplicate%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS Upgrade Duplicate Early',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+ });
+
+ it('parses websocket VLESS path early data from multiple query params', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws%3Fa%3D1%26ed%3D2048%26b%3D2#VLESS%20WS%20Path%20Early`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS WS Path Early',
+ server: 'vless-ws.example.com',
+ port: 443,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ });
+
+ it('parses kcp VLESS shares', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-kcp.example.com:443?type=kcp&headerType=srtp&host=kcp.example.com&path=%2Fkcp&seed=seed-value&mode=packet&extra=extra-value#VLESS%20KCP`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS KCP',
+ server: 'vless-kcp.example.com',
+ port: 443,
+ network: 'kcp',
+ seed: 'seed-value',
+ headerType: 'srtp',
+ _mode: 'packet',
+ _extra: 'extra-value',
+ 'kcp-opts': {
+ headers: {
+ Host: 'kcp.example.com',
+ },
+ path: '/kcp',
+ },
+ });
+ });
+
+ it('parses h2 VLESS shares from share-link http transport', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-h2.example.com:443?type=http&host=h2.example.com&path=%2Fh2&h2=1&packetEncoding=none#VLESS%20H2`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS H2',
+ server: 'vless-h2.example.com',
+ port: 443,
+ udp: true,
+ network: 'h2',
+ _h2: true,
+ 'h2-opts': {
+ headers: {
+ host: ['h2.example.com'],
+ },
+ path: '/h2',
+ },
+ });
+ expect(proxy).to.not.have.property('xudp');
+ });
+
+ it('parses xhttp VLESS shares with mihomo transport extras', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: 300,
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ expect(proxy).to.not.have.property('_extra');
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('moves xhttp Host from obfsParam to xhttp-opts.host', function () {
+ const obfsParam = JSON.stringify({
+ Host: 'header.example.com',
+ 'X-Test': 'demo',
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&obfsParam=${encodeURIComponent(
+ obfsParam,
+ )}&path=%2Fxhttp#VLESS%20XHTTP%20Host%20Header`,
+ );
+
+ expectSubset(proxy, {
+ network: 'xhttp',
+ 'xhttp-opts': {
+ host: 'header.example.com',
+ path: '/xhttp',
+ headers: {
+ 'X-Test': 'demo',
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.nested.property(
+ 'headers.Host',
+ );
+ });
+
+ it('parses xhttp VLESS shares with downloadSettings extra', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ alpn: ['h2', 'http/1.1'],
+ },
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ noGRPCHeader: true,
+ xPaddingBytes: '32-64',
+ scMaxEachPostBytes: '500000-1000000',
+ scMinPostsIntervalMs: '0-300',
+ extra: {
+ xmux: {
+ maxConnections: '8',
+ hMaxReusableSecs: '900',
+ },
+ },
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Download',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2', 'http/1.1'],
+ path: '/download',
+ host: 'download-host.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '32-64',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': '0-300',
+ 'reuse-settings': {
+ 'max-connections': '8',
+ 'h-max-reusable-secs': '900',
+ },
+ },
+ },
+ });
+ expect(
+ proxy['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('network');
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('parses xhttp VLESS shares with downloadSettings extra without mode', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download%20No%20Mode`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Download No Mode',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.property('mode');
+ });
+
+ it('parses xhttp network into structured downloadSettings while keeping unsupported nested fields in the sidecar', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ network: 'xhttp',
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download%20Unsupported%20Only`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Download Unsupported Only',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ network: 'xhttp',
+ },
+ },
+ });
+ expect(proxy._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ });
+
+ it('normalizes splithttp downloadSettings network without keeping the raw network in the sidecar', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ network: 'splithttp',
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download%20SplitHTTP`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Download SplitHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ network: 'xhttp',
+ },
+ },
+ });
+ expect(proxy._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ });
+
+ it('keeps malformed reality downloadSettings as an empty reality marker', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'reality',
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download%20Reality%20Marker`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Download Reality Marker',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ network: 'xhttp',
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ });
+ expect(
+ proxy['xhttp-opts']?.['download-settings']?.['reality-opts'],
+ ).to.deep.equal({});
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('keeps invalid xhttp extra as raw _extra for URI round-trips', function () {
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ '{bad',
+ )}#VLESS%20XHTTP%20Invalid%20Extra`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Invalid Extra',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ _extra: '{bad',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ },
+ });
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('parses xhttp VLESS shares with range-form scMinPostsIntervalMs', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMinPostsIntervalMs: '100 - 300',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Min%20Interval%20Range`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Min Interval Range',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-min-posts-interval-ms': '100-300',
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with string-form scMinPostsIntervalMs', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMinPostsIntervalMs: '300',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Min%20Interval%20String`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Min Interval String',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with zero-lower-bound scMinPostsIntervalMs range', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMinPostsIntervalMs: '0-300',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Min%20Interval%20Zero%20Lower%20Bound`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Min Interval Zero Lower Bound',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-min-posts-interval-ms': '0-300',
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with Mihomo-style leading-zero sc scalars', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: '000-1000000',
+ scMinPostsIntervalMs: '0300',
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ scMaxEachPostBytes: '000-1000000',
+ scMinPostsIntervalMs: '000-300',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Leading%20Zero%20Scalars`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Leading Zero Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': '0-300',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with Mihomo-style explicit-plus sc scalars', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: '+500000-+1000000',
+ scMinPostsIntervalMs: '+300',
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ scMaxEachPostBytes: '+0-+1000000',
+ scMinPostsIntervalMs: '+0-+300',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Explicit%20Plus%20Scalars`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Explicit Plus Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': '0-300',
+ },
+ },
+ });
+ });
+
+ it('ignores invalid xhttp VLESS scMinPostsIntervalMs values', function () {
+ const invalidValues = [
+ '1.5',
+ 1.5,
+ '0',
+ 0,
+ '0-0',
+ 'fast',
+ '10-1',
+ '9007199254740993',
+ '1-9007199254740993',
+ ];
+
+ for (const value of invalidValues) {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMinPostsIntervalMs: value,
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Min%20Interval%20Invalid`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.property(
+ 'sc-min-posts-interval-ms',
+ );
+ expect(proxy._extra_unsupported).to.deep.equal({
+ scMinPostsIntervalMs: value,
+ });
+ }
+ });
+
+ it('ignores invalid nested xhttp VLESS scMinPostsIntervalMs values in downloadSettings extra', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ },
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ noGRPCHeader: true,
+ xPaddingBytes: '32-64',
+ scMinPostsIntervalMs: '0-0',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Download%20Invalid%20Min%20Interval`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ path: '/download',
+ host: 'download-host.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '32-64',
+ },
+ },
+ });
+ expect(
+ proxy['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sc-min-posts-interval-ms');
+ expect(proxy._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ xhttpSettings: {
+ scMinPostsIntervalMs: '0-0',
+ },
+ },
+ });
+ });
+
+ it('parses extended xhttp VLESS extra fields and keeps unsupported fields in _extra_unsupported', function () {
+ const extra = JSON.stringify({
+ headers: {
+ 'X-Test': 'demo',
+ },
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ xPaddingObfsMode: true,
+ xPaddingKey: 'x_padding',
+ xPaddingHeader: 'Referer',
+ xPaddingPlacement: 'header',
+ xPaddingMethod: 'tokenish',
+ uplinkHTTPMethod: 'PUT',
+ sessionPlacement: 'query',
+ sessionKey: 'x_session_id',
+ seqPlacement: 'header',
+ seqKey: 'X-Seq',
+ uplinkDataPlacement: 'header',
+ uplinkDataKey: 'X-Data',
+ uplinkChunkSize: '64-128',
+ xmux: {
+ maxConcurrency: '16-32',
+ hKeepAlivePeriod: 15,
+ },
+ noSSEHeader: true,
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ allowInsecure: true,
+ alpn: ['h2'],
+ echConfigList: 'ECHCONFIG',
+ },
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ headers: {
+ 'X-Download': '1',
+ },
+ noGRPCHeader: true,
+ xPaddingBytes: '16-32',
+ xPaddingObfsMode: true,
+ xPaddingKey: 'x_padding_dl',
+ xPaddingHeader: 'Cookie',
+ xPaddingPlacement: 'query',
+ xPaddingMethod: 'repeat-x',
+ uplinkHTTPMethod: 'PATCH',
+ sessionPlacement: 'header',
+ sessionKey: 'X-Session',
+ seqPlacement: 'query',
+ seqKey: 'x_seq',
+ uplinkDataPlacement: 'cookie',
+ uplinkDataKey: 'x_data',
+ uplinkChunkSize: 48,
+ extra: {
+ xmux: {
+ maxConcurrency: '8-16',
+ hKeepAlivePeriod: -1,
+ },
+ },
+ },
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Extended%20Extra`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Extended Extra',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ headers: {
+ 'X-Test': 'demo',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'x-padding-obfs-mode': true,
+ 'x-padding-key': 'x_padding',
+ 'x-padding-header': 'Referer',
+ 'x-padding-placement': 'header',
+ 'x-padding-method': 'tokenish',
+ 'uplink-http-method': 'PUT',
+ 'session-placement': 'query',
+ 'session-key': 'x_session_id',
+ 'seq-placement': 'header',
+ 'seq-key': 'X-Seq',
+ 'uplink-data-placement': 'header',
+ 'uplink-data-key': 'X-Data',
+ 'uplink-chunk-size': '64-128',
+ 'reuse-settings': {
+ 'max-concurrency': '16-32',
+ 'h-keep-alive-period': 15,
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'client-fingerprint': 'chrome',
+ 'skip-cert-verify': true,
+ alpn: ['h2'],
+ 'ech-opts': {
+ enable: true,
+ config: 'ECHCONFIG',
+ },
+ path: '/download',
+ host: 'download-host.example.com',
+ headers: {
+ 'X-Download': '1',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '16-32',
+ 'x-padding-obfs-mode': true,
+ 'x-padding-key': 'x_padding_dl',
+ 'x-padding-header': 'Cookie',
+ 'x-padding-placement': 'query',
+ 'x-padding-method': 'repeat-x',
+ 'uplink-http-method': 'PATCH',
+ 'session-placement': 'header',
+ 'session-key': 'X-Session',
+ 'seq-placement': 'query',
+ 'seq-key': 'x_seq',
+ 'uplink-data-placement': 'cookie',
+ 'uplink-data-key': 'x_data',
+ 'uplink-chunk-size': 48,
+ 'reuse-settings': {
+ 'max-concurrency': '8-16',
+ 'h-keep-alive-period': -1,
+ },
+ },
+ },
+ });
+
+ expect(proxy).to.not.have.property('_extra');
+ expect(proxy._extra_unsupported).to.deep.equal({
+ noSSEHeader: true,
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.property('no-sse-header');
+ expect(
+ proxy['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sockopt');
+ });
+
+ it('parses nested xhttp download TLS ECH DNS fields into mihomo sidecar fields', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ echConfigList:
+ 'download-ech.example.com+https://1.1.1.1/dns-query',
+ echForceQuery: 'half',
+ echSockopt: {
+ mark: 255,
+ },
+ },
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Nested%20ECH%20DNS`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'download-ech.example.com',
+ '_force-query': 'half',
+ _sockopt: {
+ mark: 255,
+ },
+ },
+ path: '/download',
+ },
+ },
+ });
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('parses xhttp VLESS xmux ranges canonically and keeps unsupported keep-alive values in _extra_unsupported', function () {
+ const extra = JSON.stringify({
+ xmux: {
+ maxConnections: '+0008',
+ maxConcurrency: '0008-0016',
+ hKeepAlivePeriod: '9007199254740993',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20XMUX%20Canonical`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'reuse-settings': {
+ 'max-connections': '8',
+ 'max-concurrency': '8-16',
+ },
+ },
+ });
+ expect(
+ proxy['xhttp-opts']?.['reuse-settings'],
+ ).to.not.have.property('h-keep-alive-period');
+ expect(proxy._extra_unsupported).to.deep.equal({
+ xmux: {
+ hKeepAlivePeriod: '9007199254740993',
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with string downloadSettings ports as structured values', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: '8443',
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20String%20Download%20Port`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ });
+ expect(proxy).to.not.have.property('_extra_unsupported');
+ });
+
+ it('keeps malformed xhttp VLESS uplinkChunkSize values in _extra_unsupported', function () {
+ const extra = JSON.stringify({
+ uplinkChunkSize: 'fast',
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ uplinkChunkSize: 'faster',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Malformed%20Uplink%20Chunk`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.property(
+ 'uplink-chunk-size',
+ );
+ expect(
+ proxy['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('uplink-chunk-size');
+ expect(proxy._extra_unsupported).to.deep.equal({
+ uplinkChunkSize: 'fast',
+ downloadSettings: {
+ xhttpSettings: {
+ uplinkChunkSize: 'faster',
+ },
+ },
+ });
+ });
+
+ it('keeps mixed downloadSettings tlsSettings.alpn arrays in _extra_unsupported', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ alpn: ['h2', { foo: 1 }],
+ },
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Mixed%20ALPN`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ });
+ expect(
+ proxy['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('alpn');
+ expect(proxy._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ tlsSettings: {
+ alpn: ['h2', { foo: 1 }],
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with range-form scMaxEachPostBytes', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: '500000 - 1000000',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Range`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Range',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with string-form scMaxEachPostBytes', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: '1000000',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20String`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP String',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('parses xhttp VLESS shares with zero-lower-bound scMaxEachPostBytes range', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: '0-1000000',
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Zero%20Lower%20Bound`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'VLESS XHTTP Zero Lower Bound',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ });
+
+ it('ignores invalid xhttp VLESS scMaxEachPostBytes values', function () {
+ const invalidValues = [
+ '1.5',
+ 1.5,
+ '0',
+ 0,
+ 'fast',
+ '10-1',
+ '9007199254740993',
+ '1-9007199254740993',
+ ];
+
+ for (const value of invalidValues) {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: value,
+ xmux: {
+ maxConnections: 0,
+ maxConcurrency: '16-32',
+ cMaxReuseTimes: '64-128',
+ hMaxRequestTimes: '600-900',
+ hMaxReusableSecs: '1800-3000',
+ },
+ });
+ const proxy = parseOne(
+ `vless://${UUID}@vless-xhttp.example.com:443?type=xhttp&security=tls&host=cdn.example.com&path=%2Fxhttp&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#VLESS%20XHTTP%20Invalid`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ mode: 'stream-up',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'reuse-settings': {
+ 'max-connections': '0',
+ 'max-concurrency': '16-32',
+ 'c-max-reuse-times': '64-128',
+ 'h-max-request-times': '600-900',
+ 'h-max-reusable-secs': '1800-3000',
+ },
+ },
+ });
+ expect(proxy['xhttp-opts']).to.not.have.property(
+ 'sc-max-each-post-bytes',
+ );
+ expect(proxy._extra_unsupported).to.deep.equal({
+ scMaxEachPostBytes: value,
+ });
+ }
+ });
+
+ it('parses Shadowrocket VLESS shares', function () {
+ const base = Base64.encode(
+ `none:${UUID}@shadowrocket-vless.example.com:443`,
+ );
+ const proxy = parseOne(
+ `vless://${base}?remarks=Shadowrocket%20VLESS&tls=1&obfs=websocket&obfsParam=ws.shadow.example.com&path=%2Fshadow&xtls=2`,
+ );
+
+ expectSubset(proxy, {
+ type: 'vless',
+ name: 'Shadowrocket VLESS',
+ server: 'shadowrocket-vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/shadow',
+ headers: {
+ Host: 'ws.shadow.example.com',
+ },
+ },
+ });
+ });
+ });
+});
+
+describe('Platform raw-format parser coverage', function () {
+ function registerCases(cases) {
+ for (const { title, input, expected } of cases) {
+ it(title, function () {
+ const proxy = parseOne(input);
+ expectSubset(proxy, expected);
+ });
+ }
+ }
+
+ describe('Quantumult X raw inputs', function () {
+ registerCases([
+ {
+ title: 'parses shadowsocks over-tls lines into canonical tls fields',
+ input: 'shadowsocks=qx-ss-tls.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,obfs-host=a.com,tls-verification=false,udp-relay=true,fast-open=true,tag=QX SS Over TLS',
+ expected: {
+ type: 'ss',
+ name: 'QX SS Over TLS',
+ server: 'qx-ss-tls.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ 'skip-cert-verify': true,
+ udp: true,
+ tfo: true,
+ },
+ },
+ {
+ title: 'parses shadowsocks over-tls lines with tls-verification=true',
+ input: 'shadowsocks=qx-ss-tls-verified.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,obfs-host=verify.example.com,tls-verification=true,tag=QX SS Over TLS Verified',
+ expected: {
+ type: 'ss',
+ name: 'QX SS Over TLS Verified',
+ server: 'qx-ss-tls-verified.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'verify.example.com',
+ 'skip-cert-verify': false,
+ },
+ },
+ {
+ title: 'parses shadowsocks over-tls lines without obfs-host',
+ input: 'shadowsocks=qx-ss-tls-no-host.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,udp-relay=true,tag=QX SS Over TLS No Host',
+ expected: {
+ type: 'ss',
+ name: 'QX SS Over TLS No Host',
+ server: 'qx-ss-tls-no-host.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ udp: true,
+ },
+ },
+ {
+ title: 'keeps legacy shadowsocks obfs tls lines as plugin nodes',
+ input: 'shadowsocks=qx-ss-obfs.example.com:8388,method=aes-128-gcm,password=secret,obfs=tls,obfs-host=obfs.example.com,tag=QX SS Obfs TLS',
+ expected: {
+ type: 'ss',
+ name: 'QX SS Obfs TLS',
+ server: 'qx-ss-obfs.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'tls',
+ host: 'obfs.example.com',
+ },
+ },
+ },
+ {
+ // QX accepts plain "http" as one of the shared http-obfs
+ // tokens for ss/vmess/vless; preserve it for round-trip output.
+ title: 'parses shadowsocks http lines as http obfs and keeps the QX token',
+ input: 'shadowsocks=qx-ss-http-plain.example.com:8388,method=aes-128-gcm,password=secret,obfs=http,obfs-host=plain.example.com,obfs-uri=/plain,tag=QX SS HTTP',
+ expected: {
+ type: 'ss',
+ name: 'QX SS HTTP',
+ server: 'qx-ss-http-plain.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ _qx_obfs_http: 'http',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'plain.example.com',
+ path: '/plain',
+ },
+ },
+ },
+ {
+ // QX examples contain the upstream "vemss-http" typo; keep
+ // parsing it so the original line can round-trip unchanged.
+ title: 'parses shadowsocks vemss-http lines as http obfs and keeps the QX alias',
+ input: 'shadowsocks=qx-ss-http.example.com:8388,method=aes-128-gcm,password=secret,obfs=vemss-http,obfs-host=obfs.example.com,obfs-uri=/resource,tag=QX SS VMess HTTP',
+ expected: {
+ type: 'ss',
+ name: 'QX SS VMess HTTP',
+ server: 'qx-ss-http.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ _qx_obfs_http: 'vemss-http',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'obfs.example.com',
+ path: '/resource',
+ },
+ },
+ },
+ {
+ title: 'parses shadowsocks shadowsocks-http lines as http obfs and keeps the QX token',
+ input: 'shadowsocks=qx-ss-shadowsocks-http.example.com:8388,method=aes-128-gcm,password=secret,obfs=shadowsocks-http,obfs-host=shadow.example.com,obfs-uri=/shadow,tag=QX SS Shadowsocks HTTP',
+ expected: {
+ type: 'ss',
+ name: 'QX SS Shadowsocks HTTP',
+ server: 'qx-ss-shadowsocks-http.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ _qx_obfs_http: 'shadowsocks-http',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'shadow.example.com',
+ path: '/shadow',
+ },
+ },
+ },
+ {
+ title: 'parses shadowsocks v2ray-plugin wss lines',
+ input: 'shadowsocks=qx-ss.example.com:8388,method=aes-128-gcm,password=secret,obfs=wss,obfs-host=obfs.example.com,obfs-uri=/ws,udp-relay=true,fast-open=true,tag=QX SS',
+ expected: {
+ type: 'ss',
+ name: 'QX SS',
+ server: 'qx-ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ udp: true,
+ tfo: true,
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ tls: true,
+ host: 'obfs.example.com',
+ path: '/ws',
+ },
+ },
+ },
+ {
+ title: 'parses shadowsocksr lines',
+ input: 'shadowsocks=qx-ssr.example.com:8389,method=aes-256-cfb,password=secret,ssr-protocol=auth_chain_b,ssr-protocol-param=device-id,obfs=tls1.2_ticket_fastauth,obfs-host=cdn.example.com,tag=QX SSR',
+ expected: {
+ type: 'ssr',
+ name: 'QX SSR',
+ server: 'qx-ssr.example.com',
+ port: 8389,
+ cipher: 'aes-256-cfb',
+ password: 'secret',
+ protocol: 'auth_chain_b',
+ 'protocol-param': 'device-id',
+ obfs: 'tls1.2_ticket_fastauth',
+ 'obfs-param': 'cdn.example.com',
+ },
+ },
+ {
+ title: 'parses vmess websocket tls lines',
+ input: `vmess=qx-vmess.example.com:443,method=chacha20,password=${UUID},obfs=wss,obfs-host=cdn.example.com,obfs-uri=/vmess,tls-verification=false,tls-host=sni.example.com,aead=true,udp-relay=true,tag=QX VMess`,
+ expected: {
+ type: 'vmess',
+ name: 'QX VMess',
+ server: 'qx-vmess.example.com',
+ port: 443,
+ cipher: 'chacha20',
+ uuid: UUID,
+ aead: true,
+ alterId: 0,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ udp: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/vmess',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ },
+ {
+ title: 'parses vmess http lines as http obfs and keeps the QX token',
+ input: `vmess=qx-vmess-http-plain.example.com:80,method=none,password=${UUID},obfs=http,obfs-host=plain.example.com,obfs-uri=/http,tag=QX VMess HTTP`,
+ expected: {
+ type: 'vmess',
+ name: 'QX VMess HTTP',
+ server: 'qx-vmess-http-plain.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ alterId: 0,
+ network: 'http',
+ _qx_obfs_http: 'http',
+ 'http-opts': {
+ path: ['/http'],
+ headers: {
+ Host: ['plain.example.com'],
+ },
+ },
+ },
+ },
+ {
+ // QX examples contain the upstream "vemss-http" typo; keep
+ // parsing it so the original line can round-trip unchanged.
+ title: 'parses vmess vemss-http lines as http obfs and keeps the QX alias',
+ input: `vmess=qx-vmess-vemss-http.example.com:80,method=none,password=${UUID},obfs=vemss-http,obfs-host=vemss.example.com,obfs-uri=/vemss,tag=QX VMess VMess HTTP`,
+ expected: {
+ type: 'vmess',
+ name: 'QX VMess VMess HTTP',
+ server: 'qx-vmess-vemss-http.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ alterId: 0,
+ network: 'http',
+ _qx_obfs_http: 'vemss-http',
+ 'http-opts': {
+ path: ['/vemss'],
+ headers: {
+ Host: ['vemss.example.com'],
+ },
+ },
+ },
+ },
+ {
+ title: 'parses vmess shadowsocks-http lines as http obfs and keeps the QX alias',
+ input: `vmess=qx-vmess-http.example.com:80,method=none,password=${UUID},obfs=shadowsocks-http,obfs-host=cdn.example.com,obfs-uri=/resource/file,tag=QX VMess Shadowsocks HTTP`,
+ expected: {
+ type: 'vmess',
+ name: 'QX VMess Shadowsocks HTTP',
+ server: 'qx-vmess-http.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ alterId: 0,
+ network: 'http',
+ _qx_obfs_http: 'shadowsocks-http',
+ 'http-opts': {
+ path: ['/resource/file'],
+ headers: {
+ Host: ['cdn.example.com'],
+ },
+ },
+ },
+ },
+ {
+ title: 'parses vless vmess-http lines as http obfs and keeps the QX token',
+ input: `vless=qx-vless-http.example.com:80,method=none,password=${UUID},obfs=vmess-http,obfs-host=vless.example.com,obfs-uri=/vless-http,tag=QX VLESS HTTP`,
+ expected: {
+ type: 'vless',
+ name: 'QX VLESS HTTP',
+ server: 'qx-vless-http.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ network: 'http',
+ _qx_obfs_http: 'vmess-http',
+ 'http-opts': {
+ path: ['/vless-http'],
+ headers: {
+ Host: ['vless.example.com'],
+ },
+ },
+ },
+ },
+ {
+ title: 'parses vless reality websocket lines',
+ input: `vless=qx-vless.example.com:443,method=none,password=${UUID},obfs=wss,obfs-host=cdn.example.com,obfs-uri=/vless,tls-verification=false,tls-host=sni.example.com,reality-base64-pubkey=pubkey,reality-hex-shortid=08,vless-flow=xtls-rprx-vision,tag=QX VLESS`,
+ expected: {
+ type: 'vless',
+ name: 'QX VLESS',
+ server: 'qx-vless.example.com',
+ port: 443,
+ cipher: 'none',
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/vless',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ },
+ {
+ title: 'parses vless over-tls lines using obfs-host as the tls server name alias',
+ input: `vless=qx-vless-overtls.example.com:37001,method=none,password=${UUID},obfs=over-tls,obfs-host=tls-name.example.com,tls13=true,tls-verification=false,reality-base64-pubkey=pubkey,reality-hex-shortid=01ab,vless-flow=xtls-rprx-vision,fast-open=false,udp-relay=true,tag=QX VLESS Over TLS Alias`,
+ expected: {
+ type: 'vless',
+ name: 'QX VLESS Over TLS Alias',
+ server: 'qx-vless-overtls.example.com',
+ port: 37001,
+ cipher: 'none',
+ uuid: UUID,
+ tls: true,
+ sni: 'tls-name.example.com',
+ 'skip-cert-verify': true,
+ flow: 'xtls-rprx-vision',
+ udp: true,
+ tfo: false,
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '01ab',
+ },
+ },
+ },
+ {
+ title: 'prefers explicit tls-host over obfs-host for vless over-tls lines',
+ input: `vless=qx-vless-overtls-priority.example.com:443,method=none,password=${UUID},obfs=over-tls,obfs-host=tls-alias.example.com,tls-host=explicit-sni.example.com,tls-verification=false,vless-flow=xtls-rprx-vision,tag=QX VLESS Over TLS Explicit`,
+ expected: {
+ type: 'vless',
+ name: 'QX VLESS Over TLS Explicit',
+ server: 'qx-vless-overtls-priority.example.com',
+ port: 443,
+ cipher: 'none',
+ uuid: UUID,
+ tls: true,
+ sni: 'explicit-sni.example.com',
+ 'skip-cert-verify': true,
+ flow: 'xtls-rprx-vision',
+ },
+ },
+ {
+ title: 'parses anytls standard tls lines',
+ input: 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-standard-tls-01',
+ expected: {
+ type: 'anytls',
+ name: 'anytls-standard-tls-01',
+ server: 'example.com',
+ port: 443,
+ password: 'pwd',
+ tls: true,
+ sni: 'apple.com',
+ udp: true,
+ },
+ },
+ {
+ title: 'parses anytls reality tls lines',
+ input: 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef,tag=anytls-reality-tls-01',
+ expected: {
+ type: 'anytls',
+ name: 'anytls-reality-tls-01',
+ server: 'example.com',
+ port: 443,
+ password: 'pwd',
+ tls: true,
+ sni: 'apple.com',
+ 'reality-opts': {
+ 'public-key':
+ 'k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk',
+ 'short-id': '0123456789abcdef',
+ },
+ },
+ },
+ {
+ title: 'parses trojan websocket tls lines',
+ input: 'trojan=qx-trojan.example.com:443,password=secret,obfs=wss,obfs-host=cdn.example.com,obfs-uri=/trojan,tls-verification=false,tls-host=sni.example.com,tls-cert-sha256=fingerprint,tag=QX Trojan',
+ expected: {
+ type: 'trojan',
+ name: 'QX Trojan',
+ server: 'qx-trojan.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ 'tls-fingerprint': 'fingerprint',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/trojan',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ },
+ {
+ title: 'parses http over tls lines',
+ input: 'http=qx-http.example.com:8443,username=user,password=pass,over-tls=true,tls-host=sni.example.com,tls-verification=false,fast-open=true,tag=QX HTTP',
+ expected: {
+ type: 'http',
+ name: 'QX HTTP',
+ server: 'qx-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ tfo: true,
+ },
+ },
+ {
+ title: 'parses socks5 over tls lines',
+ input: 'socks5=qx-socks.example.com:1080,username=user,password=pass,over-tls=true,tls-host=sni.example.com,tls-verification=false,udp-relay=true,tag=QX SOCKS5',
+ expected: {
+ type: 'socks5',
+ name: 'QX SOCKS5',
+ server: 'qx-socks.example.com',
+ port: 1080,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ udp: true,
+ },
+ },
+ ]);
+
+ });
+
+ describe('Loon raw inputs', function () {
+ registerCases([
+ {
+ title: 'parses shadowsocks obfs tls lines',
+ input: 'Loon SS=shadowsocks,loon-ss.example.com,8388,aes-128-gcm,"secret",obfs-name=tls,obfs-host=obfs.example.com,obfs-uri=/tls,udp=true,fast-open=true',
+ expected: {
+ type: 'ss',
+ name: 'Loon SS',
+ server: 'loon-ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ udp: true,
+ tfo: true,
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'tls',
+ host: 'obfs.example.com',
+ path: '/tls',
+ },
+ },
+ },
+ {
+ title: 'parses shadowsocksr lines',
+ input: 'Loon SSR=shadowsocksr,loon-ssr.example.com,8389,aes-256-cfb,"secret",protocol=auth_chain_b,protocol-param=device-id,obfs=tls1.2_ticket_fastauth,obfs-param=cdn.example.com',
+ expected: {
+ type: 'ssr',
+ name: 'Loon SSR',
+ server: 'loon-ssr.example.com',
+ port: 8389,
+ cipher: 'aes-256-cfb',
+ password: 'secret',
+ protocol: 'auth_chain_b',
+ 'protocol-param': 'device-id',
+ obfs: 'tls1.2_ticket_fastauth',
+ 'obfs-param': 'cdn.example.com',
+ },
+ },
+ {
+ title: 'parses vmess http tls lines',
+ input: `Loon VMess=vmess,loon-vmess.example.com,443,auto,"${UUID}",transport=http,host=cdn.example.com,path=/http,over-tls=true,tls-name=sni.example.com,skip-cert-verify=true,alterId=0`,
+ expected: {
+ type: 'vmess',
+ name: 'Loon VMess',
+ server: 'loon-vmess.example.com',
+ port: 443,
+ cipher: 'auto',
+ uuid: UUID,
+ alterId: 0,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ network: 'http',
+ 'http-opts': {
+ path: ['/http'],
+ headers: {
+ Host: ['cdn.example.com'],
+ },
+ },
+ },
+ },
+ {
+ title: 'parses vless websocket reality lines',
+ input: `Loon VLESS=vless,loon-vless.example.com,443,"${UUID}",transport=ws,host=cdn.example.com,path=/ws,over-tls=true,tls-name=sni.example.com,skip-cert-verify=true,flow=xtls-rprx-vision,public-key=pubkey,short-id=08`,
+ expected: {
+ type: 'vless',
+ name: 'Loon VLESS',
+ server: 'loon-vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ },
+ {
+ title: 'parses trojan websocket tls lines',
+ input: 'Loon Trojan=trojan,loon-trojan.example.com,443,"secret",transport=ws,host=cdn.example.com,path=/trojan,over-tls=true,tls-name=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'trojan',
+ name: 'Loon Trojan',
+ server: 'loon-trojan.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/trojan',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ },
+ {
+ title: 'parses anytls lines',
+ input: 'Loon AnyTLS=anytls,loon-anytls.example.com,443,"secret",transport=ws,host=cdn.example.com,path=/anytls,over-tls=true,tls-name=sni.example.com,skip-cert-verify=true,idle-session-timeout=30,max-stream-count=16',
+ expected: {
+ type: 'anytls',
+ name: 'Loon AnyTLS',
+ server: 'loon-anytls.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/anytls',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'idle-session-timeout': 30,
+ 'max-stream-count': 16,
+ },
+ },
+ {
+ title: 'parses hysteria2 lines',
+ input: 'Loon Hysteria2=hysteria2,loon-hy2.example.com,443,"secret",tls-name=peer.example.com,skip-cert-verify=true,download-bandwidth=100,salamander-password=mask,ecn=true',
+ expected: {
+ type: 'hysteria2',
+ name: 'Loon Hysteria2',
+ server: 'loon-hy2.example.com',
+ port: 443,
+ password: 'secret',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ down: '100',
+ obfs: 'salamander',
+ 'obfs-password': 'mask',
+ ecn: true,
+ },
+ },
+ {
+ title: 'parses hysteria2 port hopping lines',
+ input: 'Loon Hysteria2 Port Hopping=hysteria2,loon-hy2.example.com,443,"secret",server-ports="1000,2000-3000,5000",hop-interval=30,tls-name=peer.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'hysteria2',
+ name: 'Loon Hysteria2 Port Hopping',
+ server: 'loon-hy2.example.com',
+ port: 443,
+ ports: '1000,2000-3000,5000',
+ 'hop-interval': 30,
+ password: 'secret',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'parses https auth lines',
+ input: 'Loon HTTPS=https,loon-http.example.com,8443,user,"pass",tls-name=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'http',
+ name: 'Loon HTTPS',
+ server: 'loon-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'parses https lines with ip-mode markers',
+ input: 'Loon HTTPS IP Mode=https,loon-http.example.com,8443,user,"pass",ip-mode=v4-only',
+ expected: {
+ type: 'http',
+ name: 'Loon HTTPS IP Mode',
+ server: 'loon-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ 'ip-version': 'v4-only',
+ },
+ },
+ {
+ title: 'parses https lines with tls-cert-sha256 markers',
+ input: 'Loon HTTPS Fingerprint=https,loon-http.example.com,8443,user,"pass",tls-cert-sha256=fingerprint',
+ expected: {
+ type: 'http',
+ name: 'Loon HTTPS Fingerprint',
+ server: 'loon-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ 'tls-fingerprint': 'fingerprint',
+ },
+ },
+ {
+ title: 'parses https lines with tls-pubkey-sha256 markers',
+ input: 'Loon HTTPS Pubkey=https,loon-http.example.com,8443,user,"pass",tls-pubkey-sha256=pubkey',
+ expected: {
+ type: 'http',
+ name: 'Loon HTTPS Pubkey',
+ server: 'loon-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ 'tls-pubkey-sha256': 'pubkey',
+ },
+ },
+ {
+ title: 'parses socks5 over tls lines',
+ input: 'Loon SOCKS5=socks5,loon-socks.example.com,1080,user,"pass",over-tls=true,tls-name=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'socks5',
+ name: 'Loon SOCKS5',
+ server: 'loon-socks.example.com',
+ port: 1080,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'parses socks5 lines with ip-mode markers',
+ input: 'Loon SOCKS5 IP Mode=socks5,loon-socks.example.com,1080,user,"pass",ip-mode=v4-only',
+ expected: {
+ type: 'socks5',
+ name: 'Loon SOCKS5 IP Mode',
+ server: 'loon-socks.example.com',
+ port: 1080,
+ username: 'user',
+ password: 'pass',
+ 'ip-version': 'v4-only',
+ },
+ },
+ {
+ title: 'parses socks5 over tls lines with tls-pubkey-sha256 markers',
+ input: 'Loon SOCKS5 Pubkey=socks5,loon-socks.example.com,1080,user,"pass",over-tls=true,tls-pubkey-sha256=pubkey',
+ expected: {
+ type: 'socks5',
+ name: 'Loon SOCKS5 Pubkey',
+ server: 'loon-socks.example.com',
+ port: 1080,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ 'tls-pubkey-sha256': 'pubkey',
+ },
+ },
+ {
+ title: 'parses wireguard lines',
+ input: 'Loon WG=wireguard,interface-ip=10.0.0.2,interface-ipv6=fd00::2,private-key=private-key,mtu=1280,keepalive=25,dns=1.1.1.1,dnsv6=2606:4700:4700::1111,peers=[{endpoint=loon-wg.example.com:51820,public-key=public-key,allowed-ips="0.0.0.0/0, ::/0",reserved=[1,2,3]}]',
+ expected: {
+ type: 'wireguard',
+ name: 'Loon WG',
+ server: 'loon-wg.example.com',
+ port: 51820,
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'private-key': 'private-key',
+ 'public-key': 'public-key',
+ mtu: 1280,
+ keepalive: 25,
+ reserved: [1, 2, 3],
+ 'allowed-ips': ['0.0.0.0/0', '::/0'],
+ dns: ['1.1.1.1', '2606:4700:4700::1111'],
+ 'remote-dns-resolve': true,
+ },
+ },
+ ]);
+
+ it('rejects hysteria2 hop interval ranges', function () {
+ expect(
+ parseAll(
+ 'Loon Hysteria2 Hop Range=hysteria2,loon-hy2.example.com,443,"secret",hop-interval=15-30',
+ ),
+ ).to.have.length(0);
+ });
+ });
+
+ describe('Surge raw inputs', function () {
+ registerCases([
+ {
+ title: 'parses direct lines',
+ input: 'Surge Direct = direct, udp-relay=true',
+ expected: {
+ type: 'direct',
+ name: 'Surge Direct',
+ udp: true,
+ },
+ },
+ {
+ title: 'parses anytls lines',
+ input: 'Surge AnyTLS = anytls,surge-anytls.example.com,443,password=secret,sni=sni.example.com,skip-cert-verify=true,reuse=true',
+ expected: {
+ type: 'anytls',
+ name: 'Surge AnyTLS',
+ server: 'surge-anytls.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ reuse: true,
+ },
+ },
+ {
+ title: 'parses trust-tunnel lines',
+ input: 'Surge TrustTunnel = trust-tunnel,surge-trust.example.com,443,username=user,password=secret,headers=X-Client:Surge;X-Token:abc,sni=sni.example.com,skip-cert-verify=true,reuse=true',
+ expected: {
+ type: 'trusttunnel',
+ name: 'Surge TrustTunnel',
+ server: 'surge-trust.example.com',
+ port: 443,
+ username: 'user',
+ password: 'secret',
+ tls: true,
+ headers: {
+ 'X-Client': 'Surge',
+ 'X-Token': 'abc',
+ },
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ reuse: true,
+ },
+ },
+ {
+ title: 'parses trust-tunnel lines with double-quoted max-streams',
+ input: 'Surge TrustTunnel Max Streams = trust-tunnel,surge-trust.example.com,443,username=user,password=secret,headers=X-Client:Surge,max-streams="3",sni=sni.example.com,skip-cert-verify=true,reuse=true',
+ expected: {
+ type: 'trusttunnel',
+ name: 'Surge TrustTunnel Max Streams',
+ server: 'surge-trust.example.com',
+ port: 443,
+ username: 'user',
+ password: 'secret',
+ tls: true,
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ 'max-streams': 3,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ reuse: true,
+ },
+ },
+ {
+ title: 'parses trust-tunnel lines with single-quoted max-streams',
+ input: "Surge TrustTunnel Single Max Streams = trust-tunnel,surge-trust-single.example.com,443,max-streams='2'",
+ expected: {
+ type: 'trusttunnel',
+ name: 'Surge TrustTunnel Single Max Streams',
+ server: 'surge-trust-single.example.com',
+ port: 443,
+ tls: true,
+ 'max-streams': 2,
+ },
+ },
+ {
+ title: 'parses h2-connect lines with dynamic headers',
+ input: 'Surge H2 = h2-connect,h2.example.com,443,headers=X-Padding:,sni=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'h2-connect',
+ name: 'Surge H2',
+ server: 'h2.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Padding': '',
+ },
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'parses h2-connect lines with max-streams',
+ input: 'Surge H2 Max Streams = h2-connect,h2.example.com,443,headers=X-Padding:,max-streams=1,sni=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'h2-connect',
+ name: 'Surge H2 Max Streams',
+ server: 'h2.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Padding': '',
+ },
+ 'max-streams': 1,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'keeps quoted Surge headers with semicolons inside User-Agent values',
+ input: '1=http,163.177.17.6,443,headers="Host:153.3.236.22:443;X-T5-Auth:683556433;Connection:Keep-Alive;User-Agent:okhttp/3.11.0 Dalvik/2.1.0 (Linux; U; Android 11; Redmi K30 5G Build/RKQ1.200826.002) baiduboxapp/11.0.5.12 (Baidu; P1 11)"',
+ expected: {
+ type: 'http',
+ name: '1',
+ server: '163.177.17.6',
+ port: 443,
+ headers: {
+ Host: '153.3.236.22:443',
+ 'X-T5-Auth': '683556433',
+ Connection: 'Keep-Alive',
+ 'User-Agent':
+ 'okhttp/3.11.0 Dalvik/2.1.0 (Linux; U; Android 11; Redmi K30 5G Build/RKQ1.200826.002) baiduboxapp/11.0.5.12 (Baidu; P1 11)',
+ },
+ },
+ },
+ {
+ title: 'parses quoted Surge header keys and values',
+ input: `Surge Quoted Headers = https,quoted.example.com,443,headers='Host':'153.3.236.22:443';"X-T5-Auth":"683556433";Connection:"Keep-Alive";"User-Agent":"okhttp/3.11.0 Dalvik/2.1.0 (Linux; U; Android 11; Redmi K30 5G Build/RKQ1.200826.002) baiduboxapp/11.0.5.12 (Baidu; P1 11)",sni=sni.example.com`,
+ expected: {
+ type: 'http',
+ name: 'Surge Quoted Headers',
+ server: 'quoted.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ Host: '153.3.236.22:443',
+ 'X-T5-Auth': '683556433',
+ Connection: 'Keep-Alive',
+ 'User-Agent':
+ 'okhttp/3.11.0 Dalvik/2.1.0 (Linux; U; Android 11; Redmi K30 5G Build/RKQ1.200826.002) baiduboxapp/11.0.5.12 (Baidu; P1 11)',
+ },
+ sni: 'sni.example.com',
+ },
+ },
+ {
+ title: 'parses Surge headers with nested quote values containing commas',
+ input: `Surge Nested Headers = https,nested.example.com,443,headers="Host:"nested.example.com";X-Comma:"a,b";User-Agent:"client/1.0 (Linux; U; Android 11)"",sni=sni.example.com`,
+ expected: {
+ type: 'http',
+ name: 'Surge Nested Headers',
+ server: 'nested.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ Host: 'nested.example.com',
+ 'X-Comma': 'a,b',
+ 'User-Agent': 'client/1.0 (Linux; U; Android 11)',
+ },
+ sni: 'sni.example.com',
+ },
+ },
+ {
+ title: 'parses Surge headers with nested quote values containing quote characters',
+ input: `Surge Nested Quote Headers = https,quote.example.com,443,headers="X-Quote:"a"b";X-Semi:"x;y"",sni=sni.example.com`,
+ expected: {
+ type: 'http',
+ name: 'Surge Nested Quote Headers',
+ server: 'quote.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Quote': 'a"b',
+ 'X-Semi': 'x;y',
+ },
+ sni: 'sni.example.com',
+ },
+ },
+ {
+ title: 'parses unwrapped Surge headers that start with quoted keys',
+ input: `Surge Quoted Key Start = http,quoted-key.example.com,8080,headers='Host':'quoted-key.example.com';"X-Token":"abc",test-url=http://example.com`,
+ expected: {
+ type: 'http',
+ name: 'Surge Quoted Key Start',
+ server: 'quoted-key.example.com',
+ port: 8080,
+ headers: {
+ Host: 'quoted-key.example.com',
+ 'X-Token': 'abc',
+ },
+ 'test-url': 'http://example.com',
+ },
+ },
+ {
+ title: 'parses ssh lines',
+ input: 'Surge SSH = ssh,surge-ssh.example.com,22,user,pass,server-fingerprint=sshfp',
+ expected: {
+ type: 'ssh',
+ name: 'Surge SSH',
+ server: 'surge-ssh.example.com',
+ port: 22,
+ username: 'user',
+ password: 'pass',
+ 'server-fingerprint': 'sshfp',
+ },
+ },
+ {
+ title: 'parses shadowsocks obfs tls lines',
+ input: 'Surge SS = ss,surge-ss.example.com,8388,encrypt-method=aes-128-gcm,password=secret,obfs=tls,obfs-host=obfs.example.com,obfs-uri=/tls',
+ expected: {
+ type: 'ss',
+ name: 'Surge SS',
+ server: 'surge-ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'tls',
+ host: 'obfs.example.com',
+ path: '/tls',
+ },
+ },
+ },
+ {
+ title: 'parses vmess websocket tls lines',
+ input: `Surge VMess = vmess,surge-vmess.example.com,443,username=${UUID},ws=true,ws-path=/vmess,ws-headers=Host:cdn.example.com,skip-cert-verify=true,sni=sni.example.com,tls=true,vmess-aead=true,udp-relay=true`,
+ expected: {
+ type: 'vmess',
+ name: 'Surge VMess',
+ server: 'surge-vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ aead: true,
+ alterId: 0,
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ udp: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/vmess',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ },
+ {
+ title: 'parses trojan websocket tls lines',
+ input: 'Surge Trojan = trojan,surge-trojan.example.com,443,password=secret,ws=true,ws-path=/trojan,ws-headers=Host:cdn.example.com,skip-cert-verify=true,sni=sni.example.com,tls=true',
+ expected: {
+ type: 'trojan',
+ name: 'Surge Trojan',
+ server: 'surge-trojan.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/trojan',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ },
+ {
+ title: 'parses https auth lines',
+ input: 'Surge HTTPS = https,surge-http.example.com,8443,user,pass,headers=X-Token:abc,sni=sni.example.com,skip-cert-verify=true',
+ expected: {
+ type: 'http',
+ name: 'Surge HTTPS',
+ server: 'surge-http.example.com',
+ port: 8443,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ headers: {
+ 'X-Token': 'abc',
+ },
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ },
+ },
+ {
+ title: 'parses socks5 tls lines',
+ input: 'Surge SOCKS5 = socks5-tls,surge-socks.example.com,1080,user,pass,sni=sni.example.com,skip-cert-verify=true,udp-relay=true',
+ expected: {
+ type: 'socks5',
+ name: 'Surge SOCKS5',
+ server: 'surge-socks.example.com',
+ port: 1080,
+ username: 'user',
+ password: 'pass',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ udp: true,
+ },
+ },
+ {
+ title: 'parses snell obfs tls lines',
+ input: 'Surge Snell = snell,surge-snell.example.com,443,psk=secret,version=3,obfs=tls,obfs-host=obfs.example.com,obfs-uri=/snell',
+ expected: {
+ type: 'snell',
+ name: 'Surge Snell',
+ server: 'surge-snell.example.com',
+ port: 443,
+ psk: 'secret',
+ version: 3,
+ 'obfs-opts': {
+ mode: 'tls',
+ host: 'obfs.example.com',
+ path: '/snell',
+ },
+ },
+ },
+ {
+ title: 'parses tuic v5 lines',
+ input: `Surge TUIC = tuic-v5,surge-tuic.example.com,443,uuid=${UUID},password=secret,sni=sni.example.com,skip-cert-verify=true,alpn=h3,ecn=true,port-hopping=9000;9002-9004`,
+ expected: {
+ type: 'tuic',
+ name: 'Surge TUIC',
+ server: 'surge-tuic.example.com',
+ port: 443,
+ version: 5,
+ uuid: UUID,
+ password: 'secret',
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ alpn: ['h3'],
+ ecn: true,
+ ports: '9000,9002-9004',
+ },
+ },
+ {
+ title: 'parses wireguard lines',
+ input: 'Surge WG = wireguard,section-name=wireguard-cellular',
+ expected: {
+ type: 'wireguard-surge',
+ name: 'Surge WG',
+ 'section-name': 'wireguard-cellular',
+ },
+ },
+ {
+ title: 'parses hysteria2 lines',
+ input: 'Surge Hysteria2 = hysteria2,surge-hy2.example.com,443,password=secret,sni=peer.example.com,skip-cert-verify=true,download-bandwidth=100,ecn=true,salamander-password=mask,port-hopping=8443-8445',
+ expected: {
+ type: 'hysteria2',
+ name: 'Surge Hysteria2',
+ server: 'surge-hy2.example.com',
+ port: 443,
+ password: 'secret',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ down: '100',
+ ecn: true,
+ obfs: 'salamander',
+ 'obfs-password': 'mask',
+ ports: '8443-8445',
+ },
+ },
+ {
+ title: 'parses external definitions with exec, args, local-port and addresses',
+ input: 'Surge External = external, exec="/usr/bin/ssh", local-port="1080", args="-D", args="localhost:1080", addresses="[2001:db8::1]", addresses="1.1.1.1"',
+ expected: {
+ type: 'external',
+ name: 'Surge External',
+ exec: '/usr/bin/ssh',
+ 'local-port': '1080',
+ args: ['-D', 'localhost:1080'],
+ addresses: ['2001:db8::1', '1.1.1.1'],
+ },
+ },
+ ]);
+ });
+});
diff --git a/backend/src/test/proxy-preprocessors/preprocessors.spec.js b/backend/src/test/proxy-preprocessors/preprocessors.spec.js
new file mode 100644
index 0000000000..cba0ce96b2
--- /dev/null
+++ b/backend/src/test/proxy-preprocessors/preprocessors.spec.js
@@ -0,0 +1,203 @@
+import { expect } from 'chai';
+import { Base64 } from 'js-base64';
+import { describe, it } from 'mocha';
+
+import PREPROCESSORS, {
+ normalizeClashYaml,
+} from '@/core/proxy-utils/preprocessors';
+
+function getPreprocessor(name) {
+ const processor = PREPROCESSORS.find((item) => item.name === name);
+ expect(processor, name).to.exist;
+ return processor;
+}
+
+describe('Proxy preprocessors', function () {
+ describe('normalizeClashYaml', function () {
+ it('quotes reality short-id scalars that may be re-parsed as numbers', function () {
+ const input = `proxies:
+ - name: test-1
+ type: vless
+ reality-opts:
+ short-id: 08
+ - name: test-2
+ type: vless
+ reality-opts:
+ short-id: 0088
+ - name: test-3
+ type: vless
+ reality-opts:
+ short-id: '51'
+ - name: test-4
+ type: vless
+ reality-opts:
+ short-id: ""
+ - name: test-5
+ type: vless
+ reality-opts:
+ short-id: null
+`;
+
+ const output = normalizeClashYaml(input);
+
+ expect(output).to.include('short-id: "08"');
+ expect(output).to.include('short-id: "0088"');
+ expect(output).to.include("short-id: '51'");
+ expect(output).to.include('short-id: ""');
+ expect(output).to.include('short-id: null');
+ });
+
+ it('keeps non-clash or invalid yaml input untouched', function () {
+ const invalid = 'proxies:\n - name: broken\n short-id: [';
+ const unrelated = 'ss://YWVzLTEyOC1nY206c2VjcmV0@example.com:8388';
+
+ expect(normalizeClashYaml(invalid)).to.equal(invalid);
+ expect(normalizeClashYaml(unrelated)).to.equal(unrelated);
+ });
+ });
+
+ describe('HTML preprocessor', function () {
+ it('detects html payloads and discards them', function () {
+ const processor = getPreprocessor('HTML');
+ const raw = 'blocked';
+
+ expect(processor.test(raw)).to.equal(true);
+ expect(processor.parse(raw)).to.equal('');
+ });
+ });
+
+ describe('Base64 preprocessor', function () {
+ it('decodes base64 subscriptions that expand to proxy lines', function () {
+ const processor = getPreprocessor('Base64 Pre-processor');
+ const decoded =
+ 'ss://YWVzLTEyOC1nY206c2VjcmV0@example.com:8388#Node\n' +
+ 'trojan://secret@example.com:443#Trojan';
+ const encoded = Base64.encode(decoded);
+
+ expect(processor.test(encoded)).to.equal(true);
+ expect(processor.parse(encoded)).to.equal(decoded);
+ });
+
+ it('ignores already decoded proxy payloads', function () {
+ const processor = getPreprocessor('Base64 Pre-processor');
+ const raw = 'ss://YWVzLTEyOC1nY206c2VjcmV0@example.com:8388#Node';
+
+ expect(processor.test(raw)).to.equal(false);
+ });
+ });
+
+ describe('Fallback Base64 preprocessor', function () {
+ it('decodes valid fallback payloads and preserves invalid ones', function () {
+ const processor = getPreprocessor('Fallback Base64 Pre-processor');
+ const decoded = 'vmess://dGVzdA==';
+ const encoded = Base64.encode(decoded);
+ const invalid = 'not-base64-and-not-a-proxy';
+
+ expect(processor.test(invalid)).to.equal(true);
+ expect(processor.parse(encoded)).to.equal(decoded);
+ expect(processor.parse(invalid)).to.equal(invalid);
+ });
+ });
+
+ describe('Clash preprocessor', function () {
+ it('converts clash yaml proxies into json lines', function () {
+ const processor = getPreprocessor('Clash Pre-processor');
+ const raw = `proxies:
+ - name: Clash SS
+ type: ss
+ server: ss.example.com
+ port: 8388
+ cipher: aes-128-gcm
+ password: secret
+ - name: Clash VLESS
+ type: vless
+ server: vless.example.com
+ port: 443
+ uuid: 11111111-1111-4111-8111-111111111111
+ reality-opts:
+ short-id: 08
+`;
+
+ expect(processor.test(raw)).to.equal(true);
+
+ const output = processor.parse(raw);
+ const wrapped = processor.parse(raw, true);
+
+ expect(output).to.include('"name":"Clash SS"');
+ expect(output).to.include('"name":"Clash VLESS"');
+ expect(output).to.include('"short-id":"08"');
+ expect(wrapped).to.match(/^proxies:\n - /);
+ });
+ });
+
+ describe('SSD preprocessor', function () {
+ it('expands SSD documents into ss uris with inherited and overridden fields', function () {
+ const processor = getPreprocessor('SSD Pre-processor');
+ const raw = `ssd://${Base64.encode(
+ JSON.stringify({
+ airport: 'Test SSD',
+ port: 8388,
+ encryption: 'aes-128-gcm',
+ password: 'base-secret',
+ servers: [
+ {
+ server: 'ssd1.example.com',
+ remarks: 'SSD 1',
+ },
+ {
+ server: 'ssd2.example.com',
+ port: 443,
+ encryption: 'chacha20-ietf-poly1305',
+ password: 'override-secret',
+ remarks: 'SSD 2',
+ plugin: 'v2ray-plugin',
+ plugin_options: 'host=cdn.example.com;path=/ws;tls',
+ },
+ ],
+ }),
+ )}`;
+
+ expect(processor.test(raw)).to.equal(true);
+
+ const output = processor.parse(raw).split('\n');
+
+ expect(output).to.have.length(2);
+ expect(output[0]).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:base-secret',
+ )}@ssd1.example.com:8388#SSD 1`,
+ );
+ expect(output[1]).to.equal(
+ `ss://${Base64.encode(
+ 'chacha20-ietf-poly1305:override-secret',
+ )}@ssd2.example.com:443/?plugin=${encodeURIComponent(
+ 'v2ray-plugin;host=cdn.example.com;path=/ws;tls',
+ )}#SSD 2`,
+ );
+ });
+ });
+
+ describe('Full config preprocessor', function () {
+ it('extracts the [Proxy] block from full config payloads', function () {
+ const processor = getPreprocessor('Full Config Preprocessor');
+ const raw = `[General]
+skip-proxy = 192.168.0.0/16
+
+[Proxy]
+Node 1 = ss,example.com,8388,encrypt-method=aes-128-gcm,password=secret
+Node 2 = trojan,example.com,443,password=secret
+
+[Rule]
+FINAL,DIRECT
+`;
+
+ expect(processor.test(raw)).to.equal(true);
+ expect(processor.parse(raw).trim()).to.equal(
+ [
+ 'Node 1 = ss,example.com,8388,encrypt-method=aes-128-gcm,password=secret',
+ 'Node 2 = trojan,example.com,443,password=secret',
+ ].join('\n'),
+ );
+ });
+ });
+});
diff --git a/backend/src/test/proxy-processors/resolve-domain.spec.js b/backend/src/test/proxy-processors/resolve-domain.spec.js
new file mode 100644
index 0000000000..3278a08cba
--- /dev/null
+++ b/backend/src/test/proxy-processors/resolve-domain.spec.js
@@ -0,0 +1,186 @@
+import { expect } from 'chai';
+import { describe, it, beforeEach, afterEach } from 'mocha';
+
+import $ from '@/core/app';
+import PROCESSORS, { ApplyProcessor } from '@/core/proxy-utils/processors';
+import resourceCache from '@/utils/resource-cache';
+import { hex_md5 } from '@/vendor/md5';
+
+const ResolveDomainOperator = PROCESSORS['Resolve Domain Operator'];
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+describe('Resolve Domain Operator', function () {
+ let originalGoogleResolver;
+ let cacheKeys;
+
+ beforeEach(function () {
+ originalGoogleResolver = ResolveDomainOperator.resolver.Google;
+ cacheKeys = [];
+ });
+
+ afterEach(function () {
+ ResolveDomainOperator.resolver.Google = originalGoogleResolver;
+ cacheKeys.forEach((key) => {
+ delete resourceCache.resourceCache[key];
+ });
+ resourceCache._persist();
+ });
+
+ it('limits resolver requests to the configured unresolved unique domains', async function () {
+ const calls = [];
+ const resolvedIps = {
+ 'a.example.com': '192.0.2.10',
+ 'b.example.com': '192.0.2.11',
+ 'c.example.com': '192.0.2.12',
+ };
+ let activeRequests = 0;
+ let maxActiveRequests = 0;
+
+ ResolveDomainOperator.resolver.Google = async (domain) => {
+ calls.push(domain);
+ activeRequests += 1;
+ maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
+ await sleep(5);
+ activeRequests -= 1;
+ return resolvedIps[domain];
+ };
+
+ const processor = ResolveDomainOperator({
+ provider: 'Google',
+ type: 'IPv4',
+ concurrency: 2,
+ });
+ const output = await ApplyProcessor(processor, [
+ { name: 'A', server: 'a.example.com', port: 443 },
+ { name: 'A Duplicate', server: 'a.example.com', port: 443 },
+ { name: 'Existing IP', server: '192.0.2.1', port: 443 },
+ {
+ name: 'No Resolve',
+ server: 'skip.example.com',
+ port: 443,
+ '_no-resolve': true,
+ },
+ { name: 'B', server: 'b.example.com', port: 443 },
+ { name: 'C', server: 'c.example.com', port: 443 },
+ ]);
+
+ expect(maxActiveRequests).to.equal(2);
+ expect(calls).to.have.members([
+ 'a.example.com',
+ 'b.example.com',
+ 'c.example.com',
+ ]);
+ expect(calls).to.have.length(3);
+ expect(output.find((proxy) => proxy.name === 'A').server).to.equal(
+ '192.0.2.10',
+ );
+ expect(
+ output.find((proxy) => proxy.name === 'A Duplicate').server,
+ ).to.equal('192.0.2.10');
+ expect(
+ output.find((proxy) => proxy.name === 'Existing IP').server,
+ ).to.equal('192.0.2.1');
+ expect(
+ output.find((proxy) => proxy.name === 'No Resolve').server,
+ ).to.equal('skip.example.com');
+ });
+
+ it('excludes cache hits from the concurrency pool', async function () {
+ const cachedDomain = 'cached-resolve-domain.example.com';
+ const uncachedDomain = 'uncached-resolve-domain.example.com';
+ const cacheKey = hex_md5(`GOOGLE:${cachedDomain}:IPv4`);
+ cacheKeys.push(cacheKey);
+ resourceCache.set(cacheKey, ['192.0.2.30']);
+ const calls = [];
+
+ ResolveDomainOperator.resolver.Google = async (domain) => {
+ calls.push(domain);
+ return ['192.0.2.31'];
+ };
+
+ const processor = ResolveDomainOperator({
+ provider: 'Google',
+ type: 'IPv4',
+ concurrency: 1,
+ });
+ const output = await ApplyProcessor(processor, [
+ { name: 'Cached', server: cachedDomain, port: 443 },
+ { name: 'Uncached', server: uncachedDomain, port: 443 },
+ ]);
+
+ expect(calls).to.deep.equal([uncachedDomain]);
+ expect(output.find((proxy) => proxy.name === 'Cached').server).to.equal(
+ '192.0.2.30',
+ );
+ expect(
+ output.find((proxy) => proxy.name === 'Uncached').server,
+ ).to.equal('192.0.2.31');
+ });
+
+ it('keeps the existing default concurrency at 15', async function () {
+ const domains = Array.from({ length: 20 }, (_, index) => ({
+ name: `Node ${index}`,
+ server: `node-${index}.example.com`,
+ port: 443,
+ }));
+ let activeRequests = 0;
+ let maxActiveRequests = 0;
+
+ ResolveDomainOperator.resolver.Google = async (domain) => {
+ activeRequests += 1;
+ maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
+ await sleep(5);
+ activeRequests -= 1;
+ return `192.0.2.${Number(domain.match(/\d+/)[0]) + 1}`;
+ };
+
+ const processor = ResolveDomainOperator({
+ provider: 'Google',
+ type: 'IPv4',
+ });
+ await ApplyProcessor(processor, domains);
+
+ expect(maxActiveRequests).to.equal(15);
+ });
+
+ it('rejects invalid concurrency values', function () {
+ expect(() =>
+ ResolveDomainOperator({
+ provider: 'Google',
+ type: 'IPv4',
+ concurrency: 0,
+ }),
+ ).to.throw('域名解析并发数应为大于 0 的整数');
+ });
+
+ it('warns but allows high concurrency values', async function () {
+ const originalWarn = $.warn;
+ const warnings = [];
+ $.warn = (message) => warnings.push(message);
+ ResolveDomainOperator.resolver.Google = async () => '192.0.2.40';
+
+ try {
+ const processor = ResolveDomainOperator({
+ provider: 'Google',
+ type: 'IPv4',
+ concurrency: 21,
+ });
+ const output = await ApplyProcessor(processor, [
+ { name: 'High Concurrency', server: 'high.example.com', port: 443 },
+ ]);
+
+ expect(output[0].server).to.equal('192.0.2.40');
+ expect(
+ warnings.some(
+ (message) =>
+ message.includes('21') && message.includes('20'),
+ ),
+ ).to.equal(true);
+ } finally {
+ $.warn = originalWarn;
+ }
+ });
+});
diff --git a/backend/src/test/proxy-producers/helpers.js b/backend/src/test/proxy-producers/helpers.js
new file mode 100644
index 0000000000..eaec62269e
--- /dev/null
+++ b/backend/src/test/proxy-producers/helpers.js
@@ -0,0 +1,54 @@
+import { expect } from 'chai';
+
+import { ProxyUtils } from '@/core/proxy-utils';
+import { safeLoad } from '@/utils/yaml';
+
+export const UUID = '11111111-1111-4111-8111-111111111111';
+
+export function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function asList(proxies) {
+ return Array.isArray(proxies) ? proxies : [proxies];
+}
+
+export function produceInternal(platform, proxies, opts = {}) {
+ return ProxyUtils.produce(clone(asList(proxies)), platform, 'internal', opts);
+}
+
+export function produceExternal(platform, proxies, opts = {}) {
+ const output = ProxyUtils.produce(
+ clone(asList(proxies)),
+ platform,
+ 'external',
+ opts,
+ );
+ expect(output, platform).to.be.a('string').and.not.equal('');
+ return output;
+}
+
+export function loadProducedYaml(platform, proxies, opts = {}) {
+ return safeLoad(produceExternal(platform, proxies, opts));
+}
+
+export function loadProducedJson(platform, proxies, opts = {}) {
+ return JSON.parse(produceExternal(platform, proxies, opts));
+}
+
+export function expectSubset(actual, expected, path = 'value') {
+ if (Array.isArray(expected)) {
+ expect(actual, path).to.deep.equal(expected);
+ return;
+ }
+
+ if (expected && typeof expected === 'object') {
+ expect(actual, path).to.be.an('object');
+ for (const [key, value] of Object.entries(expected)) {
+ expectSubset(actual[key], value, `${path}.${key}`);
+ }
+ return;
+ }
+
+ expect(actual, path).to.deep.equal(expected);
+}
diff --git a/backend/src/test/proxy-producers/structured.spec.js b/backend/src/test/proxy-producers/structured.spec.js
new file mode 100644
index 0000000000..19537d5516
--- /dev/null
+++ b/backend/src/test/proxy-producers/structured.spec.js
@@ -0,0 +1,3210 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import $ from '@/core/app';
+import { ProxyUtils } from '@/core/proxy-utils';
+import {
+ UUID,
+ expectSubset,
+ loadProducedJson,
+ loadProducedYaml,
+ produceExternal,
+ produceInternal,
+} from './helpers';
+
+function captureWarns(fn) {
+ const originalWarn = $.warn;
+ const warnings = [];
+ $.warn = (message) => warnings.push(message);
+ try {
+ const result = fn();
+ return { result, warnings };
+ } finally {
+ $.warn = originalWarn;
+ }
+}
+
+function captureErrors(fn) {
+ const originalError = $.error;
+ const errors = [];
+ $.error = (message) => errors.push(message);
+ try {
+ const result = fn();
+ return { result, errors };
+ } finally {
+ $.error = originalError;
+ }
+}
+
+describe('Proxy structured producers', function () {
+ it('filters unsupported Clash proxies by default and normalizes vmess ws early data', function () {
+ const proxies = [
+ {
+ type: 'vmess',
+ name: 'Clash VMess',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'chacha20',
+ aead: true,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ {
+ type: 'vless',
+ name: 'Clash Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ ];
+
+ const internal = produceInternal('Clash', proxies);
+ const external = loadProducedYaml('Clash', proxies);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vmess',
+ name: 'Clash VMess',
+ cipher: 'auto',
+ alterId: 0,
+ servername: 'sni.example.com',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ 'max-early-data': 2048,
+ },
+ });
+ });
+
+ it('keeps unsupported Clash proxies when include-unsupported-proxy is enabled', function () {
+ const internal = produceInternal(
+ 'Clash',
+ {
+ type: 'vless',
+ name: 'Clash Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ { 'include-unsupported-proxy': true },
+ );
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'Clash Reality',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ });
+
+ it('keeps only websocket shadowsocks v2ray-plugin modes for Mihomo and Stash by default', function () {
+ const buildProxy = (name, mode) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode,
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ },
+ });
+
+ const proxies = [
+ buildProxy('WS', 'websocket'),
+ buildProxy('QUIC', 'quic'),
+ ];
+
+ for (const platform of ['Mihomo', 'Stash']) {
+ const internal = produceInternal(platform, proxies);
+ const external = loadProducedYaml(platform, proxies, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expect(internal, platform).to.have.length(1);
+ expect(internal[0].name, platform).to.equal('WS');
+ expect(
+ external.proxies.map((proxy) => proxy.name),
+ platform,
+ ).to.deep.equal(['WS', 'QUIC']);
+ }
+ });
+
+ it('keeps Mihomo Snell versions 1 through 5', function () {
+ const proxies = [1, 2, 3, 4, 5, 6].map((version) => ({
+ type: 'snell',
+ name: `Snell ${version}`,
+ server: 'snell.example.com',
+ port: 44046,
+ psk: 'secret',
+ version,
+ udp: true,
+ }));
+
+ const internal = produceInternal('Mihomo', proxies);
+ const external = loadProducedYaml('Mihomo', proxies);
+
+ expect(internal.map((proxy) => proxy.version)).to.deep.equal([
+ 1, 2, 3, 4, 5,
+ ]);
+ expect(external.proxies.map((proxy) => proxy.version)).to.deep.equal([
+ 1, 2, 3, 4, 5,
+ ]);
+ expect(
+ internal.find((proxy) => proxy.version === 1),
+ ).to.not.have.property('udp');
+ expect(
+ internal.find((proxy) => proxy.version === 2),
+ ).to.not.have.property('udp');
+ expect(internal.find((proxy) => proxy.version === 4).udp).to.equal(
+ true,
+ );
+ expect(internal.find((proxy) => proxy.version === 5).udp).to.equal(
+ true,
+ );
+ });
+
+ it('keeps Snell in sing-box only when include-unsupported-proxy is enabled', function () {
+ const proxy = {
+ type: 'snell',
+ name: 'sing-box Snell',
+ server: 'snell.example.com',
+ port: 44046,
+ psk: 'secret',
+ version: 4,
+ udp: false,
+ tfo: true,
+ 'fast-open': true,
+ reuse: true,
+ 'dialer-proxy': 'proxy-out',
+ 'ip-version': 'v4-only',
+ _dns_server: 'dns-out',
+ _domain_resolver: {
+ client_subnet: '1.2.3.0/24',
+ },
+ 'obfs-opts': {
+ mode: 'tls',
+ host: 'obfs.example.com',
+ path: '/ignored-by-sing-box',
+ },
+ };
+
+ const { result, errors } = captureErrors(() =>
+ produceInternal('sing-box', proxy),
+ );
+ const internal = produceInternal('sing-box', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+ const external = loadProducedJson('sing-box', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expect(result).to.deep.equal([]);
+ expect(errors).to.have.length(1);
+ expect(errors[0]).to.include(
+ 'Platform sing-box does not support proxy type: snell',
+ );
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ tag: 'sing-box Snell',
+ type: 'snell',
+ server: 'snell.example.com',
+ server_port: 44046,
+ psk: 'secret',
+ version: 4,
+ reuse: true,
+ network: 'tcp',
+ obfs_mode: 'tls',
+ obfs_host: 'obfs.example.com',
+ tcp_fast_open: true,
+ udp_fragment: true,
+ detour: 'proxy-out',
+ domain_resolver: {
+ server: 'dns-out',
+ strategy: 'ipv4_only',
+ client_subnet: '1.2.3.0/24',
+ },
+ });
+ expect(internal[0]).to.not.have.property('obfs_uri');
+ expectSubset(external.outbounds[0], internal[0]);
+ });
+
+ it('keeps sing-box Snell versions documented by reF1nd and rejects newer versions', function () {
+ const proxies = [1, 2, 3, 4, 5, 6, '4x'].map((version) => ({
+ type: 'snell',
+ name: `sing-box Snell ${version}`,
+ server: 'snell.example.com',
+ port: 44046,
+ psk: 'secret',
+ version,
+ udp: true,
+ reuse: true,
+ }));
+
+ const { result, errors } = captureErrors(() =>
+ produceInternal('sing-box', proxies, {
+ 'include-unsupported-proxy': true,
+ }),
+ );
+
+ expect(result.map((proxy) => proxy.version)).to.deep.equal([
+ 1, 2, 3, 4, 5,
+ ]);
+ expect(
+ result.find((proxy) => proxy.version === 1),
+ ).to.not.have.property('network');
+ expect(
+ result.find((proxy) => proxy.version === 3),
+ ).to.not.have.property('reuse');
+ expect(result.find((proxy) => proxy.version === 4).reuse).to.equal(
+ true,
+ );
+ expect(errors).to.have.length(2);
+ expect(errors[0]).to.include(
+ 'Platform sing-box does not support snell version 6',
+ );
+ expect(errors[1]).to.include(
+ 'Platform sing-box does not support snell version 4x',
+ );
+ });
+
+ it('exports parsed Surge Snell lines to sing-box Snell outbounds', function () {
+ const [proxy] = ProxyUtils.parse(
+ 'Surge Snell = snell,surge-snell.example.com,443,psk=secret,version=5,obfs=http,obfs-host=obfs.example.com,obfs-uri=/snell,reuse=true,tfo=true,udp-relay=false,underlying-proxy=proxy-out',
+ );
+
+ const output = loadProducedJson('sing-box', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expectSubset(output.outbounds[0], {
+ tag: 'Surge Snell',
+ type: 'snell',
+ server: 'surge-snell.example.com',
+ server_port: 443,
+ psk: 'secret',
+ version: 5,
+ reuse: true,
+ network: 'tcp',
+ obfs_mode: 'http',
+ obfs_host: 'obfs.example.com',
+ tcp_fast_open: true,
+ detour: 'proxy-out',
+ });
+ expect(output.outbounds[0]).to.not.have.property('obfs_uri');
+ });
+
+ it('exports Snell shadow-tls field form to sing-box chained outbounds', function () {
+ const output = loadProducedJson(
+ 'sing-box',
+ {
+ type: 'snell',
+ name: 'sing-box Snell ShadowTLS',
+ server: 'snell.example.com',
+ port: 44046,
+ psk: 'secret',
+ version: 4,
+ udp: false,
+ tfo: true,
+ 'fast-open': true,
+ reuse: true,
+ 'dialer-proxy': 'proxy-out',
+ 'ip-version': 'v6-only',
+ _dns_server: 'dns-out',
+ 'shadow-tls-password': 'shadow-pass',
+ 'shadow-tls-sni': 'mask.example.com',
+ 'shadow-tls-version': 3,
+ },
+ { 'include-unsupported-proxy': true },
+ );
+
+ expect(output.outbounds).to.have.length(2);
+ expectSubset(output.outbounds[0], {
+ tag: 'sing-box Snell ShadowTLS',
+ type: 'snell',
+ psk: 'secret',
+ version: 4,
+ reuse: true,
+ network: 'tcp',
+ detour: 'sing-box Snell ShadowTLS_shadowtls',
+ });
+ expect(output.outbounds[0]).to.not.have.property('server');
+ expect(output.outbounds[0]).to.not.have.property('server_port');
+ expect(output.outbounds[0]).to.not.have.property('tcp_fast_open');
+ expectSubset(output.outbounds[1], {
+ tag: 'sing-box Snell ShadowTLS_shadowtls',
+ type: 'shadowtls',
+ server: 'snell.example.com',
+ server_port: 44046,
+ version: 3,
+ password: 'shadow-pass',
+ udp_fragment: true,
+ tcp_fast_open: true,
+ detour: 'proxy-out',
+ tls: {
+ enabled: true,
+ server_name: 'mask.example.com',
+ utls: {
+ enabled: true,
+ },
+ },
+ domain_resolver: {
+ server: 'dns-out',
+ strategy: 'ipv6_only',
+ },
+ });
+ });
+
+ it('exports parsed Surge Snell shadow-tls lines to sing-box chained outbounds', function () {
+ const [proxy] = ProxyUtils.parse(
+ 'Surge Snell ShadowTLS = snell,surge-snell.example.com,443,psk=secret,version=5,reuse=true,tfo=true,udp-relay=false,underlying-proxy=proxy-out,shadow-tls-password=shadow-pass,shadow-tls-sni=mask.example.com,shadow-tls-version=3',
+ );
+
+ const output = loadProducedJson('sing-box', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expectSubset(output.outbounds[0], {
+ tag: 'Surge Snell ShadowTLS',
+ type: 'snell',
+ psk: 'secret',
+ version: 5,
+ reuse: true,
+ network: 'tcp',
+ detour: 'Surge Snell ShadowTLS_shadowtls',
+ });
+ expectSubset(output.outbounds[1], {
+ tag: 'Surge Snell ShadowTLS_shadowtls',
+ type: 'shadowtls',
+ server: 'surge-snell.example.com',
+ server_port: 443,
+ version: 3,
+ password: 'shadow-pass',
+ detour: 'proxy-out',
+ tls: {
+ server_name: 'mask.example.com',
+ },
+ });
+ });
+
+ it('exports Mihomo-style Snell shadow-tls plugin objects to sing-box chained outbounds', function () {
+ const [proxy] = ProxyUtils.parse(`proxies:
+ - name: Mihomo Snell ShadowTLS
+ type: snell
+ server: mihomo-snell.example.com
+ port: 443
+ psk: secret
+ version: 4
+ udp: false
+ plugin: shadow-tls
+ plugin-opts:
+ host: mask.example.com
+ password: shadow-pass
+ version: 2
+ alpn:
+ - h2
+ - http/1.1`);
+
+ const output = loadProducedJson('sing-box', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expectSubset(output.outbounds[0], {
+ tag: 'Mihomo Snell ShadowTLS',
+ type: 'snell',
+ psk: 'secret',
+ version: 4,
+ network: 'tcp',
+ detour: 'Mihomo Snell ShadowTLS_shadowtls',
+ });
+ expectSubset(output.outbounds[1], {
+ tag: 'Mihomo Snell ShadowTLS_shadowtls',
+ type: 'shadowtls',
+ server: 'mihomo-snell.example.com',
+ server_port: 443,
+ version: 2,
+ password: 'shadow-pass',
+ tls: {
+ server_name: 'mask.example.com',
+ alpn: ['h2', 'http/1.1'],
+ },
+ });
+ });
+
+ it('exports Shadowsocks shadow-tls plugin ALPN to sing-box shadowtls tls options', function () {
+ const [proxy] = ProxyUtils.parse(`proxies:
+ - name: SS ShadowTLS ALPN
+ type: ss
+ server: ss.example.com
+ port: 443
+ cipher: chacha20-ietf-poly1305
+ password: password
+ plugin: shadow-tls
+ client-fingerprint: chrome
+ plugin-opts:
+ host: cloud.tencent.com
+ password: shadow_tls_password
+ version: 2
+ alpn:
+ - h2
+ - http/1.1`);
+
+ const output = loadProducedJson('sing-box', proxy);
+
+ expect(output.outbounds).to.have.length(2);
+ expectSubset(output.outbounds[0], {
+ tag: 'SS ShadowTLS ALPN',
+ type: 'shadowsocks',
+ method: 'chacha20-ietf-poly1305',
+ password: 'password',
+ detour: 'SS ShadowTLS ALPN_shadowtls',
+ });
+ expectSubset(output.outbounds[1], {
+ tag: 'SS ShadowTLS ALPN_shadowtls',
+ type: 'shadowtls',
+ server: 'ss.example.com',
+ server_port: 443,
+ version: 2,
+ password: 'shadow_tls_password',
+ tls: {
+ enabled: true,
+ server_name: 'cloud.tencent.com',
+ alpn: ['h2', 'http/1.1'],
+ utls: {
+ enabled: true,
+ fingerprint: 'chrome',
+ },
+ },
+ });
+ });
+
+ it('keeps only Shadowsocks shadow-tls versions 1 through 3 for Mihomo', function () {
+ const buildShadowTlsProxy = (name, version) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ 'shadow-tls-password': 'shadow-pass',
+ 'shadow-tls-sni': 'mask.example.com',
+ 'shadow-tls-version': version,
+ });
+ const proxies = [
+ buildShadowTlsProxy('ShadowTLS 1', 1),
+ {
+ type: 'ss',
+ name: 'ShadowTLS 2',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'shadow-tls',
+ 'plugin-opts': {
+ host: 'mask.example.com',
+ password: 'shadow-pass',
+ version: 2,
+ },
+ },
+ buildShadowTlsProxy('ShadowTLS 3', 3),
+ buildShadowTlsProxy('ShadowTLS 4', 4),
+ {
+ type: 'vmess',
+ name: 'VMess ShadowTLS',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ plugin: 'shadow-tls',
+ 'plugin-opts': {
+ host: 'mask.example.com',
+ password: 'shadow-pass',
+ version: 3,
+ },
+ },
+ ];
+
+ const internal = produceInternal('Mihomo', proxies);
+ const external = loadProducedYaml('Mihomo', proxies);
+
+ expect(internal.map((proxy) => proxy.name)).to.deep.equal([
+ 'ShadowTLS 1',
+ 'ShadowTLS 2',
+ 'ShadowTLS 3',
+ ]);
+ expect(
+ internal.map((proxy) => proxy['plugin-opts'].version),
+ ).to.deep.equal([1, 2, 3]);
+ expect(external.proxies.map((proxy) => proxy.name)).to.deep.equal([
+ 'ShadowTLS 1',
+ 'ShadowTLS 2',
+ 'ShadowTLS 3',
+ ]);
+ });
+
+ it('keeps only supported shadowsocks v2ray-plugin modes for Shadowrocket by default', function () {
+ const buildProxy = (name, mode) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode,
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ },
+ });
+
+ const proxies = [
+ buildProxy('WS', 'websocket'),
+ buildProxy('QUIC', 'quic'),
+ buildProxy('HTTP2', 'http2'),
+ buildProxy('MKCP', 'mkcp'),
+ buildProxy('GRPC', 'grpc'),
+ buildProxy('TLS', 'tls'),
+ ];
+
+ const internal = produceInternal('Shadowrocket', proxies);
+ const external = loadProducedYaml('Shadowrocket', proxies, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expect(internal.map((proxy) => proxy.name)).to.deep.equal([
+ 'WS',
+ 'QUIC',
+ 'HTTP2',
+ 'MKCP',
+ 'GRPC',
+ ]);
+ expect(external.proxies.map((proxy) => proxy.name)).to.deep.equal([
+ 'WS',
+ 'QUIC',
+ 'HTTP2',
+ 'MKCP',
+ 'GRPC',
+ 'TLS',
+ ]);
+ });
+
+ it('adds Clash.Meta reality defaults and preserves websocket early data', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ };
+
+ const internal = produceInternal('ClashMeta', proxy)[0];
+ const external = loadProducedYaml('ClashMeta', proxy);
+
+ expectSubset(internal, {
+ type: 'vless',
+ name: 'Reality',
+ 'client-fingerprint': 'chrome',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ 'max-early-data': 2048,
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'Reality',
+ 'client-fingerprint': 'chrome',
+ });
+ });
+
+ it('normalizes websocket early data query params for Stash and Shadowrocket', function () {
+ const proxy = {
+ type: 'vmess',
+ name: 'WS Early Query',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ };
+
+ for (const platform of ['Stash', 'Shadowrocket']) {
+ const internal = produceInternal(platform, proxy)[0];
+ expectSubset(internal, {
+ name: 'WS Early Query',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ 'max-early-data': 2048,
+ },
+ });
+ }
+ });
+
+ it('leaves unsafe websocket early data query params untouched', function () {
+ const proxy = {
+ type: 'vmess',
+ name: 'WS Unsafe Early Query',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=999999999999999999999&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ };
+
+ const wsOpts = produceInternal('Clash', proxy)[0]['ws-opts'];
+
+ expect(wsOpts.path).to.equal('/ws?a=1&ed=999999999999999999999&b=2');
+ expect(wsOpts).to.not.have.property('early-data-header-name');
+ expect(wsOpts).to.not.have.property('max-early-data');
+ });
+
+ it('keeps explicit websocket early data fields over stale path query values', function () {
+ const proxy = {
+ type: 'vmess',
+ name: 'WS Explicit Early Fields',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 4096,
+ 'early-data-header-name': 'X-Data',
+ },
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Stash',
+ 'Shadowrocket',
+ ]) {
+ const wsOpts = loadProducedYaml(platform, proxy).proxies[0][
+ 'ws-opts'
+ ];
+
+ expect(wsOpts.path, platform).to.equal('/ws?a=1&b=2');
+ expect(wsOpts['max-early-data'], platform).to.equal(4096);
+ expect(wsOpts['early-data-header-name'], platform).to.equal(
+ 'X-Data',
+ );
+ }
+ });
+
+ it('normalizes httpupgrade path early data as httpupgrade metadata', function () {
+ const proxy = {
+ type: 'vmess',
+ name: 'HTTPUpgrade Early Query',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ },
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Stash',
+ 'Shadowrocket',
+ ]) {
+ const wsOpts = produceInternal(platform, proxy, {
+ 'include-unsupported-proxy': true,
+ })[0]['ws-opts'];
+
+ expect(wsOpts.path).to.equal('/upgrade?a=1&b=2');
+ expect(wsOpts['v2ray-http-upgrade']).to.equal(true);
+ expect(wsOpts['v2ray-http-upgrade-fast-open']).to.equal(true);
+ expect(wsOpts['_v2ray-http-upgrade-ed']).to.equal('2048');
+ expect(wsOpts).to.not.have.property('early-data-header-name');
+ expect(wsOpts).to.not.have.property('max-early-data');
+
+ const externalWsOpts = loadProducedYaml(platform, proxy, {
+ 'include-unsupported-proxy': true,
+ }).proxies[0]['ws-opts'];
+ expect(externalWsOpts.path).to.equal('/upgrade?a=1&b=2');
+ expect(externalWsOpts['v2ray-http-upgrade-fast-open']).to.equal(
+ true,
+ );
+ expect(externalWsOpts).to.not.have.property(
+ '_v2ray-http-upgrade-ed',
+ );
+ }
+ });
+
+ it('keeps explicit httpupgrade early data over stale path query values', function () {
+ const proxy = {
+ type: 'vmess',
+ name: 'HTTPUpgrade Explicit Early',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '4096',
+ },
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Stash',
+ 'Shadowrocket',
+ ]) {
+ const wsOpts = produceInternal(platform, proxy, {
+ 'include-unsupported-proxy': true,
+ })[0]['ws-opts'];
+
+ expect(wsOpts.path, platform).to.equal('/upgrade?a=1&b=2');
+ expect(wsOpts['_v2ray-http-upgrade-ed'], platform).to.equal('4096');
+ }
+ });
+
+ it('does not emit websocket early data fields for httpupgrade transports', function () {
+ const directProxy = {
+ type: 'vmess',
+ name: 'HTTPUpgrade Legacy Fields',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ 'max-early-data': 1024,
+ 'early-data-header-name': 'X-Upgrade',
+ },
+ };
+ const directProxyWithoutPath = {
+ type: 'vmess',
+ name: 'HTTPUpgrade Legacy Fields Without Path',
+ server: 'vmess.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ network: 'ws',
+ 'ws-opts': {
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Upgrade',
+ },
+ };
+ const parsedVlessProxy = ProxyUtils.parse(
+ `vless://${UUID}@vless-upgrade.example.com:443?type=httpupgrade&host=upgrade.example.com&path=%2Fupgrade&ed=1024&eh=X-Upgrade#VLESS%20Upgrade`,
+ )[0];
+
+ for (const proxy of [
+ directProxy,
+ directProxyWithoutPath,
+ parsedVlessProxy,
+ ]) {
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Stash',
+ 'Shadowrocket',
+ ]) {
+ const externalWsOpts = loadProducedYaml(platform, proxy, {
+ 'include-unsupported-proxy': true,
+ }).proxies[0]['ws-opts'];
+
+ expect(externalWsOpts['v2ray-http-upgrade']).to.equal(true);
+ expect(externalWsOpts).to.not.have.property(
+ 'early-data-header-name',
+ );
+ expect(externalWsOpts).to.not.have.property('max-early-data');
+ expect(externalWsOpts).to.not.have.property(
+ '_v2ray-http-upgrade-ed',
+ );
+ }
+ }
+ });
+
+ it('emits Mihomo VLESS xhttp proxies and preserves xhttp options', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-up',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-min-posts-interval-ms': 300,
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ const external = loadProducedYaml('Mihomo', proxy);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'XHTTP',
+ network: 'xhttp',
+ servername: 'sni.example.com',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-up',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-min-posts-interval-ms': 300,
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'XHTTP',
+ network: 'xhttp',
+ servername: 'sni.example.com',
+ 'xhttp-opts': {
+ 'sc-min-posts-interval-ms': 300,
+ },
+ });
+ });
+
+ it('warns when Mihomo ECH sidecar DNS fields are exported', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'Mihomo ECH DNS',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'ech.example.com',
+ },
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://dns.example.com/dns-query',
+ 'query-server-name': 'download-ech.example.com',
+ },
+ },
+ },
+ };
+
+ const { result: external, warnings } = captureWarns(() =>
+ loadProducedYaml('Mihomo', proxy),
+ );
+
+ expect(external.proxies).to.have.length(1);
+ expect(warnings).to.have.length(2);
+ expect(warnings[0]).to.include(
+ 'mihomo 不支持在 ech-opts 中配置 ECH DNS',
+ );
+ expect(warnings[0]).to.include(
+ 'dns["nameserver-policy"]["ech.example.com"] = ["https://1.1.1.1/dns-query"]',
+ );
+ expect(warnings[1]).to.include(
+ 'dns["nameserver-policy"]["download-ech.example.com"] = ["https://dns.example.com/dns-query"]',
+ );
+ });
+
+ it('emits Mihomo VLESS xhttp download settings with scMinPostsIntervalMs', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Download',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '8',
+ 'h-max-reusable-secs': '900',
+ },
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ const external = loadProducedYaml('Mihomo', proxy);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'XHTTP Download',
+ network: 'xhttp',
+ servername: 'sni.example.com',
+ 'xhttp-opts': {
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '8',
+ 'h-max-reusable-secs': '900',
+ },
+ },
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'XHTTP Download',
+ network: 'xhttp',
+ servername: 'sni.example.com',
+ 'xhttp-opts': {
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '8',
+ 'h-max-reusable-secs': '900',
+ },
+ },
+ },
+ });
+ });
+
+ it('skips Mihomo VLESS xhttp stream-one proxies when download-settings are present', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Stream One Download',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-one',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ const external = ProxyUtils.produce([proxy], 'Mihomo', 'external');
+
+ expect(internal).to.have.length(0);
+ expect(external.trim()).to.equal('proxies:');
+ });
+
+ it('keeps VLESS xhttp proxy when download-settings reality-opts has empty public-key and main reality is valid', function () {
+ // 上行(主代理)有合法 reality-opts,下行(download-settings)用
+ // reality-opts: { public-key: '' } 取消继承上行 Reality,合法配置。
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Reality Cancel',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'reality-opts': { 'public-key': '' },
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ expect(internal).to.have.length(1);
+ expect(internal[0].name).to.equal('XHTTP Reality Cancel');
+ });
+
+ it('filters out VLESS xhttp proxy when download-settings reality-opts is empty object (broken URI) even with valid main reality', function () {
+ // 下行 download-settings.reality-opts 为 {} (没有 public-key 字段),
+ // 代表 URI 里声明了 security=reality 但 pbk 缺失,属于残缺配置,应过滤。
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Broken Reality',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'reality-opts': {},
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ expect(internal).to.have.length(0);
+ });
+
+ it('filters out VLESS xhttp proxy when download-settings reality-opts has empty public-key and main has no valid reality', function () {
+ // 下行 download-settings 有 reality-opts: { public-key: '' },
+ // 但上行主代理本身没有合法 Reality,属于无效配置,应被过滤。
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Invalid Reality',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'reality-opts': { 'public-key': '' },
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ expect(internal).to.have.length(0);
+ });
+
+ it('filters out VLESS proxy when main reality-opts has empty public-key', function () {
+ // 主代理 reality-opts.public-key 为空,无论下行如何,都应被过滤。
+ const proxy = {
+ type: 'vless',
+ name: 'Reality Empty Key',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ 'reality-opts': { 'public-key': '' },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ expect(internal).to.have.length(0);
+ });
+
+ it('does not let include-unsupported-proxy bypass malformed VLESS Reality validation', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'Reality Empty Key',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ 'reality-opts': { 'public-key': '' },
+ };
+
+ const { result, errors } = captureErrors(() =>
+ produceInternal('Mihomo', proxy, {
+ 'include-unsupported-proxy': true,
+ }),
+ );
+
+ expect(result).to.have.length(0);
+ expect(errors).to.have.length(1);
+ expect(errors[0]).to.include(
+ 'Skipping VLESS Reality proxy Reality Empty Key: empty reality-opts.public-key',
+ );
+ });
+
+ it('normalizes Stash TUIC defaults and external yaml wrapper', function () {
+ const proxy = {
+ type: 'tuic',
+ name: 'TUIC',
+ server: 'tuic.example.com',
+ port: 443,
+ uuid: UUID,
+ password: 'secret',
+ tfo: true,
+ };
+
+ const internal = produceInternal('Stash', proxy)[0];
+ const external = loadProducedYaml('Stash', proxy);
+
+ expectSubset(internal, {
+ type: 'tuic',
+ name: 'TUIC',
+ alpn: ['h3'],
+ 'fast-open': true,
+ version: 5,
+ });
+ expectSubset(external.proxies[0], {
+ type: 'tuic',
+ name: 'TUIC',
+ alpn: ['h3'],
+ 'fast-open': true,
+ version: 5,
+ });
+ });
+
+ it('emits Stash VLESS TCP REALITY proxies without validating flow values', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'Stash Reality Custom Flow',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ flow: 'xtls-rprx-unknown',
+ sni: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ };
+
+ const internal = produceInternal('Stash', proxy);
+ const external = loadProducedYaml('Stash', proxy);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'Stash Reality Custom Flow',
+ network: 'tcp',
+ flow: 'xtls-rprx-unknown',
+ servername: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'Stash Reality Custom Flow',
+ network: 'tcp',
+ flow: 'xtls-rprx-unknown',
+ servername: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ });
+
+ it('keeps default-tcp Stash VLESS REALITY proxies when network is omitted', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'Implicit TCP Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ };
+
+ const internal = produceInternal('Stash', proxy);
+ const external = loadProducedYaml('Stash', proxy);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'Implicit TCP Reality',
+ servername: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ expect(external.proxies[0]).to.not.have.property('network');
+ });
+
+ it('keeps Stash VLESS TCP REALITY proxies when URI input omits flow', function () {
+ const proxies = ProxyUtils.parse(
+ `vless://${UUID}@vless.example.com:443?type=tcp&security=reality&sni=sni.example.com&pbk=pubkey&sid=08#No%20Flow`,
+ );
+
+ const internal = produceInternal('Stash', proxies);
+ const external = loadProducedYaml('Stash', proxies);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expect(internal[0]).to.not.have.property('flow');
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'No Flow',
+ network: 'tcp',
+ servername: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'No Flow',
+ network: 'tcp',
+ servername: 'sni.example.com',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+ });
+
+ it('keeps Stash VLESS TCP REALITY nodes while still filtering non-tcp and unsupported variants', function () {
+ const proxies = [
+ {
+ type: 'vless',
+ name: 'Supported Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ {
+ type: 'vless',
+ name: 'Custom Flow',
+ server: 'unsupported-flow.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ flow: 'xtls-rprx-unknown',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ {
+ type: 'vless',
+ name: 'Reality WS',
+ server: 'vless-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ {
+ type: 'vless',
+ name: 'XHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ {
+ type: 'vless',
+ name: 'Encrypted VLESS',
+ server: 'encrypted.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'tcp',
+ encryption: 'aes-128-gcm',
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ },
+ ];
+
+ const internal = produceInternal('Stash', proxies);
+ const external = loadProducedYaml('Stash', proxies);
+
+ expect(internal).to.have.length(2);
+ expect(external.proxies).to.have.length(2);
+ expect(internal.map((proxy) => proxy.name)).to.deep.equal([
+ 'Supported Reality',
+ 'Custom Flow',
+ ]);
+ expect(external.proxies.map((proxy) => proxy.name)).to.deep.equal([
+ 'Supported Reality',
+ 'Custom Flow',
+ ]);
+ expectSubset(internal[0], {
+ type: 'vless',
+ name: 'Supported Reality',
+ });
+ expectSubset(internal[1], {
+ type: 'vless',
+ name: 'Custom Flow',
+ flow: 'xtls-rprx-unknown',
+ });
+ expectSubset(external.proxies[0], {
+ type: 'vless',
+ name: 'Supported Reality',
+ });
+ expectSubset(external.proxies[1], {
+ type: 'vless',
+ name: 'Custom Flow',
+ flow: 'xtls-rprx-unknown',
+ });
+ });
+
+ it('promotes shadow-tls fields for Shadowrocket', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'ShadowTLS SS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ 'shadow-tls-password': 'shadow-pass',
+ 'shadow-tls-sni': 'mask.example.com',
+ 'shadow-tls-version': 3,
+ 'skip-cert-verify': true,
+ };
+
+ const internal = produceInternal('Shadowrocket', proxy)[0];
+ const external = loadProducedYaml('Shadowrocket', proxy);
+
+ expectSubset(internal, {
+ type: 'ss',
+ name: 'ShadowTLS SS',
+ plugin: 'shadow-tls',
+ 'plugin-opts': {
+ host: 'mask.example.com',
+ password: 'shadow-pass',
+ version: 3,
+ },
+ });
+ expectSubset(external.proxies[0], {
+ type: 'ss',
+ plugin: 'shadow-tls',
+ });
+ });
+
+ it('maps canonical shadowsocks tls fields into Shadowrocket structured output', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ 'skip-cert-verify': true,
+ };
+
+ const internal = produceInternal('Shadowrocket', proxy)[0];
+ const external = loadProducedYaml('Shadowrocket', proxy);
+
+ expectSubset(internal, {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS',
+ tls: true,
+ servername: 'a.com',
+ 'skip-cert-verify': true,
+ });
+ // expect(internal).to.not.have.property('sni');
+ expectSubset(external.proxies[0], {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS',
+ tls: true,
+ servername: 'a.com',
+ 'skip-cert-verify': true,
+ });
+ // expect(external.proxies[0]).to.not.have.property('sni');
+ });
+
+ it('keeps canonical shadowsocks tls nodes without servername in Shadowrocket output when sni is absent', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS No Host',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ };
+
+ const internal = produceInternal('Shadowrocket', proxy)[0];
+ const external = loadProducedYaml('Shadowrocket', proxy);
+
+ expectSubset(internal, {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS No Host',
+ tls: true,
+ });
+ expect(internal).to.not.have.property('sni');
+ expect(internal).to.not.have.property('servername');
+ expectSubset(external.proxies[0], {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS No Host',
+ tls: true,
+ });
+ expect(external.proxies[0]).to.not.have.property('sni');
+ expect(external.proxies[0]).to.not.have.property('servername');
+ });
+
+ it('maps canonical shadowsocks tcp tls fields into Shadowrocket structured output', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS TCP',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ network: 'tcp',
+ };
+
+ const internal = produceInternal('Shadowrocket', proxy)[0];
+ const external = loadProducedYaml('Shadowrocket', proxy);
+
+ expectSubset(internal, {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS TCP',
+ tls: true,
+ network: 'tcp',
+ servername: 'a.com',
+ });
+ // expect(internal).to.not.have.property('sni');
+ expectSubset(external.proxies[0], {
+ type: 'ss',
+ name: 'Shadowrocket SS TLS TCP',
+ tls: true,
+ network: 'tcp',
+ servername: 'a.com',
+ });
+ // expect(external.proxies[0]).to.not.have.property('sni');
+ });
+
+ it('maps shadowsocks shadow-tls fields into Egern nested structures', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'ShadowTLS SS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ 'shadow-tls-password': 'shadow-pass',
+ 'shadow-tls-sni': 'mask.example.com',
+ 'shadow-tls-version': 3,
+ };
+
+ const internal = produceInternal('Egern', proxy)[0];
+ const external = loadProducedYaml('Egern', proxy);
+
+ expectSubset(internal, {
+ shadowsocks: {
+ name: 'ShadowTLS SS',
+ method: 'aes-128-gcm',
+ server: 'ss.example.com',
+ port: 8388,
+ password: 'secret',
+ shadow_tls: {
+ password: 'shadow-pass',
+ sni: 'mask.example.com',
+ },
+ },
+ });
+ expectSubset(external.proxies[0], {
+ shadowsocks: {
+ name: 'ShadowTLS SS',
+ },
+ });
+ });
+
+ it('emits Egern HTTP and HTTPS root headers', function () {
+ const proxies = [
+ {
+ type: 'http',
+ name: 'Egern HTTP Headers',
+ server: 'http.example.com',
+ port: 8080,
+ username: 'user',
+ password: 'pass',
+ headers: {
+ 'X-Client': 'Egern',
+ 'X-Token': 'abc',
+ },
+ },
+ {
+ type: 'http',
+ name: 'Egern HTTPS Headers',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ sni: 'sni.example.com',
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ ];
+
+ const internal = produceInternal('Egern', proxies);
+ const external = loadProducedYaml('Egern', proxies);
+
+ expectSubset(internal[0], {
+ http: {
+ name: 'Egern HTTP Headers',
+ headers: {
+ 'X-Client': 'Egern',
+ 'X-Token': 'abc',
+ },
+ },
+ });
+ expectSubset(internal[1], {
+ https: {
+ name: 'Egern HTTPS Headers',
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ });
+ expectSubset(external.proxies[0], {
+ http: {
+ headers: {
+ 'X-Client': 'Egern',
+ 'X-Token': 'abc',
+ },
+ },
+ });
+ expectSubset(external.proxies[1], {
+ https: {
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ });
+ });
+
+ it('emits Egern Hysteria2 upload bandwidth as bandwidth', function () {
+ const proxy = {
+ type: 'hysteria2',
+ name: 'Egern Hysteria2 Bandwidth',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ up: '50 Mbps',
+ sni: 'peer.example.com',
+ };
+
+ const internal = produceInternal('Egern', proxy)[0];
+ const external = loadProducedYaml('Egern', proxy);
+
+ expectSubset(internal, {
+ hysteria2: {
+ name: 'Egern Hysteria2 Bandwidth',
+ bandwidth: 50,
+ },
+ });
+ expectSubset(external.proxies[0], {
+ hysteria2: {
+ name: 'Egern Hysteria2 Bandwidth',
+ bandwidth: 50,
+ },
+ });
+ });
+
+ it('keeps Mihomo HTTP headers and filters unsupported h2-connect/trusttunnel header variants', function () {
+ const { result, errors } = captureErrors(() =>
+ produceInternal('Mihomo', [
+ {
+ type: 'http',
+ name: 'Mihomo HTTPS Headers',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Token': 'abc',
+ },
+ },
+ {
+ type: 'h2-connect',
+ name: 'Mihomo H2 Headers',
+ server: 'h2.example.com',
+ port: 443,
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ {
+ type: 'trusttunnel',
+ name: 'Mihomo Trust Headers',
+ server: 'trust.example.com',
+ port: 443,
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ },
+ ]),
+ );
+
+ expect(result).to.have.length(1);
+ expectSubset(result[0], {
+ type: 'http',
+ name: 'Mihomo HTTPS Headers',
+ headers: {
+ 'X-Token': 'abc',
+ },
+ });
+ expect(errors).to.have.length(2);
+ expect(errors[0]).to.include(
+ 'Target platform Mihomo does not support headers for HTTP/2 CONNECT proxy Mihomo H2 Headers',
+ );
+ expect(errors[1]).to.include(
+ 'Target platform Mihomo does not support headers for TrustTunnel proxy Mihomo Trust Headers',
+ );
+ });
+
+ it('keeps Mihomo h2-connect/trusttunnel header variants when include-unsupported-proxy is enabled', function () {
+ const { result, errors } = captureErrors(() =>
+ produceInternal(
+ 'Mihomo',
+ [
+ {
+ type: 'h2-connect',
+ name: 'Mihomo H2 Headers',
+ server: 'h2.example.com',
+ port: 443,
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ {
+ type: 'trusttunnel',
+ name: 'Mihomo Trust Headers',
+ server: 'trust.example.com',
+ port: 443,
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ },
+ ],
+ { 'include-unsupported-proxy': true },
+ ),
+ );
+
+ expect(result).to.have.length(2);
+ expectSubset(result[0], {
+ type: 'h2-connect',
+ name: 'Mihomo H2 Headers',
+ headers: {
+ 'X-Padding': '',
+ },
+ });
+ expectSubset(result[1], {
+ type: 'trusttunnel',
+ name: 'Mihomo Trust Headers',
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ });
+ expect(errors).to.have.length(0);
+ });
+
+ it('preserves supported HTTP root headers for sing-box and JSON outputs', function () {
+ const buildProxy = (name) => ({
+ type: 'http',
+ name,
+ server: 'http.example.com',
+ port: 8080,
+ username: 'user',
+ password: 'pass',
+ headers: {
+ 'X-Token': 'abc',
+ },
+ });
+
+ const singBoxInternal = produceInternal(
+ 'sing-box',
+ buildProxy('sing-box HTTP Headers'),
+ );
+ const singBoxExternal = loadProducedJson(
+ 'sing-box',
+ buildProxy('sing-box HTTP Headers'),
+ );
+ const jsonExternal = loadProducedJson(
+ 'JSON',
+ buildProxy('JSON HTTP Headers'),
+ );
+
+ expect(singBoxInternal).to.have.length(1);
+ expectSubset(singBoxInternal[0], {
+ type: 'http',
+ tag: 'sing-box HTTP Headers',
+ headers: {
+ 'X-Token': 'abc',
+ },
+ });
+ expectSubset(singBoxExternal.outbounds[0], {
+ type: 'http',
+ tag: 'sing-box HTTP Headers',
+ headers: {
+ 'X-Token': 'abc',
+ },
+ });
+ expectSubset(jsonExternal[0], {
+ type: 'http',
+ name: 'JSON HTTP Headers',
+ headers: {
+ 'X-Token': 'abc',
+ },
+ });
+ });
+
+ it('maps shadowsocks obfs plugin fields into Egern nested structures', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Egern SS Obfs TLS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'tls',
+ host: 'legacy.example.com',
+ path: '/legacy',
+ },
+ };
+
+ const internal = produceInternal('Egern', proxy)[0];
+ const external = loadProducedYaml('Egern', proxy);
+
+ expectSubset(internal, {
+ shadowsocks: {
+ name: 'Egern SS Obfs TLS',
+ method: 'aes-128-gcm',
+ server: 'ss.example.com',
+ port: 8388,
+ password: 'secret',
+ obfs: 'tls',
+ obfs_host: 'legacy.example.com',
+ obfs_uri: '/legacy',
+ },
+ });
+ expectSubset(external.proxies[0], {
+ shadowsocks: {
+ name: 'Egern SS Obfs TLS',
+ obfs: 'tls',
+ obfs_host: 'legacy.example.com',
+ obfs_uri: '/legacy',
+ },
+ });
+ });
+
+ it('preserves numeric v2ray-plugin mux values across non-Mihomo Clash-family YAML producers', function () {
+ const buildProxy = (name, mux) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ 'skip-cert-verify': false,
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ 'skip-cert-verify': true,
+ mux,
+ },
+ });
+
+ for (const platform of ['Clash', 'Shadowrocket', 'Stash']) {
+ const internal = produceInternal(platform, [
+ buildProxy(`${platform} Mux On`, 1),
+ buildProxy(`${platform} Mux Off`, 0),
+ ]);
+
+ expect(internal, platform).to.have.length(2);
+ expectSubset(internal[0], {
+ name: `${platform} Mux On`,
+ 'plugin-opts': {
+ tls: true,
+ 'skip-cert-verify': true,
+ mux: 1,
+ },
+ });
+ expectSubset(internal[1], {
+ name: `${platform} Mux Off`,
+ 'plugin-opts': {
+ tls: true,
+ 'skip-cert-verify': true,
+ mux: 0,
+ },
+ });
+ }
+ });
+
+ it('normalizes plugin mux values to booleans for Mihomo-compatible YAML producers', function () {
+ const buildProxy = (name, mux) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ mux,
+ },
+ });
+ const cases = [
+ ['Number On', 1, true],
+ ['Number Off', 0, false],
+ ['Boolean On', true, true],
+ ['Boolean Off', false, false],
+ ['String True', ' TRUE ', true],
+ ['String False', ' false ', false],
+ ['String One', '1', true],
+ ['String Zero', '0', false],
+ ];
+ const proxies = cases.map(([name, mux]) =>
+ buildProxy(`Mihomo ${name}`, mux),
+ );
+
+ for (const platform of ['Mihomo', 'ClashMeta']) {
+ const internal = produceInternal(platform, proxies);
+ const external = loadProducedYaml(platform, proxies);
+
+ expect(internal, platform).to.have.length(cases.length);
+ expect(external.proxies, platform).to.have.length(cases.length);
+
+ cases.forEach(([name, , expected], index) => {
+ expect(
+ internal[index]['plugin-opts'].mux,
+ `${platform} internal ${name}`,
+ ).to.equal(expected);
+ expect(
+ external.proxies[index]['plugin-opts'].mux,
+ `${platform} external ${name}`,
+ ).to.equal(expected);
+ });
+ }
+ });
+
+ it('preserves Mihomo shadowsocks gost-plugin options with boolean mux', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Mihomo Gost Plugin',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'gost-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ tls: true,
+ fingerprint: 'SHA256:TEST',
+ certificate: 'inline-test-client-cert',
+ 'private-key': 'inline-test-client-key',
+ 'skip-cert-verify': true,
+ host: 'cdn.example.com',
+ path: '/socket',
+ mux: 1,
+ headers: {
+ custom: 'value',
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ const external = loadProducedYaml('Mihomo', proxy);
+ const expected = {
+ type: 'ss',
+ name: 'Mihomo Gost Plugin',
+ plugin: 'gost-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ tls: true,
+ fingerprint: 'SHA256:TEST',
+ certificate: 'inline-test-client-cert',
+ 'private-key': 'inline-test-client-key',
+ 'skip-cert-verify': true,
+ host: 'cdn.example.com',
+ path: '/socket',
+ mux: true,
+ headers: {
+ custom: 'value',
+ },
+ },
+ };
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], expected);
+ expectSubset(external.proxies[0], expected);
+ });
+
+ it('keeps legacy single-line proxy output by default for Clash-style YAML producers', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Legacy YAML',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Shadowrocket',
+ 'Stash',
+ 'Egern',
+ ]) {
+ const output = produceExternal(platform, proxy);
+
+ expect(output, platform).to.match(/\n {2}- \{/);
+ }
+ });
+
+ it('emits pretty yaml for Clash-style YAML producers when prettyYaml is enabled', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Real YAML',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Shadowrocket',
+ 'Stash',
+ 'Egern',
+ ]) {
+ const output = produceExternal(platform, proxy, {
+ prettyYaml: true,
+ });
+ const external = loadProducedYaml(platform, proxy, {
+ prettyYaml: true,
+ });
+
+ expect(output, platform).to.not.match(/\n {2}- \{/);
+ expect(external.proxies, platform).to.have.length(1);
+
+ if (platform === 'Egern') {
+ expectSubset(external.proxies[0], {
+ shadowsocks: {
+ name: 'Real YAML',
+ },
+ });
+ } else {
+ expectSubset(external.proxies[0], {
+ type: 'ss',
+ name: 'Real YAML',
+ });
+ }
+ }
+ });
+
+ it('emits WireGuard interface CIDR suffixes for Mihomo and Shadowrocket outputs', function () {
+ const proxies = [
+ {
+ type: 'wireguard',
+ name: 'WG Explicit CIDR',
+ server: 'wg-explicit.example.com',
+ port: 51820,
+ 'private-key': 'private-key-1',
+ 'public-key': 'public-key-1',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ },
+ {
+ type: 'wireguard',
+ name: 'WG Default CIDR',
+ server: 'wg-default.example.com',
+ port: 51820,
+ 'private-key': 'private-key-2',
+ 'public-key': 'public-key-2',
+ ip: '10.0.0.3',
+ ipv6: 'fd00::3',
+ },
+ ];
+
+ for (const platform of ['Mihomo', 'Shadowrocket']) {
+ const output = loadProducedYaml(platform, proxies);
+ const explicit = output.proxies.find(
+ (proxy) => proxy.name === 'WG Explicit CIDR',
+ );
+ const defaults = output.proxies.find(
+ (proxy) => proxy.name === 'WG Default CIDR',
+ );
+
+ expectSubset(explicit, {
+ ip: '10.0.0.2/24',
+ ipv6: 'fd00::2/64',
+ });
+ expectSubset(defaults, {
+ ip: '10.0.0.3/32',
+ ipv6: 'fd00::3/128',
+ });
+ expect(explicit).to.not.have.property('ip-cidr');
+ expect(explicit).to.not.have.property('ipv6-cidr');
+ expect(defaults).to.not.have.property('ip-cidr');
+ expect(defaults).to.not.have.property('ipv6-cidr');
+ }
+ });
+
+ it('emits WireGuard address CIDR suffixes for sing-box exports', function () {
+ const output = loadProducedJson('sing-box', [
+ {
+ type: 'wireguard',
+ name: 'Sing-box WG Explicit CIDR',
+ server: 'wg-explicit.example.com',
+ port: 51820,
+ 'private-key': 'private-key-1',
+ 'public-key': 'public-key-1',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ },
+ {
+ type: 'wireguard',
+ name: 'Sing-box WG Default CIDR',
+ server: 'wg-default.example.com',
+ port: 51820,
+ 'private-key': 'private-key-2',
+ 'public-key': 'public-key-2',
+ ip: '10.0.0.3',
+ ipv6: 'fd00::3',
+ },
+ ]);
+
+ const explicit = output.endpoints.find(
+ (endpoint) => endpoint.tag === 'Sing-box WG Explicit CIDR',
+ );
+ const defaults = output.endpoints.find(
+ (endpoint) => endpoint.tag === 'Sing-box WG Default CIDR',
+ );
+
+ expectSubset(explicit, {
+ type: 'wireguard',
+ address: ['10.0.0.2/24', 'fd00::2/64'],
+ });
+ expectSubset(defaults, {
+ type: 'wireguard',
+ address: ['10.0.0.3/32', 'fd00::3/128'],
+ });
+ });
+
+ it('emits Tailscale endpoint fields for sing-box exports', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'tailscale',
+ name: 'Mihomo TS',
+ 'state-dir': './mihomo-ts',
+ 'auth-key': 'tskey-auth-test',
+ 'control-url': 'https://headscale.example.com',
+ ephemeral: true,
+ hostname: 'sub-store',
+ udp: true,
+ 'accept-routes': true,
+ 'exit-node': '100.64.0.1',
+ 'exit-node-allow-lan-access': true,
+ 'dialer-proxy': 'proxy-out',
+ 'udp-timeout': '30s',
+ });
+
+ const mihomo = output.endpoints.find(
+ (endpoint) => endpoint.tag === 'Mihomo TS',
+ );
+
+ expectSubset(mihomo, {
+ type: 'tailscale',
+ state_directory: './mihomo-ts',
+ auth_key: 'tskey-auth-test',
+ control_url: 'https://headscale.example.com',
+ ephemeral: true,
+ hostname: 'sub-store',
+ accept_routes: true,
+ exit_node: '100.64.0.1',
+ exit_node_allow_lan_access: true,
+ detour: 'proxy-out',
+ udp_timeout: '30s',
+ });
+ expect(mihomo).to.not.have.property('udp');
+ });
+
+ it('does not mix Tailscale control_http_client with legacy sing-box dialer fields', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'tailscale',
+ name: 'Tailscale Control Client',
+ 'control-http-client': {
+ detour: 'control-out',
+ },
+ 'dialer-proxy': 'legacy-out',
+ });
+
+ const tailscale = output.endpoints.find(
+ (endpoint) => endpoint.tag === 'Tailscale Control Client',
+ );
+
+ expect(tailscale.control_http_client).to.deep.equal({
+ detour: 'control-out',
+ });
+ expect(tailscale).to.not.have.property('detour');
+ });
+
+ it('emits WireGuard interface CIDR suffixes for Egern exports', function () {
+ const proxies = [
+ {
+ type: 'wireguard',
+ name: 'Egern WG Explicit CIDR',
+ server: 'wg-explicit.example.com',
+ port: 51820,
+ 'private-key': 'private-key-1',
+ 'public-key': 'public-key-1',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ },
+ {
+ type: 'wireguard',
+ name: 'Egern WG Default CIDR',
+ server: 'wg-default.example.com',
+ port: 51820,
+ 'private-key': 'private-key-2',
+ 'public-key': 'public-key-2',
+ ip: '10.0.0.3',
+ ipv6: 'fd00::3',
+ },
+ ];
+
+ const internal = produceInternal('Egern', proxies);
+ const external = loadProducedYaml('Egern', proxies);
+
+ const explicitInternal = internal.find(
+ (proxy) => proxy.wireguard?.name === 'Egern WG Explicit CIDR',
+ );
+ const defaultsInternal = internal.find(
+ (proxy) => proxy.wireguard?.name === 'Egern WG Default CIDR',
+ );
+ const explicitExternal = external.proxies.find(
+ (proxy) => proxy.wireguard?.name === 'Egern WG Explicit CIDR',
+ );
+ const defaultsExternal = external.proxies.find(
+ (proxy) => proxy.wireguard?.name === 'Egern WG Default CIDR',
+ );
+
+ for (const proxy of [
+ explicitInternal,
+ defaultsInternal,
+ explicitExternal,
+ defaultsExternal,
+ ]) {
+ expect(proxy.wireguard).to.not.have.property('ip-cidr');
+ expect(proxy.wireguard).to.not.have.property('ipv6-cidr');
+ }
+
+ expectSubset(explicitInternal, {
+ wireguard: {
+ local_ipv4: '10.0.0.2/24',
+ local_ipv6: 'fd00::2/64',
+ },
+ });
+ expectSubset(defaultsInternal, {
+ wireguard: {
+ local_ipv4: '10.0.0.3/32',
+ local_ipv6: 'fd00::3/128',
+ },
+ });
+ expectSubset(explicitExternal, {
+ wireguard: {
+ local_ipv4: '10.0.0.2/24',
+ local_ipv6: 'fd00::2/64',
+ },
+ });
+ expectSubset(defaultsExternal, {
+ wireguard: {
+ local_ipv4: '10.0.0.3/32',
+ local_ipv6: 'fd00::3/128',
+ },
+ });
+ });
+
+ it('filters canonical shadowsocks over-tls nodes for unsupported client targets by default', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Unsupported SS TLS',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ };
+
+ for (const platform of [
+ 'Clash',
+ 'ClashMeta',
+ 'Mihomo',
+ 'Stash',
+ 'Loon',
+ 'Surge',
+ 'SurgeMac',
+ 'Surfboard',
+ 'Egern',
+ 'sing-box',
+ 'URI',
+ 'V2Ray',
+ ]) {
+ expect(produceInternal(platform, proxy), platform).to.have.length(
+ 0,
+ );
+ }
+
+ expect(ProxyUtils.produce([proxy], 'Clash', 'external')).to.equal(
+ 'proxies:\n',
+ );
+ expect(ProxyUtils.produce([proxy], 'Loon', 'external')).to.equal('');
+ expect(ProxyUtils.produce([proxy], 'URI', 'external')).to.equal('');
+ expect(produceInternal('QX', proxy), 'QX').to.have.length(1);
+ expect(
+ produceInternal('Shadowrocket', proxy),
+ 'Shadowrocket',
+ ).to.have.length(1);
+ });
+
+ it('keeps canonical shadowsocks over-tls nodes when include-unsupported-proxy is enabled for Clash', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Clash SS TLS',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ };
+
+ const internal = produceInternal('Clash', proxy, {
+ 'include-unsupported-proxy': true,
+ });
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'ss',
+ name: 'Clash SS TLS',
+ tls: true,
+ sni: 'a.com',
+ });
+ });
+
+ it('still treats canonical shadowsocks tls nodes without sni as unsupported for disallowed targets', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Unsupported SS TLS No Host',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ };
+
+ expect(produceInternal('Clash', proxy)).to.have.length(0);
+ expect(produceExternal('Clash', proxy)).to.equal('proxies:\n');
+ });
+
+ it('still treats canonical shadowsocks tcp tls nodes as unsupported for disallowed targets', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'Unsupported SS TLS TCP',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ network: 'tcp',
+ };
+
+ expect(produceInternal('Clash', proxy)).to.have.length(0);
+ expect(ProxyUtils.produce([proxy], 'URI', 'external')).to.equal('');
+ });
+
+ it('skips invalid Egern nodes and keeps the rest of the subscription', function () {
+ const proxies = [
+ {
+ type: 'vless',
+ name: 'Invalid VLESS',
+ server: 'invalid.example.com',
+ port: 443,
+ uuid: UUID,
+ encryption: 'aes-128-gcm',
+ },
+ {
+ type: 'ss',
+ name: 'Healthy SS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ },
+ ];
+
+ const internal = produceInternal('Egern', proxies);
+ const external = loadProducedYaml('Egern', proxies);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ shadowsocks: {
+ name: 'Healthy SS',
+ method: 'aes-128-gcm',
+ },
+ });
+ expectSubset(external.proxies[0], {
+ shadowsocks: {
+ name: 'Healthy SS',
+ },
+ });
+ });
+
+ it('skips Egern shadowsocks proxies with unsupported plugins and keeps the rest of the subscription', function () {
+ const proxies = [
+ {
+ type: 'ss',
+ name: 'Invalid SS Plugin',
+ server: 'invalid.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ },
+ },
+ {
+ type: 'ss',
+ name: 'Healthy SS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ },
+ ];
+
+ const internal = produceInternal('Egern', proxies);
+ const external = loadProducedYaml('Egern', proxies);
+
+ expect(internal).to.have.length(1);
+ expect(external.proxies).to.have.length(1);
+ expectSubset(internal[0], {
+ shadowsocks: {
+ name: 'Healthy SS',
+ method: 'aes-128-gcm',
+ },
+ });
+ expectSubset(external.proxies[0], {
+ shadowsocks: {
+ name: 'Healthy SS',
+ },
+ });
+ });
+
+ it('keeps numeric v2ray-plugin mux state in sing-box plugin opts', function () {
+ const buildProxy = (name, mux) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ mux,
+ },
+ });
+
+ const output = loadProducedJson('sing-box', [
+ buildProxy('Sing-box Mux On', 1),
+ buildProxy('Sing-box Mux Off', 0),
+ ]);
+ const muxOn = output.outbounds.find(
+ (outbound) => outbound.tag === 'Sing-box Mux On',
+ );
+ const muxOff = output.outbounds.find(
+ (outbound) => outbound.tag === 'Sing-box Mux Off',
+ );
+
+ expect(output.outbounds).to.have.length(2);
+ expectSubset(muxOn, {
+ tag: 'Sing-box Mux On',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ multiplex: {
+ enabled: true,
+ },
+ });
+ expect(muxOn.plugin_opts).to.include('mux=1');
+
+ expectSubset(muxOff, {
+ tag: 'Sing-box Mux Off',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ });
+ expect(muxOff).to.not.have.property('multiplex');
+ expect(muxOff.plugin_opts).to.include('mux=0');
+ });
+
+ it('normalizes boolean v2ray-plugin mux state in sing-box plugin opts', function () {
+ const buildProxy = (name, mux) => ({
+ type: 'ss',
+ name,
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ mux,
+ },
+ });
+
+ const output = loadProducedJson('sing-box', [
+ buildProxy('Sing-box Boolean Mux On', true),
+ buildProxy('Sing-box Boolean Mux Off', false),
+ ]);
+ const muxOn = output.outbounds.find(
+ (outbound) => outbound.tag === 'Sing-box Boolean Mux On',
+ );
+ const muxOff = output.outbounds.find(
+ (outbound) => outbound.tag === 'Sing-box Boolean Mux Off',
+ );
+
+ expectSubset(muxOn, {
+ tag: 'Sing-box Boolean Mux On',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ multiplex: {
+ enabled: true,
+ },
+ });
+ expect(muxOn.plugin_opts).to.include('mux=1');
+
+ expectSubset(muxOff, {
+ tag: 'Sing-box Boolean Mux Off',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ });
+ expect(muxOff).to.not.have.property('multiplex');
+ expect(muxOff.plugin_opts).to.include('mux=0');
+ });
+
+ it('round-trips Clash-style boolean v2ray-plugin mux flags into sing-box exports', function () {
+ const proxies = ProxyUtils.parse(`proxies:
+ - name: Clash Boolean Mux On
+ type: ss
+ server: ss.example.com
+ port: 8388
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: true
+ - name: Clash Boolean Mux Off
+ type: ss
+ server: ss.example.com
+ port: 8388
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: false
+`);
+
+ const output = loadProducedJson('sing-box', proxies);
+ const muxOn = output.outbounds.find(
+ (outbound) => outbound.tag === 'Clash Boolean Mux On',
+ );
+ const muxOff = output.outbounds.find(
+ (outbound) => outbound.tag === 'Clash Boolean Mux Off',
+ );
+
+ expectSubset(muxOn, {
+ tag: 'Clash Boolean Mux On',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ multiplex: {
+ enabled: true,
+ },
+ });
+ expect(muxOn.plugin_opts).to.include('mux=1');
+
+ expectSubset(muxOff, {
+ tag: 'Clash Boolean Mux Off',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ });
+ expect(muxOff).to.not.have.property('multiplex');
+ expect(muxOff.plugin_opts).to.include('mux=0');
+ });
+
+ it('round-trips Clash-style string boolean v2ray-plugin mux flags into sing-box exports', function () {
+ const proxies = ProxyUtils.parse(`proxies:
+ - name: Clash String Mux On
+ type: ss
+ server: ss.example.com
+ port: 8388
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: ' TRUE '
+ - name: Clash String Mux Off
+ type: ss
+ server: ss.example.com
+ port: 8388
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: ' false '
+`);
+
+ const output = loadProducedJson('sing-box', proxies);
+ const muxOn = output.outbounds.find(
+ (outbound) => outbound.tag === 'Clash String Mux On',
+ );
+ const muxOff = output.outbounds.find(
+ (outbound) => outbound.tag === 'Clash String Mux Off',
+ );
+
+ expectSubset(muxOn, {
+ tag: 'Clash String Mux On',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ multiplex: {
+ enabled: true,
+ },
+ });
+ expect(muxOn.plugin_opts).to.include('mux=1');
+
+ expectSubset(muxOff, {
+ tag: 'Clash String Mux Off',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ });
+ expect(muxOff).to.not.have.property('multiplex');
+ expect(muxOff.plugin_opts).to.include('mux=0');
+ });
+
+ it('treats string v2ray-plugin mux values from legacy JSON payloads as disabled in sing-box exports', function () {
+ const legacy = 'YWVzLTEyOC1nY206c2VjcmV0QHNzLmV4YW1wbGUuY29tOjgzODg=';
+ const plugin =
+ 'eyJtb2RlIjoid2Vic29ja2V0IiwiaG9zdCI6ImNkbi5leGFtcGxlLmNvbSIsInBhdGgiOiIvc29ja2V0IiwidGxzIjp0cnVlLCJtdXgiOiIwIn0=';
+
+ const proxies = ProxyUtils.parse(
+ `ss://${legacy}?v2ray-plugin=${plugin}#Legacy%20JSON%20Mux%20Off`,
+ );
+ const output = loadProducedJson('sing-box', proxies);
+ const outbound = output.outbounds.find(
+ (item) => item.tag === 'Legacy JSON Mux Off',
+ );
+
+ expectSubset(outbound, {
+ tag: 'Legacy JSON Mux Off',
+ type: 'shadowsocks',
+ plugin: 'v2ray-plugin',
+ });
+ expect(outbound).to.not.have.property('multiplex');
+ expect(outbound.plugin_opts).to.include('mux=0');
+ });
+
+ it('maps mihomo udp capability flags to sing-box network only when disabling UDP', function () {
+ const output = loadProducedJson('sing-box', [
+ {
+ type: 'ss',
+ name: 'SS UDP On',
+ server: 'ss-on.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ udp: true,
+ },
+ {
+ type: 'ss',
+ name: 'SS UDP Off',
+ server: 'ss-off.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ udp: false,
+ },
+ {
+ type: 'ss',
+ name: 'SS Network Override',
+ server: 'ss-override.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ udp: false,
+ _network: 'udp',
+ },
+ ]);
+
+ const udpOn = output.outbounds.find((item) => item.tag === 'SS UDP On');
+ const udpOff = output.outbounds.find(
+ (item) => item.tag === 'SS UDP Off',
+ );
+ const networkOverride = output.outbounds.find(
+ (item) => item.tag === 'SS Network Override',
+ );
+
+ expect(udpOn).to.not.have.property('network');
+ expect(udpOff).to.have.property('network', 'tcp');
+ expect(networkOverride).to.have.property('network', 'udp');
+ });
+
+ it('does not emit sing-box network for protocols without sing-box network options', function () {
+ const output = loadProducedJson('sing-box', [
+ {
+ type: 'anytls',
+ name: 'AnyTLS No Network',
+ server: 'anytls.example.com',
+ port: 443,
+ password: 'secret',
+ udp: false,
+ _network: 'udp',
+ },
+ {
+ type: 'tailscale',
+ name: 'Tailscale No Network',
+ udp: false,
+ _network: 'udp',
+ },
+ {
+ type: 'wireguard',
+ name: 'WireGuard No Network',
+ server: 'wg.example.com',
+ port: 51820,
+ 'private-key': 'private-key',
+ 'public-key': 'public-key',
+ ip: '10.0.0.2',
+ udp: false,
+ _network: 'udp',
+ },
+ ]);
+
+ const anytls = output.outbounds.find(
+ (item) => item.tag === 'AnyTLS No Network',
+ );
+ const tailscale = output.endpoints.find(
+ (item) => item.tag === 'Tailscale No Network',
+ );
+ const wireguard = output.endpoints.find(
+ (item) => item.tag === 'WireGuard No Network',
+ );
+
+ expect(anytls).to.not.have.property('network');
+ expect(tailscale).to.not.have.property('network');
+ expect(wireguard).to.not.have.property('network');
+ });
+
+ it('emits sing-box outbounds with reality tls and websocket transport', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'vless',
+ name: 'Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+
+ expect(output.outbounds).to.have.length(1);
+ expect(output.endpoints).to.have.length(0);
+ expectSubset(output.outbounds[0], {
+ tag: 'Reality',
+ type: 'vless',
+ server: 'vless.example.com',
+ server_port: 443,
+ flow: 'xtls-rprx-vision',
+ tls: {
+ enabled: true,
+ server_name: 'vless.example.com',
+ reality: {
+ enabled: true,
+ public_key: 'pubkey',
+ short_id: '08',
+ },
+ utls: {
+ enabled: true,
+ fingerprint: 'chrome',
+ },
+ },
+ transport: {
+ type: 'ws',
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ early_data_header_name: 'Sec-WebSocket-Protocol',
+ max_early_data: 2048,
+ },
+ });
+ });
+
+ it('emits sing-box httpupgrade transport without websocket early data fields', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'vless',
+ name: 'Upgrade',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&ed=2048&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ },
+ });
+
+ expect(output.outbounds).to.have.length(1);
+ expectSubset(output.outbounds[0], {
+ tag: 'Upgrade',
+ type: 'vless',
+ transport: {
+ type: 'httpupgrade',
+ path: '/upgrade?a=1&b=2',
+ host: 'upgrade.example.com',
+ },
+ });
+ expect(output.outbounds[0].transport).to.not.have.property(
+ 'early_data_header_name',
+ );
+ expect(output.outbounds[0].transport).to.not.have.property(
+ 'max_early_data',
+ );
+ });
+
+ it('normalizes sing-box ech PEM config strings with escaped newlines', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'vless',
+ name: 'ECH PEM',
+ server: 'ech.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ config: [
+ '-----BEGIN ECH CONFIGS-----\\nZWNoLWNvbmZpZw==\\n-----END ECH CONFIGS-----',
+ ],
+ },
+ });
+
+ expectSubset(output.outbounds[0], {
+ tag: 'ECH PEM',
+ tls: {
+ enabled: true,
+ server_name: 'ech.example.com',
+ ech: {
+ enabled: true,
+ config: [
+ '-----BEGIN ECH CONFIGS-----',
+ 'ZWNoLWNvbmZpZw==',
+ '-----END ECH CONFIGS-----',
+ ],
+ },
+ },
+ });
+ });
+
+ it('omits xhttp proxies from sing-box exports', function () {
+ const output = loadProducedJson('sing-box', {
+ type: 'vless',
+ name: 'XHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-up',
+ },
+ });
+
+ expect(output.outbounds).to.have.length(0);
+ expect(output.endpoints).to.have.length(0);
+ });
+
+ it('stringifies JSON producer outputs as plain arrays', function () {
+ const proxy = {
+ type: 'http',
+ name: 'JSON HTTP',
+ server: 'http.example.com',
+ port: 8080,
+ username: 'user',
+ password: 'pass',
+ };
+
+ const internal = produceInternal('JSON', proxy);
+ const external = loadProducedJson('JSON', proxy);
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ type: 'http',
+ name: 'JSON HTTP',
+ });
+ expect(external).to.have.length(1);
+ expectSubset(external[0], {
+ type: 'http',
+ name: 'JSON HTTP',
+ });
+ });
+
+ it('blocks Mihomo xhttp reality-opts inheritance when upload has reality-opts but download does not', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Reality No Download Reality',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'reality-opts': {
+ 'public-key': 'upload-pubkey',
+ 'short-id': 'ab',
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+ const external = loadProducedYaml('Mihomo', proxy);
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ 'xhttp-opts': {
+ 'download-settings': {
+ 'reality-opts': { 'public-key': '' },
+ },
+ },
+ });
+ expectSubset(external.proxies[0], {
+ 'xhttp-opts': {
+ 'download-settings': {
+ 'reality-opts': { 'public-key': '' },
+ },
+ },
+ });
+ });
+
+ it('does not inject reality-opts blocker when download-settings already has reality-opts', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP Reality Both',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'reality-opts': {
+ 'public-key': 'upload-pubkey',
+ 'short-id': 'ab',
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ 'reality-opts': {
+ 'public-key': 'download-pubkey',
+ 'short-id': 'cd',
+ },
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+
+ expect(internal).to.have.length(1);
+ expectSubset(internal[0], {
+ 'xhttp-opts': {
+ 'download-settings': {
+ 'reality-opts': {
+ 'public-key': 'download-pubkey',
+ 'short-id': 'cd',
+ },
+ },
+ },
+ });
+ });
+
+ it('does not inject reality-opts blocker when upload has no reality-opts', function () {
+ const proxy = {
+ type: 'vless',
+ name: 'XHTTP No Reality',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ },
+ },
+ };
+
+ const internal = produceInternal('Mihomo', proxy);
+
+ expect(internal).to.have.length(1);
+ const ds = internal[0]['xhttp-opts']['download-settings'];
+ expect(ds).to.not.have.property('reality-opts');
+ });
+});
diff --git a/backend/src/test/proxy-producers/text.spec.js b/backend/src/test/proxy-producers/text.spec.js
new file mode 100644
index 0000000000..24d133f341
--- /dev/null
+++ b/backend/src/test/proxy-producers/text.spec.js
@@ -0,0 +1,3951 @@
+import { expect } from 'chai';
+import { Base64 } from 'js-base64';
+import { describe, it } from 'mocha';
+
+import $ from '@/core/app';
+import { ProxyUtils } from '@/core/proxy-utils';
+import QX_Producer from '@/core/proxy-utils/producers/qx';
+import { produceExternal, UUID } from './helpers';
+
+function captureWarns(fn) {
+ const originalWarn = $.warn;
+ const warnings = [];
+ $.warn = (message) => warnings.push(message);
+ try {
+ const result = fn();
+ return { result, warnings };
+ } finally {
+ $.warn = originalWarn;
+ }
+}
+
+function captureErrors(fn) {
+ const originalError = $.error;
+ const errors = [];
+ $.error = (message) => errors.push(message);
+ try {
+ const result = fn();
+ return { result, errors };
+ } finally {
+ $.error = originalError;
+ }
+}
+
+describe('Proxy text producers', function () {
+ it('produces Quantumult X shadowsocks over-tls lines from canonical tls nodes', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS Over TLS',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ 'skip-cert-verify': true,
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,obfs-host=a.com,tls-verification=false,tag=QX SS Over TLS',
+ );
+ });
+
+ it('produces Quantumult X shadowsocks over-tls lines without obfs-host when sni is absent', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS Over TLS No Host',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,tag=QX SS Over TLS No Host',
+ );
+ });
+
+ it('produces Quantumult X shadowsocks over-tls lines for canonical tcp tls nodes', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS Over TLS TCP',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ network: 'tcp',
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,obfs-host=a.com,tag=QX SS Over TLS TCP',
+ );
+ });
+
+ it('preserves Quantumult X obfs-host after Shadowrocket rewrites ss tls sni to servername', function () {
+ const proxy = {
+ type: 'ss',
+ name: 'QX SS Over TLS Re-emit',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ sni: 'a.com',
+ };
+
+ ProxyUtils.produce([proxy], 'Shadowrocket', 'internal');
+ const output = ProxyUtils.produce([proxy], 'QX', 'external');
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:443,method=aes-128-gcm,password=secret,obfs=over-tls,obfs-host=a.com,tag=QX SS Over TLS Re-emit',
+ );
+ });
+
+ it('keeps legacy Quantumult X shadowsocks obfs tls output unchanged', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS Obfs TLS',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'tls',
+ host: 'legacy.example.com',
+ },
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:8388,method=aes-128-gcm,password=secret,obfs=tls,obfs-host=legacy.example.com,tag=QX SS Obfs TLS',
+ );
+ });
+
+ it('preserves Quantumult X shadowsocks http token output', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS HTTP',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ _qx_obfs_http: 'http',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'plain.example.com',
+ path: '/plain',
+ },
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:8388,method=aes-128-gcm,password=secret,obfs=http,obfs-host=plain.example.com,obfs-uri=/plain,tag=QX SS HTTP',
+ );
+ });
+
+ // QX examples contain the upstream "vemss-http" typo; keep emitting it
+ // when the parsed source token was preserved in _qx_obfs_http.
+ it('preserves Quantumult X shadowsocks vemss-http alias output', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS VMess HTTP',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ _qx_obfs_http: 'vemss-http',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'legacy.example.com',
+ path: '/resource',
+ },
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:8388,method=aes-128-gcm,password=secret,obfs=vemss-http,obfs-host=legacy.example.com,obfs-uri=/resource,tag=QX SS VMess HTTP',
+ );
+ });
+
+ it('preserves Quantumult X shadowsocks shadowsocks-http token output', function () {
+ const output = produceExternal('QX', {
+ type: 'ss',
+ name: 'QX SS Shadowsocks HTTP',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ _qx_obfs_http: 'shadowsocks-http',
+ plugin: 'obfs',
+ 'plugin-opts': {
+ mode: 'http',
+ host: 'shadow.example.com',
+ path: '/shadow',
+ },
+ });
+
+ expect(output).to.equal(
+ 'shadowsocks=ss.example.com:8388,method=aes-128-gcm,password=secret,obfs=shadowsocks-http,obfs-host=shadow.example.com,obfs-uri=/shadow,tag=QX SS Shadowsocks HTTP',
+ );
+ });
+
+ it('preserves Quantumult X vmess http token output', function () {
+ const output = produceExternal('QX', {
+ type: 'vmess',
+ name: 'QX VMess HTTP',
+ server: 'vmess.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ _qx_obfs_http: 'http',
+ network: 'http',
+ 'http-opts': {
+ path: '/http',
+ headers: {
+ Host: 'plain.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vmess=vmess.example.com:80,method=none,password=${UUID},obfs=http,obfs-uri=/http,obfs-host=plain.example.com,aead=false,tag=QX VMess HTTP`,
+ );
+ });
+
+ // QX examples contain the upstream "vemss-http" typo; keep emitting it
+ // when the parsed source token was preserved in _qx_obfs_http.
+ it('preserves Quantumult X vmess vemss-http alias output', function () {
+ const output = produceExternal('QX', {
+ type: 'vmess',
+ name: 'QX VMess VMess HTTP',
+ server: 'vmess.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ _qx_obfs_http: 'vemss-http',
+ network: 'http',
+ 'http-opts': {
+ path: '/vemss',
+ headers: {
+ Host: 'vemss.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vmess=vmess.example.com:80,method=none,password=${UUID},obfs=vemss-http,obfs-uri=/vemss,obfs-host=vemss.example.com,aead=false,tag=QX VMess VMess HTTP`,
+ );
+ });
+
+ it('preserves Quantumult X vmess shadowsocks-http alias output', function () {
+ const output = produceExternal('QX', {
+ type: 'vmess',
+ name: 'QX VMess Shadowsocks HTTP',
+ server: 'vmess.example.com',
+ port: 80,
+ cipher: 'none',
+ uuid: UUID,
+ _qx_obfs_http: 'shadowsocks-http',
+ network: 'http',
+ 'http-opts': {
+ path: '/resource/file',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vmess=vmess.example.com:80,method=none,password=${UUID},obfs=shadowsocks-http,obfs-uri=/resource/file,obfs-host=cdn.example.com,aead=false,tag=QX VMess Shadowsocks HTTP`,
+ );
+ });
+
+ it('preserves Quantumult X vless vmess-http token output', function () {
+ const output = produceExternal('QX', {
+ type: 'vless',
+ name: 'QX VLESS HTTP',
+ server: 'vless.example.com',
+ port: 80,
+ uuid: UUID,
+ _qx_obfs_http: 'vmess-http',
+ network: 'http',
+ 'http-opts': {
+ path: '/vless-http',
+ headers: {
+ Host: 'vless.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless=vless.example.com:80,method=none,password=${UUID},obfs=vmess-http,obfs-uri=/vless-http,obfs-host=vless.example.com,tag=QX VLESS HTTP`,
+ );
+ });
+
+ it('produces Quantumult X VLESS reality websocket lines', function () {
+ const output = produceExternal('QX', {
+ type: 'vless',
+ name: 'QX Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless=vless.example.com:443,method=none,password=${UUID},obfs=wss,obfs-uri=/ws,obfs-host=cdn.example.com,tls-host=sni.example.com,vless-flow=xtls-rprx-vision,tag=QX Reality,reality-base64-pubkey=pubkey,reality-hex-shortid=08`,
+ );
+ });
+
+ it('produces Quantumult X AnyTLS standard tls lines', function () {
+ const output = produceExternal('QX', {
+ type: 'anytls',
+ name: 'anytls-standard-tls-01',
+ server: 'example.com',
+ port: 443,
+ password: 'pwd',
+ tls: true,
+ sni: 'apple.com',
+ udp: true,
+ });
+
+ expect(output).to.equal(
+ 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-standard-tls-01',
+ );
+ });
+
+ it('produces Quantumult X AnyTLS reality tls lines', function () {
+ const output = produceExternal('QX', {
+ type: 'anytls',
+ name: 'anytls-reality-tls-01',
+ server: 'example.com',
+ port: 443,
+ password: 'pwd',
+ tls: true,
+ sni: 'apple.com',
+ udp: true,
+ 'reality-opts': {
+ 'public-key': 'k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk',
+ 'short-id': '0123456789abcdef',
+ },
+ });
+
+ expect(output).to.equal(
+ 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-reality-tls-01,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef',
+ );
+ });
+
+ it('produces Quantumult X AnyTLS lines and keeps only supported fields', function () {
+ const output = produceExternal('QX', {
+ type: 'anytls',
+ name: 'QX AnyTLS Supported Fields',
+ server: 'anytls.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ udp: true,
+ tfo: true,
+ 'skip-cert-verify': true,
+ 'test-url': 'https://check.example.com',
+ 'idle-session-timeout': 30,
+ 'max-stream-count': 16,
+ });
+
+ expect(output).to.equal(
+ 'anytls=anytls.example.com:443,password=secret,over-tls=true,tls-verification=false,tls-host=sni.example.com,fast-open=true,udp-relay=true,server_check_url=https://check.example.com,tag=QX AnyTLS Supported Fields',
+ );
+ });
+
+ it('rejects Quantumult X AnyTLS transport shapes that QX cannot represent', function () {
+ const producer = QX_Producer();
+
+ expect(() =>
+ producer.produce({
+ type: 'anytls',
+ name: 'QX AnyTLS WS',
+ server: 'anytls.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/anytls',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ }),
+ ).to.throw('Platform QX does not support AnyTLS with transport ws');
+ });
+
+ it('round-trips Quantumult X AnyTLS reality lines without changing supported semantics', function () {
+ const raw =
+ 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-reality-tls-01,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef';
+ const [proxy] = ProxyUtils.parse(raw);
+ const output = ProxyUtils.produce([proxy], 'QX', 'external');
+
+ expect(output).to.equal(raw);
+ });
+
+ it('produces Loon VLESS reality websocket lines', function () {
+ const output = produceExternal('Loon', {
+ type: 'vless',
+ name: 'Loon Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ 'skip-cert-verify': true,
+ });
+
+ expect(output).to.equal(
+ `Loon Reality=vless,vless.example.com,443,"${UUID}",transport=ws,path=/ws,host=cdn.example.com,over-tls=true,skip-cert-verify=true,flow=xtls-rprx-vision,sni=sni.example.com,public-key="pubkey",short-id=08`,
+ );
+ });
+
+ it('produces Loon Hysteria2 lines with port hopping', function () {
+ const output = produceExternal('Loon', {
+ type: 'hysteria2',
+ name: 'Loon Hysteria2',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '1000,2000-3000,5000',
+ 'hop-interval': 30,
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ });
+
+ expect(output).to.equal(
+ 'Loon Hysteria2=Hysteria2,hy2.example.com,443,"secret",server-ports="1000,2000-3000,5000",hop-interval=30,tls-name=peer.example.com,skip-cert-verify=true',
+ );
+ });
+
+ it('does not emit empty Loon Hysteria2 port hopping fields', function () {
+ const output = produceExternal('Loon', {
+ type: 'hysteria2',
+ name: 'Loon Hysteria2 Plain',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '',
+ 'hop-interval': ' ',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ });
+
+ expect(output).to.equal(
+ 'Loon Hysteria2 Plain=Hysteria2,hy2.example.com,443,"secret",tls-name=peer.example.com,skip-cert-verify=true',
+ );
+ });
+
+ it('exports URI Hysteria2 port hopping nodes to Loon fields', function () {
+ const [proxy] = ProxyUtils.parse(
+ 'hy2://secret@hy2.example.com:443?mport=1000,2000-3000,5000&hop-interval=30#URI%20Hysteria2',
+ );
+ const output = produceExternal('Loon', proxy);
+
+ expect(output).to.equal(
+ 'URI Hysteria2=Hysteria2,hy2.example.com,443,"secret",server-ports="1000,2000-3000,5000",hop-interval=30,tls-name=hy2.example.com,skip-cert-verify=false,fast-open=false',
+ );
+ });
+
+ it('produces Surge TUIC v5 lines with port hopping', function () {
+ const output = produceExternal('Surge', {
+ type: 'tuic',
+ name: 'Surge TUIC',
+ server: 'tuic.example.com',
+ port: 443,
+ uuid: UUID,
+ password: 'secret',
+ ports: '9000,9002-9004',
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ alpn: ['h3'],
+ ecn: true,
+ });
+
+ expect(output).to.equal(
+ `Surge TUIC=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",alpn=h3,port-hopping="9000;9002-9004",sni="sni.example.com",skip-cert-verify=true,ecn=true`,
+ );
+ });
+
+ it('does not emit empty Surge port hopping fields', function () {
+ const output = produceExternal('Surge', [
+ {
+ type: 'tuic',
+ name: 'Surge TUIC Plain',
+ server: 'tuic.example.com',
+ port: 443,
+ uuid: UUID,
+ password: 'secret',
+ ports: '',
+ 'hop-interval': ' ',
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ alpn: ['h3'],
+ },
+ {
+ type: 'hysteria2',
+ name: 'Surge Hysteria2 Plain',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '',
+ 'hop-interval': ' ',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ },
+ ]);
+
+ expect(output).to.equal(
+ `Surge TUIC Plain=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",alpn=h3,sni="sni.example.com",skip-cert-verify=true\nSurge Hysteria2 Plain=hysteria2,hy2.example.com,443,password="secret",sni="peer.example.com",skip-cert-verify=true`,
+ );
+ });
+
+ it('produces Surge Hysteria2 lines with port hopping', function () {
+ const output = produceExternal('Surge', {
+ type: 'hysteria2',
+ name: 'Surge Hysteria2',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '8443,8445-8447',
+ 'hop-interval': 30,
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ down: '100 Mbps',
+ });
+
+ expect(output).to.equal(
+ 'Surge Hysteria2=hysteria2,hy2.example.com,443,password="secret",port-hopping="8443;8445-8447",port-hopping-interval=30,sni="peer.example.com",skip-cert-verify=true,download-bandwidth=100',
+ );
+ });
+
+ it('quotes Surge SSH private-key and TLS client-cert keystore values', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'ssh',
+ name: 'Surge SSH Key',
+ server: 'ssh.example.com',
+ port: 22,
+ username: 'user',
+ 'keystore-private-key': "'ssh-key'",
+ },
+ {
+ type: 'http',
+ name: 'Surge HTTPS Client Cert',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ 'keystore-client-cert': "'client-cert'",
+ },
+ ],
+ 'Surge',
+ 'external',
+ );
+
+ expect(output.split('\n')).to.deep.equal([
+ 'Surge SSH Key=ssh,ssh.example.com,22,username="user",private-key="ssh-key"',
+ 'Surge HTTPS Client Cert=https,https.example.com,443,client-cert="client-cert"',
+ ]);
+ });
+
+ it('produces Surge root headers for HTTP, HTTPS, HTTP/2 CONNECT, and TrustTunnel', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'http',
+ name: 'Surge HTTP Headers',
+ server: 'http.example.com',
+ port: 8080,
+ username: 'user',
+ password: 'pass',
+ headers: {
+ 'X-Client': 'Surge',
+ 'X-Token': 'abc',
+ },
+ },
+ {
+ type: 'http',
+ name: 'Surge HTTPS Headers',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ sni: 'sni.example.com',
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ {
+ type: 'h2-connect',
+ name: 'Surge H2 Headers',
+ server: 'h2.example.com',
+ port: 443,
+ tls: true,
+ sni: 'sni.example.com',
+ headers: {
+ 'X-Padding': '',
+ },
+ },
+ {
+ type: 'trusttunnel',
+ name: 'Surge Trust Headers',
+ server: 'trust.example.com',
+ port: 443,
+ username: 'user',
+ password: 'pass',
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ sni: 'sni.example.com',
+ },
+ ],
+ 'Surge',
+ 'external',
+ );
+
+ expect(output.split('\n')).to.deep.equal([
+ 'Surge HTTP Headers=http,http.example.com,8080,username="user",password="pass",headers="X-Client:"Surge";X-Token:"abc""',
+ 'Surge HTTPS Headers=https,https.example.com,443,headers="X-Padding:""",sni="sni.example.com"',
+ 'Surge H2 Headers=h2-connect,h2.example.com,443,headers="X-Padding:""",sni="sni.example.com"',
+ 'Surge Trust Headers=trust-tunnel,trust.example.com,443,username="user",password="pass",headers="X-Client:"Surge"",sni="sni.example.com"',
+ ]);
+ });
+
+ it('produces Surge max-streams for HTTP/2 CONNECT and TrustTunnel', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'h2-connect',
+ name: 'Surge H2 Max Streams',
+ server: 'h2.example.com',
+ port: 443,
+ tls: true,
+ sni: 'sni.example.com',
+ headers: {
+ 'X-Padding': '',
+ },
+ 'max-streams': 1,
+ },
+ {
+ type: 'trusttunnel',
+ name: 'Surge Trust Max Streams',
+ server: 'trust.example.com',
+ port: 443,
+ username: 'user',
+ password: 'pass',
+ headers: {
+ 'X-Client': 'Surge',
+ },
+ 'max-streams': 3,
+ sni: 'sni.example.com',
+ },
+ ],
+ 'Surge',
+ 'external',
+ );
+
+ expect(output.split('\n')).to.deep.equal([
+ 'Surge H2 Max Streams=h2-connect,h2.example.com,443,headers="X-Padding:""",max-streams=1,sni="sni.example.com"',
+ 'Surge Trust Max Streams=trust-tunnel,trust.example.com,443,username="user",password="pass",headers="X-Client:"Surge"",max-streams=3,sni="sni.example.com"',
+ ]);
+ });
+
+ it('warns when Surge max-streams is greater than 3', function () {
+ const { result: output, warnings } = captureWarns(() =>
+ ProxyUtils.produce(
+ [
+ {
+ type: 'h2-connect',
+ name: 'Surge H2 High Max Streams',
+ server: 'h2.example.com',
+ port: 443,
+ tls: true,
+ 'max-streams': 4,
+ },
+ {
+ type: 'trusttunnel',
+ name: 'Surge Trust High Max Streams',
+ server: 'trust.example.com',
+ port: 443,
+ tls: true,
+ 'max-streams': 5,
+ },
+ ],
+ 'Surge',
+ 'external',
+ ),
+ );
+
+ expect(output.split('\n')).to.deep.equal([
+ 'Surge H2 High Max Streams=h2-connect,h2.example.com,443,max-streams=4',
+ 'Surge Trust High Max Streams=trust-tunnel,trust.example.com,443,max-streams=5',
+ ]);
+ expect(warnings).to.have.length(2);
+ expect(warnings[0]).to.include('max-streams=4');
+ expect(warnings[0]).to.include('greater than 3');
+ expect(warnings[0]).to.include('performance');
+ expect(warnings[1]).to.include('max-streams=5');
+ expect(warnings[1]).to.include('greater than 3');
+ expect(warnings[1]).to.include('performance');
+ });
+
+ it('round-trips Surge max-streams for HTTP/2 CONNECT and TrustTunnel', function () {
+ const proxies = ProxyUtils.parse(
+ [
+ 'Surge H2 Round Trip = h2-connect,h2.example.com,443,headers=X-Padding:,max-streams=1,sni=sni.example.com',
+ 'Surge Trust Round Trip = trust-tunnel,trust.example.com,443,username=user,password=pass,headers=X-Client:Surge,max-streams="3",sni=sni.example.com',
+ ].join('\n'),
+ );
+ const output = ProxyUtils.produce(proxies, 'Surge', 'external');
+
+ expect(output.split('\n')).to.deep.equal([
+ 'Surge H2 Round Trip=h2-connect,h2.example.com,443,headers="X-Padding:""",max-streams=1,sni="sni.example.com"',
+ 'Surge Trust Round Trip=trust-tunnel,trust.example.com,443,username="user",password="pass",headers="X-Client:"Surge"",max-streams=3,sni="sni.example.com"',
+ ]);
+ });
+
+ it('round-trips Surge root headers with nested quotes and separators', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'http',
+ name: 'Surge Nested Headers',
+ server: 'nested.example.com',
+ port: 443,
+ tls: true,
+ sni: 'sni.example.com',
+ headers: {
+ Host: 'nested.example.com',
+ 'X-Comma': 'a,b',
+ 'User-Agent': 'client/1.0 (Linux; U; Android 11)',
+ 'X-Quote': 'a",b',
+ 'X-Backslash': 'c\\d',
+ },
+ },
+ ],
+ 'Surge',
+ 'external',
+ );
+
+ const [proxy] = ProxyUtils.parse(output);
+
+ expect(proxy.headers).to.deep.equal({
+ Host: 'nested.example.com',
+ 'X-Comma': 'a,b',
+ 'User-Agent': 'client/1.0 (Linux; U; Android 11)',
+ 'X-Quote': 'a",b',
+ 'X-Backslash': 'c\\d',
+ });
+ expect(output).to.include(String.raw`X-Quote:"a\",b"`);
+ expect(output).to.include(String.raw`X-Backslash:"c\\d"`);
+ expect(proxy.sni).to.equal('sni.example.com');
+ });
+
+ it('filters root proxy headers for unsupported text targets with an error log', function () {
+ const { result, errors } = captureErrors(() =>
+ ProxyUtils.produce(
+ [
+ {
+ type: 'http',
+ name: 'QX HTTPS Headers',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Token': 'abc',
+ },
+ },
+ ],
+ 'QX',
+ 'external',
+ ),
+ );
+
+ expect(result).to.equal('');
+ expect(errors).to.have.length(1);
+ expect(errors[0]).to.include(
+ 'Target platform QX does not support headers for HTTPS proxy QX HTTPS Headers',
+ );
+ });
+
+ it('keeps root proxy headers for unsupported text targets when include-unsupported-proxy is enabled', function () {
+ const { result, errors } = captureErrors(() =>
+ ProxyUtils.produce(
+ [
+ {
+ type: 'http',
+ name: 'QX HTTPS Headers',
+ server: 'https.example.com',
+ port: 443,
+ tls: true,
+ headers: {
+ 'X-Token': 'abc',
+ },
+ },
+ ],
+ 'QX',
+ 'external',
+ { 'include-unsupported-proxy': true },
+ ),
+ );
+
+ expect(result).to.equal(
+ 'http=https.example.com:443,over-tls=true,tag=QX HTTPS Headers',
+ );
+ expect(errors).to.have.length(0);
+ });
+
+ it('produces Loon WireGuard lines without leaking CIDR metadata fields', function () {
+ const output = produceExternal('Loon', {
+ type: 'wireguard',
+ name: 'Loon WG',
+ server: 'wg.example.com',
+ port: 51820,
+ 'private-key': 'private-key',
+ 'public-key': 'public-key',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ });
+
+ expect(output).to.include('Loon WG=wireguard');
+ expect(output).to.include(',interface-ip=10.0.0.2');
+ expect(output).to.include(',interface-ipv6=fd00::2');
+ expect(output).to.not.include('ip-cidr');
+ expect(output).to.not.include('ipv6-cidr');
+ });
+
+ it('produces Surge WireGuard sections without leaking CIDR metadata fields', function () {
+ const output = produceExternal(
+ 'Surge',
+ {
+ type: 'wireguard',
+ name: 'Surge WG',
+ server: 'wg.example.com',
+ port: 51820,
+ 'private-key': 'private-key',
+ 'public-key': 'public-key',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ },
+ { 'include-unsupported-proxy': true },
+ );
+
+ expect(output).to.include('# > WireGuard Proxy Surge WG');
+ expect(output).to.include('self-ip = 10.0.0.2');
+ expect(output).to.include('self-ip-v6 = fd00::2');
+ expect(output).to.not.include('ip-cidr');
+ expect(output).to.not.include('ipv6-cidr');
+ });
+
+ it('produces Surfboard trojan websocket lines', function () {
+ const output = produceExternal('Surfboard', {
+ type: 'trojan',
+ name: 'Trojan',
+ server: 'trojan.example.com',
+ port: 443,
+ password: 'secret',
+ network: 'ws',
+ tls: true,
+ sni: 'sni.example.com',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ 'Trojan=trojan,trojan.example.com,443,password=secret,ws=true,ws-path=/ws,ws-headers=Host:"cdn.example.com",tls=true,sni="sni.example.com"',
+ );
+ });
+
+ it('produces Surfboard Hysteria2 lines', function () {
+ const output = produceExternal('Surfboard', {
+ type: 'hysteria2',
+ name: 'Surfboard Hysteria2',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '8443,8445-8447',
+ 'hop-interval': 30,
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ down: '100 Mbps',
+ udp: true,
+ });
+
+ expect(output).to.equal(
+ 'Surfboard Hysteria2=hysteria2,hy2.example.com,443,password="secret",port-hopping="8443;8445-8447",port-hopping-interval=30,sni="peer.example.com",skip-cert-verify=true,download-bandwidth=100,udp-relay=true',
+ );
+ });
+
+ it('does not emit empty Surfboard Hysteria2 port hopping fields', function () {
+ const output = produceExternal('Surfboard', {
+ type: 'hysteria2',
+ name: 'Surfboard Hysteria2 Plain',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ ports: '',
+ 'hop-interval': ' ',
+ sni: 'peer.example.com',
+ 'skip-cert-verify': true,
+ });
+
+ expect(output).to.equal(
+ 'Surfboard Hysteria2 Plain=hysteria2,hy2.example.com,443,password="secret",sni="peer.example.com",skip-cert-verify=true',
+ );
+ });
+
+ it('omits Surfboard Hysteria2 lines when obfs is present', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'hysteria2',
+ name: 'Surfboard Hysteria2 Obfs',
+ server: 'hy2.example.com',
+ port: 443,
+ password: 'secret',
+ obfs: 'salamander',
+ 'obfs-password': 'mask',
+ },
+ ],
+ 'Surfboard',
+ 'external',
+ );
+
+ expect(output).to.equal('');
+ });
+
+ it('produces SurgeMac external lines', function () {
+ const output = produceExternal('SurgeMac', {
+ type: 'external',
+ name: 'External',
+ exec: '/usr/bin/ssh',
+ 'local-port': 1080,
+ args: ['-D', 'localhost:1080'],
+ addresses: ['1.1.1.1', '2001:db8::1'],
+ udp: true,
+ tfo: true,
+ 'test-url': 'https://test.example.com',
+ 'block-quic': 'on',
+ });
+
+ expect(output).to.equal(
+ 'External=external,exec="/usr/bin/ssh",local-port=1080,args="-D",args="localhost:1080",addresses=1.1.1.1,addresses=2001:db8::1,udp-relay=true,tfo=true,test-url=https://test.example.com,block-quic=on',
+ );
+ });
+
+ it('wraps unsupported SurgeMac protocols with Mihomo external mode when requested', function () {
+ const output = produceExternal(
+ 'SurgeMac',
+ {
+ type: 'vless',
+ name: 'Mihomo VLESS',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ },
+ { useMihomoExternal: true, localPort: 16666 },
+ );
+
+ expect(output).to.include(
+ 'Mihomo VLESS=external,exec="/usr/local/bin/mihomo",local-port=16666',
+ );
+ expect(output).to.include('args="-config"');
+ expect(output).to.include('udp-relay=true');
+ });
+
+ it('does not wrap malformed SurgeMac nodes with Mihomo external mode', function () {
+ const output = ProxyUtils.produce(
+ [
+ {
+ type: 'ss',
+ name: 'Broken Obfs',
+ server: 'ss.example.com',
+ port: 8388,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'obfs',
+ },
+ ],
+ 'SurgeMac',
+ 'external',
+ { useMihomoExternal: true, localPort: 16666 },
+ );
+
+ expect(output).to.equal('');
+ });
+
+ it('forces SurgeMac nodes with _mihomoExternal to use Mihomo external mode', function () {
+ const output = produceExternal(
+ 'SurgeMac',
+ {
+ type: 'tuic',
+ name: 'Forced Mihomo',
+ server: 'tuic.example.com',
+ port: 443,
+ uuid: UUID,
+ password: 'secret',
+ sni: 'sni.example.com',
+ _mihomoExternal: true,
+ },
+ { localPort: 17777 },
+ );
+
+ expect(output).to.include(
+ 'Forced Mihomo=external,exec="/usr/local/bin/mihomo",local-port=17777',
+ );
+ expect(output).to.not.include('Forced Mihomo=tuic-v5');
+ });
+
+ it('produces URI WireGuard links with stored and default CIDR suffixes', function () {
+ const output = produceExternal('URI', [
+ {
+ type: 'wireguard',
+ name: 'URI WG Explicit CIDR',
+ server: 'wg-explicit.example.com',
+ port: 51820,
+ 'private-key': 'private-key-1',
+ 'public-key': 'public-key-1',
+ ip: '10.0.0.2',
+ ipv6: 'fd00::2',
+ 'ip-cidr': 24,
+ 'ipv6-cidr': 64,
+ udp: true,
+ },
+ {
+ type: 'wireguard',
+ name: 'URI WG Default CIDR',
+ server: 'wg-default.example.com',
+ port: 51820,
+ 'private-key': 'private-key-2',
+ 'public-key': 'public-key-2',
+ ip: '10.0.0.3',
+ ipv6: 'fd00::3',
+ udp: true,
+ },
+ ]);
+
+ const [explicit, defaults] = output.split('\n');
+ expect(explicit).to.include('address=10.0.0.2%2F24%2Cfd00%3A%3A2%2F64');
+ expect(defaults).to.include(
+ 'address=10.0.0.3%2F32%2Cfd00%3A%3A3%2F128',
+ );
+ expect(explicit).to.not.include('ip-cidr=');
+ expect(explicit).to.not.include('ipv6-cidr=');
+ expect(defaults).to.not.include('ip-cidr=');
+ expect(defaults).to.not.include('ipv6-cidr=');
+ });
+
+ it('produces URI shadowsocks links with v2ray-plugin mux and tls flags', function () {
+ const plugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;tls;sni=sni.example.com;skip-cert-verify=true;mux=0',
+ );
+ const output = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS V2ray Flags',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ sni: 'sni.example.com',
+ 'skip-cert-verify': true,
+ mux: 0,
+ },
+ });
+
+ expect(output).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:secret',
+ )}@ss.example.com:443/?plugin=${plugin}#SS%20V2ray%20Flags`,
+ );
+ });
+
+ it('produces URI shadowsocks httpupgrade links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS Upgrade',
+ server: 'ss-upgrade.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+
+ expect(output).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:secret',
+ )}@ss-upgrade.example.com:443?sni=ss-upgrade.example.com&type=httpupgrade&path=%2Fupgrade%3Fa%3D1%26b%3D2%26ed%3D1024&host=upgrade.example.com&security=tls#SS%20Upgrade`,
+ );
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/upgrade?a=1&b=2');
+ expect(reparsed['ws-opts']['v2ray-http-upgrade-fast-open']).to.equal(
+ true,
+ );
+ expect(reparsed['ws-opts']['_v2ray-http-upgrade-ed']).to.equal('1024');
+ expect(reparsed['ws-opts']).to.not.have.property('max-early-data');
+ expect(reparsed['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('produces URI shadowsocks websocket links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS WS Early',
+ server: 'ss-ws.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+
+ expect(output).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:secret',
+ )}@ss-ws.example.com:443?sni=ss-ws.example.com&type=ws&path=%2Fws%3Fa%3D1%26b%3D2%26ed%3D2048&host=cdn.example.com&security=tls#SS%20WS%20Early`,
+ );
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsed['ws-opts']['max-early-data']).to.equal(2048);
+ expect(reparsed['ws-opts']['early-data-header-name']).to.equal(
+ 'Sec-WebSocket-Protocol',
+ );
+ expect(reparsed['ws-opts']).to.not.have.property('v2ray-http-upgrade');
+ });
+
+ it('does not serialize URI websocket early data for formats without custom header support', function () {
+ const proxies = [
+ {
+ type: 'ss',
+ name: 'SS WS Custom Early',
+ server: 'ss-ws.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Data',
+ },
+ },
+ {
+ type: 'vmess',
+ name: 'VMess WS Custom Early',
+ server: 'vmess-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Data',
+ },
+ },
+ {
+ type: 'trojan',
+ name: 'Trojan WS Custom Early',
+ server: 'trojan-ws.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Data',
+ },
+ },
+ ];
+
+ for (const proxy of proxies) {
+ const reparsed = ProxyUtils.parse(produceExternal('URI', proxy))[0];
+
+ expect(reparsed['ws-opts'].path, proxy.type).to.equal(
+ '/ws?a=1&b=2',
+ );
+ expect(reparsed['ws-opts'], proxy.type).to.not.have.property(
+ 'max-early-data',
+ );
+ expect(reparsed['ws-opts'], proxy.type).to.not.have.property(
+ 'early-data-header-name',
+ );
+ }
+ });
+
+ it('normalizes boolean v2ray-plugin mux values to integers in URI links', function () {
+ const muxOnPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;tls;mux=1',
+ );
+ const muxOffPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;mux=0',
+ );
+
+ const muxOnOutput = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS Boolean Mux On',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ tls: true,
+ mux: true,
+ },
+ });
+ const muxOffOutput = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS Boolean Mux Off',
+ server: 'ss.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ plugin: 'v2ray-plugin',
+ 'plugin-opts': {
+ mode: 'websocket',
+ host: 'cdn.example.com',
+ path: '/socket',
+ mux: false,
+ },
+ });
+
+ expect(muxOnOutput).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:secret',
+ )}@ss.example.com:443/?plugin=${muxOnPlugin}#SS%20Boolean%20Mux%20On`,
+ );
+ expect(muxOffOutput).to.equal(
+ `ss://${Base64.encode(
+ 'aes-128-gcm:secret',
+ )}@ss.example.com:443/?plugin=${muxOffPlugin}#SS%20Boolean%20Mux%20Off`,
+ );
+ });
+
+ it('round-trips Clash-style boolean v2ray-plugin mux flags into URI links', function () {
+ const proxies = ProxyUtils.parse(`proxies:
+ - name: Clash Boolean Mux On
+ type: ss
+ server: ss.example.com
+ port: 443
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: true
+ - name: Clash Boolean Mux Off
+ type: ss
+ server: ss.example.com
+ port: 443
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ mux: false
+`);
+
+ const output = produceExternal('URI', proxies);
+ const userInfo = Base64.encode('aes-128-gcm:secret');
+ const muxOnPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;tls;mux=1',
+ );
+ const muxOffPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;mux=0',
+ );
+
+ expect(output).to.equal(
+ [
+ `ss://${userInfo}@ss.example.com:443/?plugin=${muxOnPlugin}#Clash%20Boolean%20Mux%20On`,
+ `ss://${userInfo}@ss.example.com:443/?plugin=${muxOffPlugin}#Clash%20Boolean%20Mux%20Off`,
+ ].join('\n'),
+ );
+ });
+
+ it('round-trips Clash-style string boolean v2ray-plugin mux flags into URI links', function () {
+ const proxies = ProxyUtils.parse(`proxies:
+ - name: Clash String Mux On
+ type: ss
+ server: ss.example.com
+ port: 443
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ tls: true
+ mux: ' TRUE '
+ - name: Clash String Mux Off
+ type: ss
+ server: ss.example.com
+ port: 443
+ cipher: aes-128-gcm
+ password: secret
+ plugin: v2ray-plugin
+ plugin-opts:
+ mode: websocket
+ host: cdn.example.com
+ path: /socket
+ mux: ' false '
+`);
+
+ const output = produceExternal('URI', proxies);
+ const userInfo = Base64.encode('aes-128-gcm:secret');
+ const muxOnPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;tls;mux=1',
+ );
+ const muxOffPlugin = encodeURIComponent(
+ 'v2ray-plugin;obfs=websocket;mode=websocket;obfs-host=cdn.example.com;host=cdn.example.com;path=/socket;mux=0',
+ );
+
+ expect(output).to.equal(
+ [
+ `ss://${userInfo}@ss.example.com:443/?plugin=${muxOnPlugin}#Clash%20String%20Mux%20On`,
+ `ss://${userInfo}@ss.example.com:443/?plugin=${muxOffPlugin}#Clash%20String%20Mux%20Off`,
+ ].join('\n'),
+ );
+ });
+
+ it('produces URI VLESS reality websocket links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ '_spider-x': '/spider',
+ },
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2'],
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=reality&type=ws&path=%2Fws&host=cdn.example.com&ed=2048&alpn=h2&sni=sni.example.com&fp=chrome&flow=xtls-rprx-vision&sid=08&spx=%2Fspider&pbk=pubkey#URI%20Reality`,
+ );
+ });
+
+ it('produces URI VLESS websocket links with packet encoding and custom early data headers', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS Early',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ udp: true,
+ 'packet-addr': true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'X-Data',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&ed=2048&eh=X-Data&packetEncoding=packet&sni=sni.example.com#URI%20WS%20Early`,
+ );
+ });
+
+ it('replaces stale URI VLESS websocket path early data with max-early-data', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS Conflict',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=999&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws%3Fa%3D1%26b%3D2&host=cdn.example.com&ed=2048#URI%20WS%20Conflict`,
+ );
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsed['ws-opts']['max-early-data']).to.equal(2048);
+ });
+
+ it('produces URI VLESS websocket links with pcs from tls fingerprint', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS PCS',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'tls-fingerprint': 'fingerprint',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&pcs=fingerprint&sni=sni.example.com#URI%20WS%20PCS`,
+ );
+ });
+
+ it('produces URI VLESS links with ech from mihomo ech opts config', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS ECH',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ _echConfigList: 'STALE',
+ 'ech-opts': {
+ enable: true,
+ config: 'ECHCONFIG',
+ },
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&ech=ECHCONFIG&sni=sni.example.com#URI%20WS%20ECH`,
+ );
+ });
+
+ it('matches mihomo ECH enable decoding when producing URI VLESS links', function () {
+ const baseProxy = {
+ type: 'vless',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ _echConfigList: 'FALLBACK',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ };
+ const outputWithNumericEnable = produceExternal('URI', {
+ ...baseProxy,
+ name: 'URI WS ECH Numeric',
+ 'ech-opts': {
+ enable: 1,
+ config: 'ECHCONFIG',
+ },
+ });
+ const outputWithoutEnable = produceExternal('URI', {
+ ...baseProxy,
+ name: 'URI WS ECH Missing Enable',
+ 'ech-opts': {
+ config: 'ECHCONFIG',
+ },
+ });
+ const outputWithStringEnable = produceExternal('URI', {
+ ...baseProxy,
+ name: 'URI WS ECH String Enable',
+ 'ech-opts': {
+ enable: 'true',
+ config: 'ECHCONFIG',
+ },
+ });
+
+ expect(outputWithNumericEnable).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&ech=ECHCONFIG&sni=sni.example.com#URI%20WS%20ECH%20Numeric`,
+ );
+ expect(outputWithoutEnable).to.not.include('&ech=');
+ expect(outputWithStringEnable).to.not.include('&ech=');
+ });
+
+ it('produces URI VLESS links with ech DNS from mihomo sidecar fields', function () {
+ const echConfigList = 'ech.example.com+https://1.1.1.1/dns-query';
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS ECH DNS',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'ech.example.com',
+ },
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&ech=${encodeURIComponent(
+ echConfigList,
+ )}&sni=sni.example.com#URI%20WS%20ECH%20DNS`,
+ );
+ });
+
+ it('uses default ECH DNS and warns when URI VLESS ech opts only set query server name', function () {
+ const echConfigList =
+ 'ech.example.com+https://dns.alidns.com/dns-query';
+ const { result: output, warnings } = captureWarns(() =>
+ produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS ECH Default DNS',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ 'ech-opts': {
+ enable: true,
+ 'query-server-name': 'ech.example.com',
+ },
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ }),
+ );
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=tls&type=ws&path=%2Fws&host=cdn.example.com&ech=${encodeURIComponent(
+ echConfigList,
+ )}&sni=sni.example.com#URI%20WS%20ECH%20Default%20DNS`,
+ );
+ expect(warnings).to.have.length(1);
+ expect(warnings[0]).to.include('https://dns.alidns.com/dns-query');
+ expect(warnings[0]).to.include('ech-opts._dns');
+ });
+
+ it('produces URI VLESS fake-http links with method and headerType', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI HTTP',
+ server: 'vless-http.example.com',
+ port: 80,
+ uuid: UUID,
+ network: 'http',
+ udp: true,
+ 'http-opts': {
+ path: ['/edge'],
+ method: 'GET',
+ headers: {
+ Host: ['http.example.com'],
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-http.example.com:80?security=none&type=tcp&headerType=http&path=%2Fedge&host=http.example.com&method=GET&packetEncoding=none#URI%20HTTP`,
+ );
+ });
+
+ it('produces URI VLESS httpupgrade links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ 'early-data-header-name': 'X-Upgrade',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fed%3D1024&host=upgrade.example.com&eh=X-Upgrade#URI%20Upgrade`,
+ );
+ });
+
+ it('defaults URI VLESS httpupgrade path early data without reusing websocket early data', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade Default',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ 'max-early-data': 1024,
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fed%3D2560&host=upgrade.example.com#URI%20Upgrade%20Default`,
+ );
+ });
+
+ it('keeps URI VLESS httpupgrade early data from path when metadata is absent', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade Existing Path Early',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fa%3D1%26b%3D2%26ed%3D1024&host=upgrade.example.com#URI%20Upgrade%20Existing%20Path%20Early`,
+ );
+ });
+
+ it('adds URI VLESS httpupgrade early data to paths with existing query params', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade Query',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fa%3D1%26b%3D2%26ed%3D1024&host=upgrade.example.com#URI%20Upgrade%20Query`,
+ );
+ });
+
+ it('replaces invalid URI VLESS httpupgrade path early data', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade Invalid Query',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?ed=abc&x=1',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fx%3D1%26ed%3D1024&host=upgrade.example.com#URI%20Upgrade%20Invalid%20Query`,
+ );
+ });
+
+ it('does not serialize invalid URI early data values', function () {
+ const invalidVlessWsOutput = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI WS Invalid Early',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': '999999999999999999999',
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ const invalidSsWsOutput = produceExternal('URI', {
+ type: 'ss',
+ name: 'SS WS Invalid Early',
+ server: 'ss-ws.example.com',
+ port: 443,
+ cipher: 'aes-128-gcm',
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&ed=1024&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 'abc',
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ const invalidHttpUpgradeOutput = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI Upgrade Invalid Metadata',
+ server: 'vless-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?ed=abc',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': 'abc',
+ },
+ });
+
+ const reparsedVlessWs = ProxyUtils.parse(invalidVlessWsOutput)[0];
+ const reparsedSsWs = ProxyUtils.parse(invalidSsWsOutput)[0];
+
+ expect(reparsedVlessWs['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsedVlessWs['ws-opts']).to.not.have.property(
+ 'max-early-data',
+ );
+ expect(reparsedSsWs['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsedSsWs['ws-opts']).to.not.have.property('max-early-data');
+ expect(invalidHttpUpgradeOutput).to.equal(
+ `vless://${UUID}@vless-upgrade.example.com:443?security=tls&type=httpupgrade&path=%2Fupgrade%3Fed%3D2560&host=upgrade.example.com#URI%20Upgrade%20Invalid%20Metadata`,
+ );
+ });
+
+ it('produces URI VMess httpupgrade links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'vmess',
+ name: 'URI VMess Upgrade',
+ server: 'vmess-upgrade.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+ const payload = JSON.parse(
+ Base64.decode(output.replace(/^vmess:\/\//, '')),
+ );
+
+ expect(payload.net).to.equal('httpupgrade');
+ expect(payload.path).to.equal('/upgrade?a=1&b=2&ed=1024');
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/upgrade?a=1&b=2');
+ expect(reparsed['ws-opts']['v2ray-http-upgrade-fast-open']).to.equal(
+ true,
+ );
+ expect(reparsed['ws-opts']['_v2ray-http-upgrade-ed']).to.equal('1024');
+ expect(reparsed['ws-opts']).to.not.have.property('max-early-data');
+ expect(reparsed['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('produces URI VMess websocket links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'vmess',
+ name: 'URI VMess WS Early',
+ server: 'vmess-ws.example.com',
+ port: 443,
+ uuid: UUID,
+ cipher: 'auto',
+ alterId: 0,
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+ const payload = JSON.parse(
+ Base64.decode(output.replace(/^vmess:\/\//, '')),
+ );
+
+ expect(payload.net).to.equal('ws');
+ expect(payload.path).to.equal('/ws?a=1&b=2&ed=2048');
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsed['ws-opts']['max-early-data']).to.equal(2048);
+ expect(reparsed['ws-opts']['early-data-header-name']).to.equal(
+ 'Sec-WebSocket-Protocol',
+ );
+ });
+
+ it('produces URI VLESS h2 links using share-link http transport type', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI H2',
+ server: 'vless-h2.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ udp: true,
+ network: 'h2',
+ _h2: true,
+ 'h2-opts': {
+ path: '/h2',
+ headers: {
+ host: ['h2.example.com'],
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-h2.example.com:443?security=tls&type=http&path=%2Fh2&host=h2.example.com&packetEncoding=none&h2=1#URI%20H2`,
+ );
+ });
+
+ it('produces URI VLESS xhttp links with mihomo transport mode', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ mode: 'stream-up',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP`,
+ );
+ });
+
+ it('preserves _extra_unsupported without letting it override structured xhttp fields when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Unsupported Extra',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ _extra_unsupported: {
+ customField: 'keep-me',
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ address: 'old.example.com',
+ },
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'no-grpc-header': true,
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.customField).to.equal('keep-me');
+ expect(extra.noGRPCHeader).to.equal(true);
+ expect(extra.downloadSettings?.address).to.equal(
+ 'download.example.com',
+ );
+ expect(extra.downloadSettings?.port).to.equal(8443);
+ expect(extra.downloadSettings?.network).to.equal('xhttp');
+ expect(extra.downloadSettings?.security).to.equal('tls');
+ expect(extra.downloadSettings?.sockopt).to.deep.equal({
+ mark: 255,
+ });
+ expect(extra.downloadSettings?.xhttpSettings?.path).to.equal(
+ '/download',
+ );
+ expect(extra.downloadSettings?.xhttpSettings?.host).to.equal(
+ 'download-host.example.com',
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.server,
+ ).to.equal('download.example.com');
+ expect(reparsed[0]._extra_unsupported).to.deep.equal({
+ customField: 'keep-me',
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ });
+
+ it('uses string _extra as the final xhttp URI extra without rebuilding it', function () {
+ const rawExtra = JSON.stringify({
+ customField: 'keep-me',
+ scMinPostsIntervalMs: '0-300',
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Raw Extra String',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ _extra: rawExtra,
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-min-posts-interval-ms': 300,
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ expect(decodeURIComponent(encodedExtra)).to.equal(rawExtra);
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal('0-300');
+ });
+
+ it('stringifies plain-object _extra before using it as the final xhttp URI extra', function () {
+ const rawExtra = {
+ customField: 'keep-me',
+ scMinPostsIntervalMs: '0-300',
+ };
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Raw Extra Object',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ _extra: rawExtra,
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-min-posts-interval-ms': 300,
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ expect(JSON.parse(decodeURIComponent(encodedExtra))).to.deep.equal(
+ rawExtra,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal('0-300');
+ });
+
+ it('prefers explicit _extra over _extra_unsupported when both are present', function () {
+ const rawExtra = JSON.stringify({
+ customField: 'raw-wins',
+ scMinPostsIntervalMs: '0-300',
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Raw Extra Wins',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ _extra: rawExtra,
+ _extra_unsupported: {
+ customField: 'stale-sidecar',
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-min-posts-interval-ms': 300,
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ expect(decodeURIComponent(encodedExtra)).to.equal(rawExtra);
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal('0-300');
+ expect(reparsed[0]._extra_unsupported).to.deep.equal({
+ customField: 'raw-wins',
+ });
+ });
+
+ it('keeps structured xhttp scalar normalization even if _extra_unsupported carries conflicting raw fields', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Unsupported Extra Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ _extra_unsupported: {
+ customField: 'keep-me',
+ scMaxEachPostBytes: '000-1000000',
+ scMinPostsIntervalMs: '0300',
+ uplinkChunkSize: '00064 - 00128',
+ downloadSettings: {
+ xhttpSettings: {
+ scMaxEachPostBytes: '000-1000000',
+ scMinPostsIntervalMs: '000-300',
+ uplinkChunkSize: '00048',
+ },
+ },
+ },
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'uplink-chunk-size': '00064 - 00128',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'uplink-chunk-size': '00048',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.customField).to.equal('keep-me');
+ expect(extra.scMaxEachPostBytes).to.equal(1000000);
+ expect(extra.scMinPostsIntervalMs).to.equal(300);
+ expect(extra.uplinkChunkSize).to.equal('64-128');
+ expect(
+ extra.downloadSettings?.xhttpSettings?.scMaxEachPostBytes,
+ ).to.equal(1000000);
+ expect(
+ extra.downloadSettings?.xhttpSettings?.scMinPostsIntervalMs,
+ ).to.equal(300);
+ expect(extra.downloadSettings?.xhttpSettings?.uplinkChunkSize).to.equal(
+ 48,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['sc-max-each-post-bytes']).to.equal(
+ 1000000,
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal(300);
+ expect(reparsed[0]['xhttp-opts']?.['uplink-chunk-size']).to.equal(
+ '64-128',
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'uplink-chunk-size'
+ ],
+ ).to.equal(48);
+ expect(reparsed[0]._extra_unsupported).to.deep.equal({
+ customField: 'keep-me',
+ });
+ });
+
+ it('round-trips unsupported xhttp extra sidecar values without dropping them', function () {
+ const rawExtra = JSON.stringify({
+ uplinkChunkSize: 'fast',
+ xmux: {
+ hKeepAlivePeriod: '9007199254740993',
+ },
+ downloadSettings: {
+ address: 'download.example.com',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ alpn: ['h2', { foo: 1 }],
+ },
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const [parsed] = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Unsupported%20Roundtrip`,
+ );
+ const output = produceExternal('URI', parsed);
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.uplinkChunkSize).to.equal('fast');
+ expect(extra.xmux).to.deep.equal({
+ hKeepAlivePeriod: '9007199254740993',
+ });
+ expect(extra.downloadSettings?.address).to.equal(
+ 'download.example.com',
+ );
+ expect(extra.downloadSettings?.port).to.equal(8443);
+ expect(extra.downloadSettings?.network).to.equal('xhttp');
+ expect(extra.downloadSettings?.security).to.equal('tls');
+ expect(extra.downloadSettings?.tlsSettings?.alpn).to.deep.equal([
+ 'h2',
+ { foo: 1 },
+ ]);
+ expect(extra.downloadSettings?.xhttpSettings?.path).to.equal(
+ '/download',
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.network,
+ ).to.equal('xhttp');
+ expect(reparsed[0]).to.have.property('_extra_unsupported');
+ expect(reparsed[0]._extra_unsupported).to.deep.equal({
+ uplinkChunkSize: 'fast',
+ xmux: {
+ hKeepAlivePeriod: '9007199254740993',
+ },
+ downloadSettings: {
+ tlsSettings: {
+ alpn: ['h2', { foo: 1 }],
+ },
+ },
+ });
+ });
+
+ it('round-trips unsupported-only nested download settings with structured xhttp network', function () {
+ const rawExtra = JSON.stringify({
+ downloadSettings: {
+ network: 'xhttp',
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ const [parsed] = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Download%20Unsupported%20Only`,
+ );
+ expect(parsed['xhttp-opts']?.['download-settings']).to.deep.equal({
+ network: 'xhttp',
+ });
+ expect(parsed._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+
+ const output = produceExternal('URI', parsed);
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings).to.deep.equal({
+ network: 'xhttp',
+ xhttpSettings: {
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ mode: 'stream-up',
+ },
+ sockopt: {
+ mark: 255,
+ },
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']).to.deep.equal({
+ network: 'xhttp',
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ mode: 'stream-up',
+ });
+ expect(reparsed[0]._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ });
+
+ it('keeps structured nested xhttp network out of mixed download settings sidecars', function () {
+ const rawExtra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ },
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+ const [parsed] = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Download%20Mixed%20Sidecar`,
+ );
+
+ expect(parsed['xhttp-opts']?.['download-settings']).to.deep.equal({
+ network: 'xhttp',
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ });
+ expect(parsed._extra_unsupported).to.deep.equal({
+ downloadSettings: {
+ sockopt: {
+ mark: 255,
+ },
+ },
+ });
+
+ delete parsed['xhttp-opts']['download-settings'];
+
+ const output = produceExternal('URI', parsed);
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings).to.deep.equal({
+ sockopt: {
+ mark: 255,
+ },
+ });
+ });
+
+ it('round-trips invalid xhttp extra strings through _extra', function () {
+ const rawExtra = '{bad';
+ const [parsed] = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Invalid%20Extra`,
+ );
+
+ expect(parsed._extra).to.equal(rawExtra);
+
+ const output = produceExternal('URI', parsed);
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ expect(decodeURIComponent(encodedExtra)).to.equal(rawExtra);
+ });
+
+ it('normalizes structured xhttp xmux values when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP XMUX',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'reuse-settings': {
+ 'max-connections': '+0008',
+ 'max-concurrency': '0008-0016',
+ 'h-keep-alive-period': '+15',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ 'reuse-settings': {
+ 'h-max-request-times': '+0004-0008',
+ 'h-keep-alive-period': -1,
+ },
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.xmux).to.deep.equal({
+ maxConnections: '8',
+ maxConcurrency: '8-16',
+ hKeepAlivePeriod: 15,
+ });
+ expect(
+ extra.downloadSettings?.xhttpSettings?.extra?.xmux,
+ ).to.deep.equal({
+ hMaxRequestTimes: '4-8',
+ hKeepAlivePeriod: -1,
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['reuse-settings']).to.deep.equal({
+ 'max-connections': '8',
+ 'max-concurrency': '8-16',
+ 'h-keep-alive-period': 15,
+ });
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'reuse-settings'
+ ],
+ ).to.deep.equal({
+ 'h-max-request-times': '4-8',
+ 'h-keep-alive-period': -1,
+ });
+ });
+
+ it('produces URI VLESS xhttp links with structured scMinPostsIntervalMs', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: 300,
+ xmux: {
+ maxConnections: '8',
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Min Interval',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-connections': '8',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Min%20Interval`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal(300);
+ });
+
+ it('produces URI VLESS xhttp links with extended structured extra fields', function () {
+ const extra = JSON.stringify({
+ headers: {
+ 'X-Test': 'demo',
+ },
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ xPaddingObfsMode: true,
+ xPaddingKey: 'x_padding',
+ xPaddingHeader: 'Referer',
+ xPaddingPlacement: 'header',
+ xPaddingMethod: 'tokenish',
+ uplinkHTTPMethod: 'PUT',
+ sessionPlacement: 'query',
+ sessionKey: 'x_session_id',
+ seqPlacement: 'header',
+ seqKey: 'X-Seq',
+ uplinkDataPlacement: 'header',
+ uplinkDataKey: 'X-Data',
+ uplinkChunkSize: '64-128',
+ xmux: {
+ maxConcurrency: '16-32',
+ hKeepAlivePeriod: 15,
+ },
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ allowInsecure: true,
+ alpn: ['h2'],
+ echConfigList: 'ECHCONFIG',
+ },
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ headers: {
+ 'X-Download': '1',
+ },
+ noGRPCHeader: true,
+ xPaddingBytes: '16-32',
+ xPaddingObfsMode: true,
+ xPaddingKey: 'x_padding_dl',
+ xPaddingHeader: 'Cookie',
+ xPaddingPlacement: 'query',
+ xPaddingMethod: 'repeat-x',
+ uplinkHTTPMethod: 'PATCH',
+ sessionPlacement: 'header',
+ sessionKey: 'X-Session',
+ seqPlacement: 'query',
+ seqKey: 'x_seq',
+ uplinkDataPlacement: 'cookie',
+ uplinkDataKey: 'x_data',
+ uplinkChunkSize: 48,
+ extra: {
+ xmux: {
+ maxConcurrency: '8-16',
+ hKeepAlivePeriod: -1,
+ },
+ },
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Extended',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ 'X-Test': 'demo',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'x-padding-obfs-mode': true,
+ 'x-padding-key': 'x_padding',
+ 'x-padding-header': 'Referer',
+ 'x-padding-placement': 'header',
+ 'x-padding-method': 'tokenish',
+ 'uplink-http-method': 'PUT',
+ 'session-placement': 'query',
+ 'session-key': 'x_session_id',
+ 'seq-placement': 'header',
+ 'seq-key': 'X-Seq',
+ 'uplink-data-placement': 'header',
+ 'uplink-data-key': 'X-Data',
+ 'uplink-chunk-size': '64-128',
+ 'reuse-settings': {
+ 'max-concurrency': '16-32',
+ 'h-keep-alive-period': 15,
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'client-fingerprint': 'chrome',
+ 'skip-cert-verify': true,
+ alpn: ['h2'],
+ 'ech-opts': {
+ enable: true,
+ config: 'ECHCONFIG',
+ },
+ path: '/download',
+ host: 'download-host.example.com',
+ headers: {
+ 'X-Download': '1',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '16-32',
+ 'x-padding-obfs-mode': true,
+ 'x-padding-key': 'x_padding_dl',
+ 'x-padding-header': 'Cookie',
+ 'x-padding-placement': 'query',
+ 'x-padding-method': 'repeat-x',
+ 'uplink-http-method': 'PATCH',
+ 'session-placement': 'header',
+ 'session-key': 'X-Session',
+ 'seq-placement': 'query',
+ 'seq-key': 'x_seq',
+ 'uplink-data-placement': 'cookie',
+ 'uplink-data-key': 'x_data',
+ 'uplink-chunk-size': 48,
+ 'reuse-settings': {
+ 'max-concurrency': '8-16',
+ 'h-keep-alive-period': -1,
+ },
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Extended`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.headers?.['X-Test']).to.equal('demo');
+ expect(
+ reparsed[0]['xhttp-opts']?.['reuse-settings']?.[
+ 'h-keep-alive-period'
+ ],
+ ).to.equal(15);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.headers?.[
+ 'X-Download'
+ ],
+ ).to.equal('1');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'uplink-chunk-size'
+ ],
+ ).to.equal(48);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'reuse-settings'
+ ]?.['h-keep-alive-period'],
+ ).to.equal(-1);
+ });
+
+ it('normalizes Mihomo leading-zero structured xhttp scalars when producing URI VLESS links', function () {
+ const extra = JSON.stringify({
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: 300,
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: '0-300',
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Leading Zero Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-max-each-post-bytes': '000-1000000',
+ 'sc-min-posts-interval-ms': '0300',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-max-each-post-bytes': '000-1000000',
+ 'sc-min-posts-interval-ms': '000-300',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Leading%20Zero%20Scalars`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['sc-max-each-post-bytes']).to.equal(
+ 1000000,
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal(300);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-max-each-post-bytes'
+ ],
+ ).to.equal(1000000);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-min-posts-interval-ms'
+ ],
+ ).to.equal('0-300');
+ });
+
+ it('normalizes Mihomo explicit-plus structured xhttp scalars when producing URI VLESS links', function () {
+ const extra = JSON.stringify({
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: 300,
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: '0-300',
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Explicit Plus Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-max-each-post-bytes': '+500000-+1000000',
+ 'sc-min-posts-interval-ms': '+300',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-max-each-post-bytes': '+0-+1000000',
+ 'sc-min-posts-interval-ms': '+0-+300',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Explicit%20Plus%20Scalars`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['sc-max-each-post-bytes']).to.equal(
+ 1000000,
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal(300);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-max-each-post-bytes'
+ ],
+ ).to.equal(1000000);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-min-posts-interval-ms'
+ ],
+ ).to.equal('0-300');
+ });
+
+ it('produces URI VLESS xhttp links from structured download settings', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ xmux: {
+ maxConnections: '8',
+ },
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ tlsSettings: {
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ alpn: ['h2', 'http/1.1'],
+ },
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ noGRPCHeader: true,
+ xPaddingBytes: '32-64',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: 300,
+ extra: {
+ xmux: {
+ maxConcurrency: '16-32',
+ },
+ },
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Download',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'reuse-settings': {
+ 'max-connections': '8',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'client-fingerprint': 'chrome',
+ alpn: ['h2', 'http/1.1'],
+ path: '/download',
+ host: 'download-host.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '32-64',
+ 'sc-max-each-post-bytes': 1000000,
+ 'sc-min-posts-interval-ms': 300,
+ 'reuse-settings': {
+ 'max-concurrency': '16-32',
+ },
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Download`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-max-each-post-bytes'
+ ],
+ ).to.equal(1000000);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-min-posts-interval-ms'
+ ],
+ ).to.equal(300);
+ });
+
+ it('normalizes structured xhttp uplinkChunkSize values when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Uplink Chunk Size',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'uplink-chunk-size': '00064 - 00128',
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'uplink-chunk-size': '00048',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.uplinkChunkSize).to.equal('64-128');
+ expect(extra.downloadSettings?.xhttpSettings?.uplinkChunkSize).to.equal(
+ 48,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['uplink-chunk-size']).to.equal(
+ '64-128',
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'uplink-chunk-size'
+ ],
+ ).to.equal(48);
+ });
+
+ it('preserves reality download settings TLS extras when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Reality Download',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ servername: 'download-sni.example.com',
+ 'client-fingerprint': 'chrome',
+ 'skip-cert-verify': true,
+ alpn: ['h2', 'http/1.1'],
+ 'ech-opts': {
+ enable: true,
+ config: 'ECHCONFIG',
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ path: '/download',
+ host: 'download-host.example.com',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.network).to.equal('xhttp');
+ expect(extra.downloadSettings?.security).to.equal('reality');
+ expect(extra.downloadSettings?.tlsSettings).to.deep.equal({
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ allowInsecure: true,
+ alpn: ['h2', 'http/1.1'],
+ echConfigList: 'ECHCONFIG',
+ });
+ expect(extra.downloadSettings?.realitySettings).to.deep.equal({
+ serverName: 'download-sni.example.com',
+ fingerprint: 'chrome',
+ publicKey: 'pubkey',
+ shortId: '08',
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.servername,
+ ).to.equal('download-sni.example.com');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'client-fingerprint'
+ ],
+ ).to.equal('chrome');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'skip-cert-verify'
+ ],
+ ).to.equal(true);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.alpn,
+ ).to.deep.equal(['h2', 'http/1.1']);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.['ech-opts']
+ ?.config,
+ ).to.equal('ECHCONFIG');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.['reality-opts'],
+ ).to.deep.equal({
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ });
+ });
+
+ it('produces nested xhttp download TLS ECH DNS fields from mihomo sidecar fields', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Nested ECH DNS',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'download-ech.example.com',
+ '_force-query': 'full',
+ _sockopt: {
+ mark: 255,
+ },
+ },
+ path: '/download',
+ host: 'download-host.example.com',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.tlsSettings).to.deep.equal({
+ echConfigList: 'download-ech.example.com+https://1.1.1.1/dns-query',
+ echForceQuery: 'full',
+ echSockopt: {
+ mark: 255,
+ },
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.['ech-opts'],
+ ).to.deep.equal({
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ 'query-server-name': 'download-ech.example.com',
+ '_force-query': 'full',
+ _sockopt: {
+ mark: 255,
+ },
+ });
+ });
+
+ it('uses default ECH DNS and warns for nested xhttp download ECH query server name without DNS', function () {
+ const { result: output, warnings } = captureWarns(() =>
+ produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Nested ECH Default DNS',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ 'query-server-name': 'download-ech.example.com',
+ '_force-query': 'half',
+ },
+ path: '/download',
+ },
+ },
+ }),
+ );
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.tlsSettings).to.deep.equal({
+ echConfigList:
+ 'download-ech.example.com+https://dns.alidns.com/dns-query',
+ echForceQuery: 'half',
+ });
+ expect(warnings).to.have.length(1);
+ expect(warnings[0]).to.include('https://dns.alidns.com/dns-query');
+ expect(warnings[0]).to.include(
+ 'xhttp-opts.download-settings.ech-opts._dns',
+ );
+ });
+
+ it('omits nested xhttp download TLS ECH extras without an ECH config list', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Nested ECH Sidecar Only',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ '_force-query': 'full',
+ _sockopt: {
+ mark: 255,
+ },
+ },
+ path: '/download',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.security).to.equal('tls');
+ expect(extra.downloadSettings).to.not.have.property('tlsSettings');
+ });
+
+ it('drops unsupported nested xhttp download TLS ECH force query values', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Nested ECH Invalid Force Query',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'ech-opts': {
+ enable: true,
+ _dns: 'https://1.1.1.1/dns-query',
+ '_force-query': 'invalid',
+ _sockopt: {
+ mark: 255,
+ },
+ },
+ path: '/download',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.tlsSettings).to.deep.equal({
+ echConfigList: 'https://1.1.1.1/dns-query',
+ echSockopt: {
+ mark: 255,
+ },
+ });
+ });
+
+ it('skips URI VLESS exports when nested reality download settings are missing public keys', function () {
+ const rawExtra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'reality',
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const parsed = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Invalid%20Reality%20Download`,
+ );
+
+ expect(parsed).to.have.length(1);
+ expect(
+ parsed[0]['xhttp-opts']?.['download-settings']?.['reality-opts'],
+ ).to.deep.equal({});
+
+ const output = ProxyUtils.produce(parsed, 'URI', 'external');
+ expect(output).to.equal('');
+ });
+
+ it('skips URI VLESS exports when outer reality is valid but nested download reality is missing public keys', function () {
+ const rawExtra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'reality',
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const parsed = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=reality&pbk=outer-pub&sid=01&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Mixed%20Reality`,
+ );
+
+ expect(parsed).to.have.length(1);
+ expect(parsed[0]['reality-opts']).to.deep.equal({
+ 'public-key': 'outer-pub',
+ 'short-id': '01',
+ });
+ expect(
+ parsed[0]['xhttp-opts']?.['download-settings']?.['reality-opts'],
+ ).to.deep.equal({});
+
+ const output = ProxyUtils.produce(parsed, 'URI', 'external');
+ expect(output).to.equal('');
+ });
+
+ it('skips URI VLESS exports when xhttp stream-one is combined with download-settings', function () {
+ const rawExtra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ },
+ },
+ });
+ const parsed = ProxyUtils.parse(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-one&extra=${encodeURIComponent(
+ rawExtra,
+ )}#URI%20XHTTP%20Stream%20One%20Download`,
+ );
+
+ expect(parsed).to.have.length(1);
+ expect(parsed[0]['xhttp-opts']?.mode).to.equal('stream-one');
+ expect(parsed[0]['xhttp-opts']?.['download-settings']).to.deep.include({
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ });
+
+ const output = ProxyUtils.produce(parsed, 'URI', 'external');
+ expect(output).to.equal('');
+ });
+
+ it('uses nested download Host header when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Download Host Header',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ headers: {
+ Host: 'download-host.example.com',
+ 'X-Download': '1',
+ },
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.xhttpSettings?.host).to.equal(
+ 'download-host.example.com',
+ );
+ expect(extra.downloadSettings?.xhttpSettings?.headers).to.deep.equal({
+ 'X-Download': '1',
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']?.host).to.equal(
+ 'download-host.example.com',
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.headers,
+ ).to.deep.equal({
+ 'X-Download': '1',
+ });
+ });
+
+ it('prefers explicit nested download host over nested download Host header when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Download Host Precedence',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ headers: {
+ Host: 'ignored-header.example.com',
+ 'X-Download': '1',
+ },
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings?.xhttpSettings?.host).to.equal(
+ 'download-host.example.com',
+ );
+ expect(extra.downloadSettings?.xhttpSettings?.headers).to.deep.equal({
+ 'X-Download': '1',
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']?.host).to.equal(
+ 'download-host.example.com',
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.headers,
+ ).to.deep.equal({
+ 'X-Download': '1',
+ });
+ });
+
+ it('normalizes structured xhttp range-form scalars when producing URI VLESS links', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: '0-300',
+ xmux: {
+ maxConnections: '8',
+ },
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ scMaxEachPostBytes: 1000000,
+ scMinPostsIntervalMs: '0-300',
+ extra: {
+ xmux: {
+ maxConcurrency: '16-32',
+ },
+ },
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Range Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': '0-1000000',
+ 'sc-min-posts-interval-ms': '0-300',
+ 'reuse-settings': {
+ 'max-connections': '8',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'sc-max-each-post-bytes': '500000 - 1000000',
+ 'sc-min-posts-interval-ms': '0 - 300',
+ 'reuse-settings': {
+ 'max-concurrency': '16-32',
+ },
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Range%20Scalars`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']?.['sc-max-each-post-bytes']).to.equal(
+ 1000000,
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['sc-min-posts-interval-ms'],
+ ).to.equal('0-300');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-max-each-post-bytes'
+ ],
+ ).to.equal(1000000);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings']?.[
+ 'sc-min-posts-interval-ms'
+ ],
+ ).to.equal('0-300');
+ });
+
+ it('does not truncate malformed nested download port strings when producing URI VLESS links', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Invalid Download Port',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: '8443foo',
+ tls: true,
+ path: '/download',
+ },
+ },
+ });
+
+ const [, encodedExtra] = output.match(/[?&]extra=([^#]+)/);
+ const extra = JSON.parse(decodeURIComponent(encodedExtra));
+ expect(extra.downloadSettings).to.not.have.property('port');
+ expect(extra.downloadSettings).to.deep.equal({
+ address: 'download.example.com',
+ network: 'xhttp',
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'cdn.example.com',
+ mode: 'stream-up',
+ },
+ });
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('port');
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']).to.deep.equal({
+ server: 'download.example.com',
+ network: 'xhttp',
+ tls: true,
+ path: '/download',
+ host: 'cdn.example.com',
+ mode: 'stream-up',
+ });
+ });
+
+ it('drops invalid structured xhttp scalars when producing URI VLESS links', function () {
+ const extra = JSON.stringify({
+ noGRPCHeader: true,
+ xPaddingBytes: '64-128',
+ xmux: {
+ maxConnections: '8',
+ },
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/download',
+ host: 'download-host.example.com',
+ mode: 'stream-up',
+ noGRPCHeader: true,
+ xPaddingBytes: '32-64',
+ extra: {
+ xmux: {
+ maxConcurrency: '16-32',
+ },
+ },
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Invalid Scalars',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '64-128',
+ 'sc-max-each-post-bytes': '9007199254740993',
+ 'sc-min-posts-interval-ms': 0,
+ 'uplink-chunk-size': '64-fast',
+ 'reuse-settings': {
+ 'max-connections': '8',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ path: '/download',
+ host: 'download-host.example.com',
+ 'no-grpc-header': true,
+ 'x-padding-bytes': '32-64',
+ 'sc-max-each-post-bytes': '1-9007199254740993',
+ 'sc-min-posts-interval-ms': 'fast',
+ 'uplink-chunk-size': 'fast',
+ 'reuse-settings': {
+ 'max-concurrency': '16-32',
+ },
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Invalid%20Scalars`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(reparsed[0]['xhttp-opts']).to.not.have.property(
+ 'sc-max-each-post-bytes',
+ );
+ expect(reparsed[0]['xhttp-opts']).to.not.have.property(
+ 'sc-min-posts-interval-ms',
+ );
+ expect(reparsed[0]['xhttp-opts']).to.not.have.property(
+ 'uplink-chunk-size',
+ );
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sc-max-each-post-bytes');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sc-min-posts-interval-ms');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('uplink-chunk-size');
+ });
+
+ it('omits top-level URI xhttp extra when only invalid structured scalars remain', function () {
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Invalid Scalars Only',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'sc-max-each-post-bytes': '9007199254740993',
+ 'sc-min-posts-interval-ms': 0,
+ 'uplink-chunk-size': 'fast',
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up#URI%20XHTTP%20Invalid%20Scalars%20Only`,
+ );
+ expect(output).to.not.include('&extra=');
+ });
+
+ it('omits nested xhttp download settings when only invalid structured scalars remain', function () {
+ const extra = JSON.stringify({
+ downloadSettings: {
+ address: 'download.example.com',
+ network: 'xhttp',
+ port: 8443,
+ security: 'tls',
+ xhttpSettings: {
+ path: '/xhttp',
+ host: 'cdn.example.com',
+ mode: 'stream-up',
+ },
+ },
+ });
+ const output = produceExternal('URI', {
+ type: 'vless',
+ name: 'URI XHTTP Invalid Download Scalars Only',
+ server: 'vless-xhttp.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ network: 'xhttp',
+ 'xhttp-opts': {
+ path: '/xhttp',
+ mode: 'stream-up',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'download-settings': {
+ server: 'download.example.com',
+ port: 8443,
+ tls: true,
+ 'sc-max-each-post-bytes': '1-9007199254740993',
+ 'sc-min-posts-interval-ms': 'fast',
+ 'uplink-chunk-size': 'fast',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ `vless://${UUID}@vless-xhttp.example.com:443?security=tls&type=xhttp&path=%2Fxhttp&host=cdn.example.com&sni=sni.example.com&mode=stream-up&extra=${encodeURIComponent(
+ extra,
+ )}#URI%20XHTTP%20Invalid%20Download%20Scalars%20Only`,
+ );
+
+ const reparsed = ProxyUtils.parse(output);
+ expect(reparsed, output).to.have.length(1);
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sc-max-each-post-bytes');
+ expect(
+ reparsed[0]['xhttp-opts']?.['download-settings'],
+ ).to.not.have.property('sc-min-posts-interval-ms');
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']?.path).to.equal(
+ '/xhttp',
+ );
+ expect(reparsed[0]['xhttp-opts']?.['download-settings']?.host).to.equal(
+ 'cdn.example.com',
+ );
+ });
+
+ it('produces URI Trojan websocket links with pcs from tls fingerprint', function () {
+ const output = produceExternal('URI', {
+ type: 'trojan',
+ name: 'URI Trojan PCS',
+ server: 'trojan.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ sni: 'sni.example.com',
+ 'tls-fingerprint': 'fingerprint',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ });
+
+ expect(output).to.equal(
+ 'trojan://secret@trojan.example.com:443?sni=sni.example.com&type=ws&path=%2Fws&host=cdn.example.com&pcs=fingerprint#URI%20Trojan%20PCS',
+ );
+ });
+
+ it('produces URI Trojan websocket links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'trojan',
+ name: 'URI Trojan WS Early',
+ server: 'trojan.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws?a=1&b=2',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ 'max-early-data': 2048,
+ 'early-data-header-name': 'Sec-WebSocket-Protocol',
+ },
+ });
+
+ expect(output).to.equal(
+ 'trojan://secret@trojan.example.com:443?sni=trojan.example.com&type=ws&path=%2Fws%3Fa%3D1%26b%3D2%26ed%3D2048&host=cdn.example.com#URI%20Trojan%20WS%20Early',
+ );
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/ws?a=1&b=2');
+ expect(reparsed['ws-opts']['max-early-data']).to.equal(2048);
+ expect(reparsed['ws-opts']['early-data-header-name']).to.equal(
+ 'Sec-WebSocket-Protocol',
+ );
+ });
+
+ it('produces URI Trojan httpupgrade links with early data metadata', function () {
+ const output = produceExternal('URI', {
+ type: 'trojan',
+ name: 'URI Trojan Upgrade',
+ server: 'trojan-upgrade.example.com',
+ port: 443,
+ password: 'secret',
+ tls: true,
+ network: 'ws',
+ 'ws-opts': {
+ path: '/upgrade?a=1&b=2',
+ headers: {
+ Host: 'upgrade.example.com',
+ },
+ 'v2ray-http-upgrade': true,
+ 'v2ray-http-upgrade-fast-open': true,
+ '_v2ray-http-upgrade-ed': '1024',
+ },
+ });
+
+ expect(output).to.equal(
+ 'trojan://secret@trojan-upgrade.example.com:443?sni=trojan-upgrade.example.com&type=httpupgrade&path=%2Fupgrade%3Fa%3D1%26b%3D2%26ed%3D1024&host=upgrade.example.com#URI%20Trojan%20Upgrade',
+ );
+
+ const reparsed = ProxyUtils.parse(output)[0];
+ expect(reparsed['ws-opts'].path).to.equal('/upgrade?a=1&b=2');
+ expect(reparsed['ws-opts']['v2ray-http-upgrade-fast-open']).to.equal(
+ true,
+ );
+ expect(reparsed['ws-opts']['_v2ray-http-upgrade-ed']).to.equal('1024');
+ expect(reparsed['ws-opts']).to.not.have.property('max-early-data');
+ expect(reparsed['ws-opts']).to.not.have.property(
+ 'early-data-header-name',
+ );
+ });
+
+ it('produces V2Ray exports as base64 encoded URI lists', function () {
+ const output = produceExternal('V2Ray', {
+ type: 'vless',
+ name: 'URI Reality',
+ server: 'vless.example.com',
+ port: 443,
+ uuid: UUID,
+ tls: true,
+ sni: 'sni.example.com',
+ flow: 'xtls-rprx-vision',
+ network: 'ws',
+ 'ws-opts': {
+ path: '/ws',
+ headers: {
+ Host: 'cdn.example.com',
+ },
+ },
+ 'reality-opts': {
+ 'public-key': 'pubkey',
+ 'short-id': '08',
+ },
+ });
+
+ expect(Base64.decode(output)).to.equal(
+ `vless://${UUID}@vless.example.com:443?security=reality&type=ws&path=%2Fws&host=cdn.example.com&sni=sni.example.com&flow=xtls-rprx-vision&sid=08&pbk=pubkey#URI%20Reality`,
+ );
+ });
+});
diff --git a/backend/src/test/restful/archive.spec.js b/backend/src/test/restful/archive.spec.js
new file mode 100644
index 0000000000..22c55c6ffe
--- /dev/null
+++ b/backend/src/test/restful/archive.spec.js
@@ -0,0 +1,546 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ ARTIFACTS_KEY,
+ COLLECTIONS_KEY,
+ FILES_KEY,
+ ARCHIVES_KEY,
+ SETTINGS_KEY,
+ SUBS_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+
+let $;
+let registerCollectionRoutes;
+let registerArtifactRoutes;
+let registerMiscRoutes;
+let registerArchiveRoutes;
+let registerSortingRoutes;
+let registerSubscriptionRoutes;
+let registerTokenRoutes;
+let originalError;
+let originalInfo;
+let originalRead;
+let originalWrite;
+let state;
+let tempDir;
+let previousDataBasePath;
+
+function createRouteApp() {
+ const handlers = new Map();
+ const methods = ['get', 'post', 'put', 'patch', 'delete', 'all'];
+
+ const app = {
+ handlers,
+ route(pattern) {
+ const chain = {};
+ methods
+ .filter((method) => method !== 'all')
+ .forEach((method) => {
+ chain[method] = (handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return chain;
+ };
+ });
+ return chain;
+ },
+ };
+
+ methods.forEach((method) => {
+ app[method] = (pattern, handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return app;
+ };
+ });
+
+ return app;
+}
+
+function getHandler(register, method, pattern) {
+ const app = createRouteApp();
+ register(app);
+ return app.handlers.get(`${method} ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ body: null,
+ headers: {},
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ sent: null,
+ statusCode: 200,
+ json(payload) {
+ this.body = payload;
+ return this;
+ },
+ send(payload) {
+ this.sent = payload;
+ return this;
+ },
+ set(key, value) {
+ this.headers[key] = value;
+ return this;
+ },
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ };
+}
+
+describe('archive routes', function () {
+ before(async function () {
+ previousDataBasePath = process.env.SUB_STORE_DATA_BASE_PATH;
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sub-store-archives-'));
+ process.env.SUB_STORE_DATA_BASE_PATH = tempDir;
+
+ ({ default: $ } = require('@/core/app'));
+ ({ default: registerSubscriptionRoutes } = require(
+ '@/restful/subscriptions'
+ ));
+ ({ default: registerCollectionRoutes } = require('@/restful/collections'));
+ ({ default: registerArtifactRoutes } = require('@/restful/artifacts'));
+ ({ default: registerTokenRoutes } = require('@/restful/token'));
+ ({ default: registerArchiveRoutes } = require('@/restful/archives'));
+ ({ default: registerSortingRoutes } = require('@/restful/sort'));
+ ({ default: registerMiscRoutes } = require('@/restful/miscs'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+
+ if (previousDataBasePath == null) {
+ delete process.env.SUB_STORE_DATA_BASE_PATH;
+ } else {
+ process.env.SUB_STORE_DATA_BASE_PATH = previousDataBasePath;
+ }
+
+ if (tempDir) {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SUBS_KEY]: [],
+ [COLLECTIONS_KEY]: [],
+ [FILES_KEY]: [],
+ [ARTIFACTS_KEY]: [],
+ [TOKENS_KEY]: [],
+ [ARCHIVES_KEY]: [],
+ [SETTINGS_KEY]: {},
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.error = () => {};
+ });
+
+ it('archives and restores subscriptions through the live delete route', async function () {
+ state[SUBS_KEY] = [
+ {
+ name: 'demo-sub',
+ displayName: 'Demo Sub',
+ source: 'remote',
+ tag: ['alpha'],
+ url: 'https://example.com/sub',
+ process: [],
+ },
+ ];
+ state[COLLECTIONS_KEY] = [
+ {
+ name: 'demo-collection',
+ subscriptions: ['demo-sub'],
+ },
+ ];
+
+ const deleteHandler = getHandler(
+ registerSubscriptionRoutes,
+ 'DELETE',
+ '/api/sub/:name',
+ );
+ const deleteRes = createResponse('/api/sub/:name');
+
+ deleteHandler(
+ {
+ params: { name: 'demo-sub' },
+ query: { mode: 'archive' },
+ },
+ deleteRes,
+ );
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(state[SUBS_KEY]).to.deep.equal([]);
+ expect(state[COLLECTIONS_KEY][0].subscriptions).to.deep.equal([]);
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+ expect(state[ARCHIVES_KEY][0].itemType).to.equal('sub');
+ expect(state[ARCHIVES_KEY][0].snapshot.name).to.equal('demo-sub');
+
+ const restoreHandler = getHandler(
+ registerArchiveRoutes,
+ 'POST',
+ '/api/archives/:id/restore',
+ );
+ const restoreRes = createResponse('/api/archives/:id/restore');
+
+ restoreHandler(
+ {
+ params: { id: state[ARCHIVES_KEY][0].id },
+ query: {},
+ },
+ restoreRes,
+ );
+
+ expect(restoreRes.body.status).to.equal('success');
+ expect(state[SUBS_KEY]).to.have.length(1);
+ expect(state[SUBS_KEY][0].name).to.equal('demo-sub');
+ expect(state[ARCHIVES_KEY]).to.deep.equal([]);
+ });
+
+ it('keeps archived entries when restore fails and preserves exact-datetime share tokens on success', async function () {
+ state[SUBS_KEY] = [
+ {
+ name: 'shared-sub',
+ source: 'remote',
+ process: [],
+ },
+ ];
+ const exactExp = Date.now() - 5 * 60 * 1000;
+ state[TOKENS_KEY] = [
+ {
+ token: 'keep-me',
+ type: 'sub',
+ name: 'shared-sub',
+ displayName: 'Shared Link',
+ mode: 'datetime',
+ exp: exactExp,
+ },
+ ];
+
+ const deleteHandler = getHandler(
+ registerTokenRoutes,
+ 'DELETE',
+ '/api/token/:token',
+ );
+ const deleteRes = createResponse('/api/token/:token');
+
+ deleteHandler(
+ {
+ params: { token: 'keep-me' },
+ query: {
+ mode: 'archive',
+ type: 'sub',
+ name: 'shared-sub',
+ },
+ },
+ deleteRes,
+ );
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+ expect(state[ARCHIVES_KEY][0].itemType).to.equal('share');
+ expect(state[ARCHIVES_KEY][0].snapshot.token).to.equal('keep-me');
+
+ const archivedId = state[ARCHIVES_KEY][0].id;
+ state[TOKENS_KEY].push({
+ token: 'keep-me',
+ type: 'sub',
+ name: 'shared-sub',
+ });
+
+ const restoreHandler = getHandler(
+ registerArchiveRoutes,
+ 'POST',
+ '/api/archives/:id/restore',
+ );
+ const failedRestoreRes = createResponse('/api/archives/:id/restore');
+
+ restoreHandler(
+ {
+ params: { id: archivedId },
+ query: {},
+ },
+ failedRestoreRes,
+ );
+
+ expect(failedRestoreRes.body.status).to.equal('failed');
+ expect(failedRestoreRes.statusCode).to.equal(400);
+ expect(failedRestoreRes.body.error.code).to.equal('DUPLICATE_TOKEN');
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+
+ state[TOKENS_KEY] = [];
+ const successfulRestoreRes = createResponse(
+ '/api/archives/:id/restore',
+ );
+
+ restoreHandler(
+ {
+ params: { id: archivedId },
+ query: {},
+ },
+ successfulRestoreRes,
+ );
+
+ expect(successfulRestoreRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.have.length(1);
+ expect(state[TOKENS_KEY][0].token).to.equal('keep-me');
+ expect(state[TOKENS_KEY][0].mode).to.equal('datetime');
+ expect(state[TOKENS_KEY][0].exp).to.equal(exactExp);
+ expect(state[TOKENS_KEY][0]).to.not.have.property('expiresIn');
+ expect(state[ARCHIVES_KEY]).to.deep.equal([]);
+ });
+
+ it('preserves legacy duration share tokens on restore', async function () {
+ state[SUBS_KEY] = [
+ {
+ name: 'shared-sub',
+ source: 'remote',
+ process: [],
+ },
+ ];
+ state[TOKENS_KEY] = [
+ {
+ token: 'legacy-duration',
+ type: 'sub',
+ name: 'shared-sub',
+ displayName: 'Legacy Share',
+ expiresIn: '1d',
+ },
+ ];
+
+ const deleteHandler = getHandler(
+ registerTokenRoutes,
+ 'DELETE',
+ '/api/token/:token',
+ );
+ const deleteRes = createResponse('/api/token/:token');
+
+ deleteHandler(
+ {
+ params: { token: 'legacy-duration' },
+ query: {
+ mode: 'archive',
+ type: 'sub',
+ name: 'shared-sub',
+ },
+ },
+ deleteRes,
+ );
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+
+ const restoreHandler = getHandler(
+ registerArchiveRoutes,
+ 'POST',
+ '/api/archives/:id/restore',
+ );
+ const restoreRes = createResponse('/api/archives/:id/restore');
+
+ restoreHandler(
+ {
+ params: { id: state[ARCHIVES_KEY][0].id },
+ query: {},
+ },
+ restoreRes,
+ );
+
+ expect(restoreRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.have.length(1);
+ expect(state[TOKENS_KEY][0].token).to.equal('legacy-duration');
+ expect(state[TOKENS_KEY][0].mode).to.equal('duration');
+ expect(state[TOKENS_KEY][0].expiresIn).to.equal('1d');
+ expect(state[TOKENS_KEY][0].exp).to.be.a('number');
+ expect(state[ARCHIVES_KEY]).to.deep.equal([]);
+ });
+
+ it('restores legacy exp-only share tokens as exact-datetime shares', async function () {
+ state[SUBS_KEY] = [
+ {
+ name: 'shared-sub',
+ source: 'remote',
+ process: [],
+ },
+ ];
+ const exactExp = Date.now() - 5 * 60 * 1000;
+ state[TOKENS_KEY] = [
+ {
+ token: 'legacy-datetime',
+ type: 'sub',
+ name: 'shared-sub',
+ displayName: 'Legacy Exact Share',
+ exp: exactExp,
+ },
+ ];
+
+ const deleteHandler = getHandler(
+ registerTokenRoutes,
+ 'DELETE',
+ '/api/token/:token',
+ );
+ const deleteRes = createResponse('/api/token/:token');
+
+ deleteHandler(
+ {
+ params: { token: 'legacy-datetime' },
+ query: {
+ mode: 'archive',
+ type: 'sub',
+ name: 'shared-sub',
+ },
+ },
+ deleteRes,
+ );
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+
+ const restoreHandler = getHandler(
+ registerArchiveRoutes,
+ 'POST',
+ '/api/archives/:id/restore',
+ );
+ const restoreRes = createResponse('/api/archives/:id/restore');
+
+ restoreHandler(
+ {
+ params: { id: state[ARCHIVES_KEY][0].id },
+ query: {},
+ },
+ restoreRes,
+ );
+
+ expect(restoreRes.body.status).to.equal('success');
+ expect(state[TOKENS_KEY]).to.have.length(1);
+ expect(state[TOKENS_KEY][0].token).to.equal('legacy-datetime');
+ expect(state[TOKENS_KEY][0].mode).to.equal('datetime');
+ expect(state[TOKENS_KEY][0].exp).to.equal(exactExp);
+ expect(state[TOKENS_KEY][0]).to.not.have.property('expiresIn');
+ expect(state[ARCHIVES_KEY]).to.deep.equal([]);
+ });
+
+ it('restores artifacts without stale sync metadata', async function () {
+ state[ARTIFACTS_KEY] = [
+ {
+ name: 'demo-artifact',
+ type: 'subscription',
+ source: 'demo-sub',
+ platform: 'Clash',
+ sync: true,
+ updated: 1711111111111,
+ url: 'https://gist.example.com/demo-artifact',
+ },
+ ];
+
+ const deleteHandler = getHandler(
+ registerArtifactRoutes,
+ 'DELETE',
+ '/api/artifact/:name',
+ );
+ const deleteRes = createResponse('/api/artifact/:name');
+
+ await deleteHandler(
+ {
+ params: { name: 'demo-artifact' },
+ query: { mode: 'archive' },
+ },
+ deleteRes,
+ );
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(state[ARTIFACTS_KEY]).to.deep.equal([]);
+ expect(state[ARCHIVES_KEY]).to.have.length(1);
+ expect(state[ARCHIVES_KEY][0].itemType).to.equal('artifact');
+ expect(state[ARCHIVES_KEY][0].snapshot.updated).to.equal(1711111111111);
+ expect(state[ARCHIVES_KEY][0].snapshot.url).to.equal(
+ 'https://gist.example.com/demo-artifact',
+ );
+
+ const restoreHandler = getHandler(
+ registerArchiveRoutes,
+ 'POST',
+ '/api/archives/:id/restore',
+ );
+ const restoreRes = createResponse('/api/archives/:id/restore');
+
+ restoreHandler(
+ {
+ params: { id: state[ARCHIVES_KEY][0].id },
+ query: {},
+ },
+ restoreRes,
+ );
+
+ expect(restoreRes.body.status).to.equal('success');
+ expect(state[ARTIFACTS_KEY]).to.have.length(1);
+ expect(state[ARTIFACTS_KEY][0].name).to.equal('demo-artifact');
+ expect(state[ARTIFACTS_KEY][0].sync).to.equal(true);
+ expect(state[ARTIFACTS_KEY][0]).to.not.have.property('updated');
+ expect(state[ARTIFACTS_KEY][0]).to.not.have.property('url');
+ expect(state[ARCHIVES_KEY]).to.deep.equal([]);
+ });
+
+ it('sorts archive entries by id order', function () {
+ state[ARCHIVES_KEY] = [
+ { id: 'entry-b', itemType: 'sub', name: 'b' },
+ { id: 'entry-a', itemType: 'sub', name: 'a' },
+ ];
+
+ const sortHandler = getHandler(
+ registerSortingRoutes,
+ 'POST',
+ '/api/sort/archives',
+ );
+ const sortRes = createResponse('/api/sort/archives');
+
+ sortHandler(
+ {
+ body: ['entry-a', 'entry-b'],
+ query: {},
+ },
+ sortRes,
+ );
+
+ expect(sortRes.body.status).to.equal('success');
+ expect(state[ARCHIVES_KEY].map((item) => item.id)).to.deep.equal([
+ 'entry-a',
+ 'entry-b',
+ ]);
+ });
+
+ it('advertises archive capability in env payload', function () {
+ const envHandler = getHandler(registerMiscRoutes, 'GET', '/api/utils/env');
+ const envRes = createResponse('/api/utils/env');
+
+ envHandler({ query: {} }, envRes);
+
+ const payload = JSON.parse(envRes.sent);
+ expect(payload.status).to.equal('success');
+ expect(payload.data.feature.archive).to.equal(true);
+ });
+});
diff --git a/backend/src/test/restful/create-position.spec.js b/backend/src/test/restful/create-position.spec.js
new file mode 100644
index 0000000000..0dc47992da
--- /dev/null
+++ b/backend/src/test/restful/create-position.spec.js
@@ -0,0 +1,344 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ ARTIFACTS_KEY,
+ COLLECTIONS_KEY,
+ FILES_KEY,
+ SETTINGS_KEY,
+ SUBS_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+import { insertByPosition } from '@/utils/database';
+import { getCreateItemPosition } from '@/utils/create-item-position';
+
+let $;
+let registerSubscriptionRoutes;
+let registerCollectionRoutes;
+let registerFileRoutes;
+let registerArtifactRoutes;
+let registerTokenRoutes;
+let originalRead;
+let originalWrite;
+let originalInfo;
+let originalError;
+let state;
+let tempDir;
+let previousDataBasePath;
+
+function createRouteApp() {
+ const handlers = new Map();
+ const methods = ['get', 'post', 'put', 'patch', 'delete'];
+
+ const app = {
+ handlers,
+ route(pattern) {
+ const chain = {};
+ methods.forEach((method) => {
+ chain[method] = (handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return chain;
+ };
+ });
+ return chain;
+ },
+ };
+
+ methods.forEach((method) => {
+ app[method] = (pattern, handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return app;
+ };
+ });
+
+ return app;
+}
+
+function getHandler(register, method, pattern) {
+ const app = createRouteApp();
+ register(app);
+ return app.handlers.get(`${method} ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ statusCode: 200,
+ body: null,
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ json(payload) {
+ this.body = payload;
+ return this;
+ },
+ };
+}
+
+describe('create position behavior', function () {
+ before(async function () {
+ previousDataBasePath = process.env.SUB_STORE_DATA_BASE_PATH;
+ tempDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'sub-store-create-position-'),
+ );
+ process.env.SUB_STORE_DATA_BASE_PATH = tempDir;
+
+ ({ default: $ } = require('@/core/app'));
+ ({ default: registerSubscriptionRoutes } = require(
+ '@/restful/subscriptions'
+ ));
+ ({ default: registerCollectionRoutes } = require('@/restful/collections'));
+ ({ default: registerFileRoutes } = require('@/restful/file'));
+ ({ default: registerArtifactRoutes } = require('@/restful/artifacts'));
+ ({ default: registerTokenRoutes } = require('@/restful/token'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+
+ if (previousDataBasePath == null) {
+ delete process.env.SUB_STORE_DATA_BASE_PATH;
+ } else {
+ process.env.SUB_STORE_DATA_BASE_PATH = previousDataBasePath;
+ }
+
+ if (tempDir) {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SUBS_KEY]: [],
+ [COLLECTIONS_KEY]: [],
+ [FILES_KEY]: [],
+ [ARTIFACTS_KEY]: [],
+ [TOKENS_KEY]: [],
+ [SETTINGS_KEY]: {},
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.error = () => {};
+ });
+
+ describe('insertByPosition', function () {
+ it('prepends when position is top', function () {
+ const list = ['existing'];
+
+ insertByPosition(list, 'new', 'top');
+
+ expect(list).to.deep.equal(['new', 'existing']);
+ });
+
+ it('appends when position is bottom', function () {
+ const list = ['existing'];
+
+ insertByPosition(list, 'new', 'bottom');
+
+ expect(list).to.deep.equal(['existing', 'new']);
+ });
+ });
+
+ describe('getCreateItemPosition', function () {
+ it('reads top from synced settings', function () {
+ state[SETTINGS_KEY] = {
+ appearanceSetting: {
+ createItemPosition: 'top',
+ },
+ };
+
+ expect(getCreateItemPosition()).to.equal('top');
+ });
+
+ it('falls back to bottom when settings are absent', function () {
+ expect(getCreateItemPosition()).to.equal('bottom');
+ });
+ });
+
+ describe('create route insertion position', function () {
+ beforeEach(function () {
+ state[SETTINGS_KEY] = {
+ appearanceSetting: {
+ createItemPosition: 'top',
+ },
+ };
+ });
+
+ it('creates subscriptions at the top when settings request it', function () {
+ state[SUBS_KEY] = [{ name: 'older-sub' }];
+ const handler = getHandler(
+ registerSubscriptionRoutes,
+ 'POST',
+ '/api/subs',
+ );
+ const res = createResponse('/api/subs');
+
+ handler(
+ {
+ body: { name: 'new-sub', subscriptions: ['ignored'] },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[SUBS_KEY].map((item) => item.name)).to.deep.equal([
+ 'new-sub',
+ 'older-sub',
+ ]);
+ expect(state[SUBS_KEY][0]).to.not.have.property('subscriptions');
+ expect(res.statusCode).to.equal(201);
+ });
+
+ it('creates collections at the top when settings request it', function () {
+ state[COLLECTIONS_KEY] = [{ name: 'older-collection' }];
+ const handler = getHandler(
+ registerCollectionRoutes,
+ 'POST',
+ '/api/collections',
+ );
+ const res = createResponse('/api/collections');
+
+ handler(
+ {
+ body: { name: 'new-collection', subscriptions: [] },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[COLLECTIONS_KEY].map((item) => item.name)).to.deep.equal([
+ 'new-collection',
+ 'older-collection',
+ ]);
+ expect(res.statusCode).to.equal(201);
+ });
+
+ it('creates files at the top when settings request it', function () {
+ state[FILES_KEY] = [{ name: 'older-file' }];
+ const handler = getHandler(registerFileRoutes, 'POST', '/api/files');
+ const res = createResponse('/api/files');
+
+ handler(
+ {
+ body: { name: 'new-file' },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[FILES_KEY].map((item) => item.name)).to.deep.equal([
+ 'new-file',
+ 'older-file',
+ ]);
+ expect(res.statusCode).to.equal(201);
+ });
+
+ it('creates artifacts at the top when settings request it', function () {
+ state[ARTIFACTS_KEY] = [{ name: 'older-artifact' }];
+ const handler = getHandler(
+ registerArtifactRoutes,
+ 'POST',
+ '/api/artifacts',
+ );
+ const res = createResponse('/api/artifacts');
+
+ handler(
+ {
+ body: {
+ name: 'new-artifact',
+ type: 'file',
+ source: 'demo',
+ platform: 'JSON',
+ },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[ARTIFACTS_KEY].map((item) => item.name)).to.deep.equal([
+ 'new-artifact',
+ 'older-artifact',
+ ]);
+ expect(res.statusCode).to.equal(201);
+ });
+
+ it('creates tokens at the top when settings request it without persisting transport metadata', async function () {
+ state[SUBS_KEY] = [{ name: 'sub-1' }];
+ state[TOKENS_KEY] = [
+ { token: 'older-token', type: 'sub', name: 'sub-1' },
+ ];
+ const handler = getHandler(registerTokenRoutes, 'POST', '/api/token');
+ const res = createResponse('/api/token');
+
+ await handler(
+ {
+ body: {
+ payload: {
+ type: 'sub',
+ name: 'sub-1',
+ token: 'new-token',
+ },
+ options: {},
+ },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[TOKENS_KEY].map((item) => item.token)).to.deep.equal([
+ 'new-token',
+ 'older-token',
+ ]);
+ expect(state[TOKENS_KEY][0]).to.not.have.property('position');
+ expect(res.statusCode).to.equal(200);
+ });
+
+ it('defaults to bottom insertion when no position is provided', function () {
+ state[SUBS_KEY] = [{ name: 'older-sub' }];
+ state[SETTINGS_KEY] = {};
+ const handler = getHandler(
+ registerSubscriptionRoutes,
+ 'POST',
+ '/api/subs',
+ );
+ const res = createResponse('/api/subs');
+
+ handler(
+ {
+ body: { name: 'new-sub' },
+ query: {},
+ },
+ res,
+ );
+
+ expect(state[SUBS_KEY].map((item) => item.name)).to.deep.equal([
+ 'older-sub',
+ 'new-sub',
+ ]);
+ expect(res.statusCode).to.equal(201);
+ });
+ });
+});
diff --git a/backend/src/test/restful/download.spec.js b/backend/src/test/restful/download.spec.js
new file mode 100644
index 0000000000..2359fcb0dd
--- /dev/null
+++ b/backend/src/test/restful/download.spec.js
@@ -0,0 +1,275 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
+
+const UUID = '11111111-1111-4111-8111-111111111111';
+const VLESS_WS = `vless://${UUID}@1.1.1.1:443?security=tls&type=ws&host=cdn.example.com&path=%2Fws&sni=sni.example.com#VLESS%20WS`;
+
+let $;
+let registerDownloadRoutes;
+let originalError;
+let originalInfo;
+let originalNotify;
+let originalRead;
+let originalWarn;
+let originalWrite;
+let state;
+
+function createRouteApp() {
+ const handlers = new Map();
+
+ return {
+ handlers,
+ get(pattern, handler) {
+ handlers.set(`GET ${pattern}`, handler);
+ return this;
+ },
+ };
+}
+
+function getHandler(pattern) {
+ const app = createRouteApp();
+ registerDownloadRoutes(app);
+ return app.handlers.get(`GET ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ headers: {},
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ sent: null,
+ statusCode: 200,
+ removeHeader(key) {
+ delete this.headers[key];
+ return this;
+ },
+ getHeaders() {
+ return this.headers;
+ },
+ send(payload) {
+ this.sent = payload;
+ return this;
+ },
+ set(key, value) {
+ this.headers[key] = value;
+ return this;
+ },
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ };
+}
+
+async function requestDownloadSubscription(query) {
+ const handler = getHandler('/download/:name');
+ const res = createResponse('/download/:name');
+
+ await handler(
+ {
+ body: {},
+ headers: {},
+ method: 'GET',
+ params: { name: 'local-vless' },
+ path: '/download/local-vless',
+ query,
+ url: '/download/local-vless',
+ },
+ res,
+ );
+
+ return res;
+}
+
+async function downloadSubscription(query) {
+ const res = await requestDownloadSubscription(query);
+ return res.sent;
+}
+
+async function downloadCollection(query) {
+ const handler = getHandler('/download/collection/:name');
+ const res = createResponse('/download/collection/:name');
+
+ await handler(
+ {
+ body: {},
+ headers: {},
+ method: 'GET',
+ params: { name: 'local-col' },
+ path: '/download/collection/local-col',
+ query,
+ url: '/download/collection/local-col',
+ },
+ res,
+ );
+
+ return res.sent;
+}
+
+describe('download routes', function () {
+ before(async function () {
+ ({ default: $ } = require('@/core/app'));
+ ({ default: registerDownloadRoutes } = require('@/restful/download'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalWarn = $.warn.bind($);
+ originalError = $.error.bind($);
+ originalNotify = $.notify.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.warn = originalWarn;
+ $.error = originalError;
+ $.notify = originalNotify;
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SUBS_KEY]: [
+ {
+ name: 'local-vless',
+ source: 'local',
+ content: VLESS_WS,
+ },
+ ],
+ [COLLECTIONS_KEY]: [
+ {
+ name: 'local-col',
+ subscriptions: ['local-vless'],
+ firstSubFlow: false,
+ },
+ ],
+ };
+
+ $.read = (key) => state[key] || [];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.warn = () => {};
+ $.error = () => {};
+ $.notify = () => {};
+ });
+
+ it('keeps SurgeMac Mihomo external output unmerged by default', async function () {
+ const output = await downloadSubscription({ target: 'SurgeMac' });
+
+ expect(output).to.include(
+ 'VLESS WS=external,exec="/usr/local/bin/mihomo"',
+ );
+ expect(output).to.not.include('VLESS WS=socks5');
+ expect(output).to.not.include('mihomo merged=external');
+ });
+
+ it('enables merged Mihomo external output when the query parameter is present', async function () {
+ const output = await downloadSubscription({
+ target: 'SurgeMac',
+ mihomoMerge: 'true',
+ mihomoMergeName: 'Shared Mihomo',
+ });
+
+ expect(output).to.include('VLESS WS=socks5,127.0.0.1,65535');
+ expect(output).to.include(
+ 'Shared Mihomo=external,exec="/usr/local/bin/mihomo"',
+ );
+ expect(output).to.not.include(
+ 'VLESS WS=external,exec="/usr/local/bin/mihomo"',
+ );
+ });
+
+ it('treats mihomoMerge=false as enabled because query flags are presence-based', async function () {
+ const output = await downloadSubscription({
+ target: 'SurgeMac',
+ mihomoMerge: 'false',
+ mihomoMergeName: 'Shared Mihomo',
+ });
+
+ expect(output).to.include('VLESS WS=socks5,127.0.0.1,65535');
+ expect(output).to.include(
+ 'Shared Mihomo=external,exec="/usr/local/bin/mihomo"',
+ );
+ expect(output).to.not.include(
+ 'VLESS WS=external,exec="/usr/local/bin/mihomo"',
+ );
+ });
+
+ it('keeps merged Mihomo process external when mihomoExternal is enabled', async function () {
+ const output = await downloadSubscription({
+ target: 'SurgeMac',
+ mihomoExternal: 'true',
+ mihomoMerge: 'true',
+ });
+
+ expect(output).to.include('VLESS WS=socks5,127.0.0.1,65535');
+ expect(output).to.include(
+ 'mihomo merged=external,exec="/usr/local/bin/mihomo",local-port=65534',
+ );
+ expect(output).to.not.include('mihomo merged=socks5');
+ });
+
+ it('enables merged Mihomo external output for collection downloads', async function () {
+ const output = await downloadCollection({
+ target: 'SurgeMac',
+ mihomoMerge: 'true',
+ mihomoMergeName: 'Shared Mihomo',
+ });
+
+ expect(output).to.include('VLESS WS=socks5,127.0.0.1,65535');
+ expect(output).to.include(
+ 'Shared Mihomo=external,exec="/usr/local/bin/mihomo"',
+ );
+ });
+
+ it('applies shortcut response transformers before sending subscription output', async function () {
+ state[SUBS_KEY][0].process = [
+ {
+ type: 'Response Transformer',
+ args: {
+ mode: 'script',
+ content:
+ "$res.status = 202\n$res.header['x-test'] = 'ok'\n$res.body = 'changed'",
+ },
+ },
+ ];
+
+ const res = await requestDownloadSubscription({ target: 'JSON' });
+
+ expect(res.statusCode).to.equal(202);
+ expect(res.headers['x-test']).to.equal('ok');
+ expect(res.sent).to.equal('changed');
+ });
+
+ it('applies transformFunction response transformers before sending subscription output', async function () {
+ state[SUBS_KEY][0].process = [
+ {
+ type: 'Response Transformer',
+ args: {
+ mode: 'script',
+ content: `async function transformFunction(res, context) {
+ res.status = 201;
+ res.body = context.source['local-vless'].name;
+ return res;
+ }`,
+ },
+ },
+ ];
+
+ const res = await requestDownloadSubscription({ target: 'JSON' });
+
+ expect(res.statusCode).to.equal(201);
+ expect(res.sent).to.equal('local-vless');
+ });
+});
diff --git a/backend/src/test/restful/file.spec.js b/backend/src/test/restful/file.spec.js
new file mode 100644
index 0000000000..210f15d98a
--- /dev/null
+++ b/backend/src/test/restful/file.spec.js
@@ -0,0 +1,174 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import { FILES_KEY } from '@/constants';
+
+let $;
+let registerFileRoutes;
+let originalError;
+let originalInfo;
+let originalNotify;
+let originalRead;
+let originalWarn;
+let originalWrite;
+let state;
+
+function createRouteApp() {
+ const handlers = new Map();
+ const methods = ['get', 'post', 'put', 'patch', 'delete'];
+
+ const app = {
+ handlers,
+ route(pattern) {
+ const chain = {};
+ methods.forEach((method) => {
+ chain[method] = (handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return chain;
+ };
+ });
+ return chain;
+ },
+ };
+
+ methods.forEach((method) => {
+ app[method] = (pattern, handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return app;
+ };
+ });
+
+ return app;
+}
+
+function getHandler(pattern) {
+ const app = createRouteApp();
+ registerFileRoutes(app);
+ return app.handlers.get(`GET ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ headers: {},
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ sent: null,
+ statusCode: 200,
+ removeHeader(key) {
+ delete this.headers[key];
+ return this;
+ },
+ getHeaders() {
+ return this.headers;
+ },
+ send(payload) {
+ this.sent = payload;
+ return this;
+ },
+ set(key, value) {
+ this.headers[key] = value;
+ return this;
+ },
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ };
+}
+
+async function requestFile(query = {}) {
+ const handler = getHandler('/api/file/:name');
+ const res = createResponse('/api/file/:name');
+
+ await handler(
+ {
+ body: {},
+ headers: {},
+ method: 'GET',
+ params: { name: 'local-file' },
+ path: '/api/file/local-file',
+ query,
+ url: '/api/file/local-file',
+ },
+ res,
+ );
+
+ return res;
+}
+
+describe('file routes', function () {
+ before(async function () {
+ ({ default: $ } = require('@/core/app'));
+ ({ default: registerFileRoutes } = require('@/restful/file'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalWarn = $.warn.bind($);
+ originalError = $.error.bind($);
+ originalNotify = $.notify.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.warn = originalWarn;
+ $.error = originalError;
+ $.notify = originalNotify;
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [FILES_KEY]: [
+ {
+ name: 'local-file',
+ source: 'local',
+ content: 'base',
+ process: [],
+ },
+ ],
+ };
+
+ $.read = (key) => state[key] || [];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.warn = () => {};
+ $.error = () => {};
+ $.notify = () => {};
+ });
+
+ it('passes Script Operator $options changes to response transformers', async function () {
+ state[FILES_KEY][0].process = [
+ {
+ type: 'Script Operator',
+ args: {
+ mode: 'script',
+ content:
+ "$options.fromOperator = 'carried'\n$content = `${$content}-script`",
+ },
+ },
+ {
+ type: 'Response Transformer',
+ args: {
+ mode: 'script',
+ content:
+ "$res.header['x-from-options'] = $options.fromOperator\n$res.body = `${$res.body}-${$options.fromOperator}`",
+ },
+ },
+ ];
+
+ const res = await requestFile();
+
+ expect(res.headers['x-from-options']).to.equal('carried');
+ expect(res.sent).to.equal('base-script-carried');
+ });
+});
diff --git a/backend/src/test/restful/logs.spec.js b/backend/src/test/restful/logs.spec.js
new file mode 100644
index 0000000000..91b44892f2
--- /dev/null
+++ b/backend/src/test/restful/logs.spec.js
@@ -0,0 +1,500 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import { LOGS_KEY, SETTINGS_KEY } from '@/constants';
+
+let $;
+let appendLogEntry;
+let getLogEntries;
+let clearLogEntries;
+let clearLogSettingsCache;
+let registerLogRoutes;
+let registerSettingsRoutes;
+let originalRead;
+let originalWrite;
+let state;
+
+const SUB_STORE_BANNER = `
+${'\u2505'.repeat(44)}
+ Sub-Store -- v2.22.6
+${'\u2505'.repeat(44)}
+`;
+const SUB_STORE_PLATFORM_BANNER = `
+${'\u2505'.repeat(44)}
+ Sub-Store -- v2.22.6
+ Loon -- Loon(842)
+${'\u2505'.repeat(44)}
+`;
+const DEFAULT_IGNORED_NOISE_LOGS = [
+ '[sub-store] INFO: Surge AnyTLS Parser is activated',
+ '[sub-store] ERROR: Fallback Base64 Pre-processor error: decoded line does not start with protocol',
+];
+const DEFAULT_VISIBLE_PREPROCESSOR_LOGS = [
+ '[sub-store] INFO: Pre-processor [Fallback Base64 Pre-processor] activated',
+ '[sub-store] INFO: Pre-processor [Clash Pre-processor] activated',
+];
+
+function createRouteApp() {
+ const handlers = new Map();
+ const methods = ['get', 'delete', 'patch'];
+
+ const app = {
+ handlers,
+ route(pattern) {
+ const chain = {};
+ methods.forEach((method) => {
+ chain[method] = (handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return chain;
+ };
+ });
+ return chain;
+ },
+ };
+
+ methods.forEach((method) => {
+ app[method] = (pattern, handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return app;
+ };
+ });
+
+ return app;
+}
+
+function getHandler(method, pattern) {
+ const app = createRouteApp();
+ registerLogRoutes(app);
+ return app.handlers.get(`${method} ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ body: null,
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ statusCode: 200,
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ json(payload) {
+ this.body = payload;
+ return this;
+ },
+ };
+}
+
+describe('logs routes', function () {
+ before(async function () {
+ ({ default: $ } = require('@/core/app'));
+ ({
+ appendLogEntry,
+ getLogEntries,
+ clearLogEntries,
+ clearLogSettingsCache,
+ } = require('@/utils/debug-logs'));
+ ({ default: registerLogRoutes } = require('@/restful/logs'));
+ ({ default: registerSettingsRoutes } = require('@/restful/settings'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SETTINGS_KEY]: { logsMaxCount: 500 },
+ [LOGS_KEY]: '[]',
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ clearLogSettingsCache();
+ });
+
+ it('persists log entries and trims by logsMaxCount', function () {
+ state[SETTINGS_KEY] = { logsMaxCount: 2 };
+
+ appendLogEntry($, 'info', ['first']);
+ appendLogEntry($, 'warn', ['second']);
+ appendLogEntry($, 'error', ['third']);
+
+ const storedLogs = JSON.parse(state[LOGS_KEY]);
+ expect(storedLogs).to.have.length(2);
+ expect(storedLogs[0].message).to.equal('[unknown] WARN: second');
+ expect(storedLogs[1].message).to.equal('[unknown] ERROR: third');
+ });
+
+ it('disables persistent log cache IO when logsMaxCount is zero', function () {
+ state[SETTINGS_KEY] = { logsMaxCount: 0 };
+ state[LOGS_KEY] = JSON.stringify([
+ {
+ id: 'existing-log',
+ time: 1,
+ level: 'info',
+ message: '[unknown] INFO: existing',
+ },
+ ]);
+ const readKeys = [];
+ const writeKeys = [];
+ $.read = (key) => {
+ readKeys.push(key);
+ return state[key];
+ };
+ $.write = (data, key) => {
+ writeKeys.push(key);
+ state[key] = data;
+ return true;
+ };
+
+ appendLogEntry($, 'info', ['disabled write']);
+
+ expect(readKeys).to.deep.equal([SETTINGS_KEY]);
+ expect(writeKeys).to.deep.equal([]);
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal(['[unknown] INFO: existing']);
+
+ readKeys.length = 0;
+ const result = getLogEntries($, { limit: 10 });
+
+ expect(result).to.deep.equal({
+ logs: [],
+ total: 0,
+ maxCount: 0,
+ });
+ expect(readKeys).to.deep.equal([SETTINGS_KEY]);
+ expect(writeKeys).to.deep.equal([]);
+ });
+
+ it('disables persistent log cache IO by default', function () {
+ state[SETTINGS_KEY] = {};
+ appendLogEntry($, 'info', ['default disabled write']);
+
+ expect(JSON.parse(state[LOGS_KEY])).to.deep.equal([]);
+ expect(getLogEntries($, { limit: 10 })).to.deep.equal({
+ logs: [],
+ total: 0,
+ maxCount: 0,
+ });
+ });
+
+ it('preserves explicit zero logsMaxCount in settings', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ const zeroRes = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: '0' } }, zeroRes);
+
+ expect(zeroRes.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.deep.include({
+ logsMaxCount: '0',
+ });
+
+ const emptyRes = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: '' } }, emptyRes);
+
+ expect(emptyRes.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
+ });
+
+ it('normalizes stored log messages with missing scope or level', function () {
+ appendLogEntry($, 'log', [
+ '[测试] 有 scope 无 level 的日志: 🇺🇸 Oracle [snell]',
+ ]);
+ appendLogEntry($, 'warn', [
+ '测试无 scope 无 level 的日志: 🇺🇸 Oracle [snell]',
+ ]);
+ appendLogEntry($, 'error', [
+ '[测试] ERROR: 已经完整的日志: 🇺🇸 Oracle [snell]',
+ ]);
+
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal([
+ '[测试] LOG: 有 scope 无 level 的日志: 🇺🇸 Oracle [snell]',
+ '[unknown] WARN: 测试无 scope 无 level 的日志: 🇺🇸 Oracle [snell]',
+ '[测试] ERROR: 已经完整的日志: 🇺🇸 Oracle [snell]',
+ ]);
+ });
+
+ it('skips Sub-Store startup banner logs when appending', function () {
+ appendLogEntry($, 'log', [SUB_STORE_BANNER]);
+ appendLogEntry($, 'log', [SUB_STORE_PLATFORM_BANNER]);
+
+ expect(JSON.parse(state[LOGS_KEY])).to.deep.equal([]);
+ });
+
+ it('skips default noisy parser and fallback preprocessor error logs when appending', function () {
+ DEFAULT_IGNORED_NOISE_LOGS.forEach((message) => {
+ appendLogEntry($, 'info', [message]);
+ });
+ appendLogEntry($, 'info', ['normal message']);
+
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal(['[unknown] INFO: normal message']);
+ });
+
+ it('filters previously persisted Sub-Store startup banner logs', function () {
+ const normalLog = {
+ id: 'normal-log',
+ time: 2,
+ level: 'info',
+ message: 'normal message',
+ };
+ state[LOGS_KEY] = JSON.stringify([
+ {
+ id: 'banner-log',
+ time: 1,
+ level: 'log',
+ message: SUB_STORE_BANNER,
+ },
+ {
+ id: 'platform-banner-log',
+ time: 2,
+ level: 'log',
+ message: `[sub-store] LOG: ${SUB_STORE_PLATFORM_BANNER}`,
+ },
+ {
+ id: 'unknown-platform-banner-log',
+ time: 3,
+ level: 'log',
+ message: `[unknown] LOG: ${SUB_STORE_PLATFORM_BANNER}`,
+ },
+ normalLog,
+ ]);
+
+ const result = getLogEntries($, { limit: 10 });
+
+ expect(result.logs.map((log) => log.message)).to.deep.equal([
+ 'normal message',
+ ]);
+ expect(result.total).to.equal(1);
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal(['normal message']);
+ });
+
+ it('filters previously persisted default noisy parser and fallback preprocessor error logs', function () {
+ state[LOGS_KEY] = JSON.stringify([
+ ...DEFAULT_IGNORED_NOISE_LOGS.map((message, index) => ({
+ id: `noise-log-${index}`,
+ time: index + 1,
+ level: 'info',
+ message,
+ })),
+ {
+ id: 'normal-log',
+ time: 4,
+ level: 'info',
+ message: 'normal message',
+ },
+ ]);
+
+ const result = getLogEntries($, { limit: 10 });
+
+ expect(result.logs.map((log) => log.message)).to.deep.equal([
+ 'normal message',
+ ]);
+ expect(result.total).to.equal(1);
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal(['normal message']);
+ });
+
+ it('returns recent logs by limit', function () {
+ appendLogEntry($, 'info', ['one']);
+ appendLogEntry($, 'info', ['two']);
+ appendLogEntry($, 'info', ['three']);
+
+ const result = getLogEntries($, { limit: 2 });
+
+ expect(result.logs.map((log) => log.message)).to.deep.equal([
+ '[unknown] INFO: three',
+ '[unknown] INFO: two',
+ ]);
+ expect(result.total).to.equal(3);
+ });
+
+ it('trims persisted logs when logsMaxCount is reduced', function () {
+ appendLogEntry($, 'info', ['one']);
+ appendLogEntry($, 'info', ['two']);
+ appendLogEntry($, 'info', ['three']);
+
+ state[SETTINGS_KEY] = { logsMaxCount: 2 };
+ const result = getLogEntries($, { limit: 10 });
+
+ expect(result.logs.map((log) => log.message)).to.deep.equal([
+ '[unknown] INFO: three',
+ '[unknown] INFO: two',
+ ]);
+ expect(
+ JSON.parse(state[LOGS_KEY]).map((log) => log.message),
+ ).to.deep.equal(['[unknown] INFO: two', '[unknown] INFO: three']);
+ });
+
+ it('filters logs by keyword', function () {
+ appendLogEntry($, 'info', ['Alpha message']);
+ appendLogEntry($, 'error', ['beta message']);
+
+ const result = getLogEntries($, { keyword: 'Alpha' });
+
+ expect(result.logs).to.have.length(1);
+ expect(result.logs[0].message).to.equal(
+ '[unknown] INFO: Alpha message',
+ );
+ });
+
+ it('supports case-insensitive keyword filtering', function () {
+ appendLogEntry($, 'info', ['Alpha message']);
+ appendLogEntry($, 'error', ['beta message']);
+
+ const result = getLogEntries($, {
+ keyword: 'alpha',
+ ignoreCase: 'true',
+ });
+
+ expect(result.logs).to.have.length(1);
+ expect(result.logs[0].message).to.equal(
+ '[unknown] INFO: Alpha message',
+ );
+ });
+
+ it('supports regular expression keyword filtering', function () {
+ appendLogEntry($, 'info', ['request GET /api/logs']);
+ appendLogEntry($, 'warn', ['request POST /api/subs']);
+
+ const result = getLogEntries($, {
+ keyword: 'GET\\s+/api/logs',
+ regex: 'true',
+ });
+
+ expect(result.logs).to.have.length(1);
+ expect(result.logs[0].message).to.equal(
+ '[unknown] INFO: request GET /api/logs',
+ );
+ });
+
+ it('rejects invalid regular expressions from GET /api/logs', function () {
+ const getHandler_ = getHandler('GET', '/api/logs');
+ const getRes = createResponse('/api/logs');
+ getHandler_(
+ {
+ query: {
+ keyword: '[',
+ regex: 'true',
+ },
+ },
+ getRes,
+ );
+
+ expect(getRes.statusCode).to.equal(400);
+ expect(getRes.body.status).to.equal('failed');
+ expect(getRes.body.error.code).to.equal('INVALID_LOG_KEYWORD_REGEX');
+ });
+
+ it('serves logs from GET /api/logs and clears them with DELETE /api/logs', function () {
+ appendLogEntry($, 'info', ['route log']);
+
+ const getHandler_ = getHandler('GET', '/api/logs');
+ const getRes = createResponse('/api/logs');
+ getHandler_({ query: { limit: 10 } }, getRes);
+
+ expect(getRes.body.status).to.equal('success');
+ expect(getRes.body.data.logs).to.have.length(1);
+ expect(getRes.body.data.logs[0].message).to.equal(
+ '[unknown] INFO: route log',
+ );
+
+ const deleteHandler = getHandler('DELETE', '/api/logs');
+ const deleteRes = createResponse('/api/logs');
+ deleteHandler({ query: {} }, deleteRes);
+
+ expect(deleteRes.body.status).to.equal('success');
+ expect(JSON.parse(state[LOGS_KEY])).to.deep.equal([]);
+ expect(clearLogEntries).to.be.a('function');
+ });
+
+ it('rejects negative logsMaxCount in settings PATCH', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ const res = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: -1 } }, res);
+
+ expect(res.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
+ });
+
+ it('rejects NaN logsMaxCount in settings PATCH', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ const res = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: 'abc' } }, res);
+
+ expect(res.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
+ });
+
+ it('rejects null logsMaxCount in settings PATCH', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ const res = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: null } }, res);
+
+ expect(res.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.not.have.property('logsMaxCount');
+ });
+
+ it('accepts valid positive integer logsMaxCount in settings PATCH', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ const res = createResponse('/api/settings');
+ await patchHandler({ body: { logsMaxCount: 300 } }, res);
+
+ expect(res.body.status).to.equal('success');
+ expect(state[SETTINGS_KEY]).to.deep.include({ logsMaxCount: 300 });
+ });
+
+ it('clears log settings cache after settings PATCH so new logsMaxCount takes effect immediately', async function () {
+ const app = createRouteApp();
+ registerSettingsRoutes(app);
+ const patchHandler = app.handlers.get('PATCH /api/settings');
+
+ state[SETTINGS_KEY] = { logsMaxCount: 500 };
+ appendLogEntry($, 'info', ['before change']);
+ appendLogEntry($, 'info', ['before change 2']);
+ appendLogEntry($, 'info', ['before change 3']);
+
+ await patchHandler({ body: { logsMaxCount: 1 } }, createResponse('/api/settings'));
+
+ appendLogEntry($, 'info', ['after change']);
+
+ const storedLogs = JSON.parse(state[LOGS_KEY]);
+ expect(storedLogs).to.have.length(1);
+ expect(storedLogs[0].message).to.equal('[unknown] INFO: after change');
+ });
+
+});
diff --git a/backend/src/test/restful/settings.spec.js b/backend/src/test/restful/settings.spec.js
new file mode 100644
index 0000000000..d59009378d
--- /dev/null
+++ b/backend/src/test/restful/settings.spec.js
@@ -0,0 +1,74 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import {
+ getGithubAvatarApiUrl,
+ shouldRefreshArtifactStoreForSettingsPatch,
+} from '@/restful/settings';
+
+describe('settings routes', function () {
+ describe('artifact store refresh detection', function () {
+ it('refreshes when GitHub API URL changes', function () {
+ expect(
+ shouldRefreshArtifactStoreForSettingsPatch({
+ githubApiUrl: 'https://litegist.example.com/api',
+ }),
+ ).to.equal(true);
+ });
+
+ it('refreshes when a Gist-affecting setting is cleared', function () {
+ expect(
+ shouldRefreshArtifactStoreForSettingsPatch({
+ githubProxy: '',
+ }),
+ ).to.equal(true);
+ });
+
+ it('does not refresh for unrelated settings', function () {
+ expect(
+ shouldRefreshArtifactStoreForSettingsPatch({
+ logsMaxCount: 100,
+ }),
+ ).to.equal(false);
+ });
+
+ it('does not refresh when only GitHub username changes', function () {
+ expect(
+ shouldRefreshArtifactStoreForSettingsPatch({
+ githubUser: 'xream',
+ }),
+ ).to.equal(false);
+ });
+ });
+
+ describe('GitHub avatar API URL', function () {
+ it('uses the default GitHub users API when no custom API URL is set', function () {
+ expect(
+ getGithubAvatarApiUrl({
+ username: 'xream',
+ }),
+ ).to.equal('https://api.github.com/users/xream');
+ });
+
+ it('applies GitHub proxy only to the default GitHub users API', function () {
+ expect(
+ getGithubAvatarApiUrl({
+ username: 'xream',
+ githubProxy: 'https://proxy.example.com/',
+ }),
+ ).to.equal(
+ 'https://proxy.example.com/https://api.github.com/users/xream',
+ );
+ });
+
+ it('does not apply GitHub proxy to a custom GitHub users API', function () {
+ expect(
+ getGithubAvatarApiUrl({
+ username: 'xream',
+ githubApiUrl: 'https://litegist.example.com/api/',
+ githubProxy: 'https://proxy.example.com/',
+ }),
+ ).to.equal('https://litegist.example.com/api/users/xream');
+ });
+ });
+});
diff --git a/backend/src/test/restful/sync.spec.js b/backend/src/test/restful/sync.spec.js
new file mode 100644
index 0000000000..551b3191a5
--- /dev/null
+++ b/backend/src/test/restful/sync.spec.js
@@ -0,0 +1,213 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import { ARTIFACTS_KEY, FILES_KEY, SETTINGS_KEY } from '@/constants';
+
+let $;
+let registerArtifactRoutes;
+let registerSyncRoutes;
+let originalError;
+let originalInfo;
+let originalRead;
+let originalWrite;
+let state;
+
+function createRouteApp() {
+ const handlers = new Map();
+ const methods = ['get', 'post', 'put', 'patch', 'delete'];
+ const app = {
+ handlers,
+ route(pattern) {
+ const chain = {};
+ methods.forEach((method) => {
+ chain[method] = (handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return chain;
+ };
+ });
+ return chain;
+ },
+ };
+
+ methods.forEach((method) => {
+ app[method] = (pattern, handler) => {
+ handlers.set(`${method.toUpperCase()} ${pattern}`, handler);
+ return app;
+ };
+ });
+
+ return app;
+}
+
+function getHandler(pattern) {
+ const app = createRouteApp();
+ registerSyncRoutes(app);
+ return app.handlers.get(`GET ${pattern}`);
+}
+
+function createResponse(routePath) {
+ return {
+ req: {
+ route: {
+ path: routePath,
+ },
+ },
+ body: null,
+ statusCode: 200,
+ json(payload) {
+ this.body = payload;
+ return this;
+ },
+ status(code) {
+ this.statusCode = code;
+ return this;
+ },
+ };
+}
+
+async function requestSyncArtifact(name) {
+ const handler = getHandler('/api/sync/artifact/:name');
+ const res = createResponse('/api/sync/artifact/:name');
+
+ await handler(
+ {
+ body: {},
+ params: { name },
+ query: {},
+ },
+ res,
+ );
+
+ return res;
+}
+
+async function requestDeleteArtifact(name) {
+ const app = createRouteApp();
+ registerArtifactRoutes(app);
+ const handler = app.handlers.get('DELETE /api/artifact/:name');
+ const res = createResponse('/api/artifact/:name');
+
+ await handler(
+ {
+ params: { name },
+ query: {},
+ },
+ res,
+ );
+
+ return res;
+}
+
+describe('sync routes', function () {
+ before(async function () {
+ ({ default: $ } = require('@/core/app'));
+ ({ default: registerArtifactRoutes } = require('@/restful/artifacts'));
+ ({ default: registerSyncRoutes } = require('@/restful/sync'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SETTINGS_KEY]: {},
+ [ARTIFACTS_KEY]: [
+ {
+ name: 'local-artifact',
+ type: 'file',
+ source: 'local-file',
+ sync: true,
+ upload: false,
+ updated: 1711111111111,
+ url: 'https://gist.example.com/old',
+ },
+ ],
+ [FILES_KEY]: [
+ {
+ name: 'local-file',
+ source: 'local',
+ content: 'local content',
+ },
+ ],
+ };
+
+ $.read = (key) => state[key] || [];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.error = () => {};
+ });
+
+ it('updates run time without requiring a Gist URL when upload is disabled', async function () {
+ const startedAt = new Date().getTime();
+
+ const res = await requestSyncArtifact('local-artifact');
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.body.status).to.equal('success');
+ expect(res.body.data.updated).to.be.at.least(startedAt);
+ expect(res.body.data).to.not.have.property('url');
+ expect(state[ARTIFACTS_KEY][0].updated).to.be.at.least(startedAt);
+ expect(state[ARTIFACTS_KEY][0]).to.not.have.property('url');
+ });
+
+ it('preserves artifact edits made while a single artifact sync is running', async function () {
+ const startedAt = new Date().getTime();
+ let artifactReads = 0;
+ $.read = (key) => {
+ if (key === ARTIFACTS_KEY) {
+ artifactReads++;
+ if (artifactReads === 2) {
+ state[ARTIFACTS_KEY] = [
+ {
+ ...state[ARTIFACTS_KEY][0],
+ remark: 'edited while syncing',
+ cron: '55 23 * * *',
+ },
+ ];
+ }
+ }
+ return state[key] || [];
+ };
+
+ const res = await requestSyncArtifact('local-artifact');
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.body.status).to.equal('success');
+ expect(res.body.data.remark).to.equal('edited while syncing');
+ expect(res.body.data.cron).to.equal('55 23 * * *');
+ expect(res.body.data.updated).to.be.at.least(startedAt);
+ expect(res.body.data).to.not.have.property('url');
+ expect(state[ARTIFACTS_KEY][0].remark).to.equal('edited while syncing');
+ expect(state[ARTIFACTS_KEY][0].cron).to.equal('55 23 * * *');
+ expect(state[ARTIFACTS_KEY][0].updated).to.be.at.least(startedAt);
+ expect(state[ARTIFACTS_KEY][0]).to.not.have.property('url');
+ });
+
+ it('does not try to delete a remote file when only run time exists', async function () {
+ delete state[ARTIFACTS_KEY][0].url;
+
+ const res = await requestDeleteArtifact('local-artifact');
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.body.status).to.equal('success');
+ expect(res.body.data.remote).to.deep.equal({
+ attempted: false,
+ status: 'not_attempted',
+ });
+ expect(state[ARTIFACTS_KEY]).to.deep.equal([]);
+ });
+});
diff --git a/backend/src/test/restful/token.spec.js b/backend/src/test/restful/token.spec.js
new file mode 100644
index 0000000000..3143ed33be
--- /dev/null
+++ b/backend/src/test/restful/token.spec.js
@@ -0,0 +1,272 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import {
+ SETTINGS_KEY,
+ SUBS_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+
+let $;
+let createTokenItem;
+let state;
+let originalRead;
+let originalWrite;
+let originalInfo;
+let originalError;
+
+describe('token routes', function () {
+ before(async function () {
+ ({ default: $ } = require('@/core/app'));
+ ({ createTokenItem } = require('@/restful/token'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+ });
+
+ beforeEach(function () {
+ state = {
+ [SUBS_KEY]: [{ name: 'shared-sub' }],
+ [TOKENS_KEY]: [],
+ [SETTINGS_KEY]: {},
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.error = () => {};
+ });
+
+ it('stores exact datetime shares without converting them to expiresIn', function () {
+ const exp = Date.now() + 5 * 60 * 1000;
+
+ const token = createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'datetime-token',
+ },
+ {
+ mode: 'datetime',
+ exp,
+ },
+ );
+
+ expect(token.token).to.equal('datetime-token');
+ expect(token.mode).to.equal('datetime');
+ expect(token.exp).to.equal(exp);
+ expect(token).to.not.have.property('expiresIn');
+ expect(state[TOKENS_KEY][0].mode).to.equal('datetime');
+ expect(state[TOKENS_KEY][0].exp).to.equal(exp);
+ });
+
+ it('allows exact datetime shares in the past', function () {
+ const exp = Date.now() - 5 * 60 * 1000;
+
+ const token = createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'expired-token',
+ },
+ {
+ mode: 'datetime',
+ exp,
+ },
+ );
+
+ expect(token.mode).to.equal('datetime');
+ expect(token.exp).to.equal(exp);
+ expect(state[TOKENS_KEY]).to.have.length(1);
+ });
+
+ it('rejects invalid exact datetime shares', function () {
+ let capturedError;
+
+ try {
+ createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'bad-token',
+ },
+ {
+ mode: 'datetime',
+ exp: 'not-a-timestamp',
+ },
+ );
+ } catch (error) {
+ capturedError = error;
+ }
+
+ expect(capturedError).to.include({
+ type: 'RequestInvalidError',
+ code: 'INVALID_EXPIRATION_DATETIME',
+ });
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ });
+
+ it('rejects exact datetime shares that are not positive millisecond timestamps', function () {
+ for (const invalidExp of [
+ 0,
+ -1,
+ 1713753600,
+ '1713753600',
+ 1713753600000.5,
+ '1713753600000.5',
+ ]) {
+ let capturedError;
+
+ try {
+ createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: `invalid-ms-${String(invalidExp)}`,
+ },
+ {
+ mode: 'datetime',
+ exp: invalidExp,
+ },
+ );
+ } catch (error) {
+ capturedError = error;
+ }
+
+ expect(capturedError).to.include({
+ type: 'RequestInvalidError',
+ code: 'INVALID_EXPIRATION_DATETIME',
+ });
+ }
+
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ });
+
+ it('rejects empty exact datetime shares', function () {
+ for (const invalidExp of [null, '', ' ']) {
+ let capturedError;
+
+ try {
+ createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: `bad-token-${String(invalidExp)}`,
+ },
+ {
+ mode: 'datetime',
+ exp: invalidExp,
+ },
+ );
+ } catch (error) {
+ capturedError = error;
+ }
+
+ expect(capturedError).to.include({
+ type: 'RequestInvalidError',
+ code: 'INVALID_EXPIRATION_DATETIME',
+ });
+ }
+
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ });
+
+ it('rejects duration shares when expiresIn is missing', function () {
+ let capturedError;
+
+ try {
+ createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'duration-missing-expiration',
+ },
+ {
+ mode: 'duration',
+ },
+ );
+ } catch (error) {
+ capturedError = error;
+ }
+
+ expect(capturedError).to.include({
+ type: 'RequestInvalidError',
+ code: 'INVALID_EXPIRES_IN',
+ });
+ expect(state[TOKENS_KEY]).to.deep.equal([]);
+ });
+
+ it('keeps legacy duration behavior when mode is omitted', function () {
+ const token = createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'legacy-duration',
+ },
+ {
+ expiresIn: '1d',
+ exp: Date.now() + 10 * 24 * 60 * 60 * 1000,
+ },
+ );
+
+ expect(token.mode).to.equal('duration');
+ expect(token.expiresIn).to.equal('1d');
+ expect(token.exp).to.be.a('number');
+ });
+
+ it('infers legacy exact datetime behavior when mode is omitted but exp exists', function () {
+ const exp = Date.now() - 5 * 60 * 1000;
+
+ const token = createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'legacy-datetime',
+ },
+ {
+ exp,
+ },
+ );
+
+ expect(token.mode).to.equal('datetime');
+ expect(token.exp).to.equal(exp);
+ expect(token).to.not.have.property('expiresIn');
+ expect(state[TOKENS_KEY][0].mode).to.equal('datetime');
+ expect(state[TOKENS_KEY][0].exp).to.equal(exp);
+ });
+
+ it('ignores expiration metadata injected through payload', function () {
+ const token = createTokenItem(
+ {
+ type: 'sub',
+ name: 'shared-sub',
+ token: 'payload-mode-ignored',
+ mode: 'datetime',
+ exp: Date.now() + 60 * 1000,
+ expiresIn: '1d',
+ },
+ {},
+ );
+
+ expect(token).to.not.have.property('mode');
+ expect(token).to.not.have.property('exp');
+ expect(token).to.not.have.property('expiresIn');
+ expect(state[TOKENS_KEY][0]).to.not.have.property('mode');
+ expect(state[TOKENS_KEY][0]).to.not.have.property('exp');
+ expect(state[TOKENS_KEY][0]).to.not.have.property('expiresIn');
+ });
+});
diff --git a/backend/src/test/utils/artifact-cron.spec.js b/backend/src/test/utils/artifact-cron.spec.js
new file mode 100644
index 0000000000..5cc6b06df2
--- /dev/null
+++ b/backend/src/test/utils/artifact-cron.spec.js
@@ -0,0 +1,127 @@
+import { expect } from 'chai';
+import { afterEach, beforeEach, describe, it } from 'mocha';
+
+import $ from '@/core/app';
+import {
+ formatArtifactLogName,
+ hasArtifactCron,
+ normalizeArtifactCron,
+ runArtifactCron,
+ shouldSyncArtifactInGlobalCron,
+ startArtifactCronJobs,
+ stopArtifactCronJobs,
+ validateArtifactCron,
+} from '@/utils/artifact-cron';
+
+describe('artifact cron policy', function () {
+ let read;
+ let info;
+ let error;
+
+ beforeEach(function () {
+ read = $.read;
+ info = $.info;
+ error = $.error;
+
+ $.read = () => [];
+ $.info = () => {};
+ $.error = () => {};
+ });
+
+ afterEach(function () {
+ stopArtifactCronJobs();
+ $.read = read;
+ $.info = info;
+ $.error = error;
+ });
+
+ it('treats blank cron as unset', function () {
+ const artifact = { name: 'demo', cron: ' ' };
+
+ normalizeArtifactCron(artifact);
+
+ expect(artifact).to.not.have.property('cron');
+ expect(hasArtifactCron(artifact)).to.equal(false);
+ expect(shouldSyncArtifactInGlobalCron(artifact)).to.equal(true);
+ });
+
+ it('keeps valid cron and removes it from global cron eligibility', function () {
+ const artifact = { name: 'demo', cron: ' 55 23 * * * ' };
+
+ normalizeArtifactCron(artifact);
+
+ expect(artifact.cron).to.equal('55 23 * * *');
+ expect(hasArtifactCron(artifact)).to.equal(true);
+ expect(shouldSyncArtifactInGlobalCron(artifact)).to.equal(false);
+ });
+
+ it('rejects invalid cron expressions', function () {
+ expect(() => validateArtifactCron('not a cron')).to.throw(
+ /Artifact cron not a cron is invalid/,
+ );
+ });
+
+ it('formats display name for logs when available', function () {
+ expect(
+ formatArtifactLogName({
+ name: 'demo',
+ displayName: 'Demo Display',
+ }),
+ ).to.equal('demo (Demo Display)');
+ expect(
+ formatArtifactLogName({
+ name: 'legacy',
+ 'display-name': 'Legacy Display',
+ }),
+ ).to.equal('legacy (Legacy Display)');
+ expect(
+ formatArtifactLogName({
+ name: 'same',
+ displayName: 'same',
+ }),
+ ).to.equal('same');
+ });
+
+ it('skips overlapping runs for the same artifact', async function () {
+ const calls = [];
+ const logs = [];
+ let releaseFirstRun;
+ let firstRunStarted;
+ let runCount = 0;
+
+ const firstRunPromise = new Promise((resolve) => {
+ firstRunStarted = resolve;
+ });
+
+ $.info = (message) => {
+ logs.push(message);
+ };
+ startArtifactCronJobs(async (name) => {
+ calls.push(name);
+ runCount++;
+ if (runCount === 1) {
+ firstRunStarted();
+ await new Promise((resolve) => {
+ releaseFirstRun = resolve;
+ });
+ }
+ });
+
+ const firstRun = runArtifactCron('demo', 'demo', '* * * * * *');
+ await firstRunPromise;
+
+ await runArtifactCron('demo', 'demo', '* * * * * *');
+
+ expect(calls).to.deep.equal(['demo']);
+ expect(logs).to.include(
+ '[ARTIFACT CRON] demo * * * * * * skipped: previous run is still running',
+ );
+
+ releaseFirstRun();
+ await firstRun;
+
+ await runArtifactCron('demo', 'demo', '* * * * * *');
+
+ expect(calls).to.deep.equal(['demo', 'demo']);
+ });
+});
diff --git a/backend/src/test/utils/artifact-sync-policy.spec.js b/backend/src/test/utils/artifact-sync-policy.spec.js
new file mode 100644
index 0000000000..2b2a71e7b0
--- /dev/null
+++ b/backend/src/test/utils/artifact-sync-policy.spec.js
@@ -0,0 +1,69 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import {
+ resolveCronArtifactSyncPolicy,
+ shouldSkipCronArtifactWithoutUploadCredentials,
+} from '@/utils/artifact-sync-policy';
+
+describe('cron sync artifact policy', function () {
+ it('keeps upload-only schedules quiet without Gist credentials', function () {
+ const policy = resolveCronArtifactSyncPolicy({
+ settings: {},
+ artifacts: [{ name: 'upload-me', sync: true }],
+ });
+
+ expect(policy).to.deep.equal({
+ canUpload: false,
+ shouldRun: false,
+ });
+ });
+
+ it('runs only no-upload artifacts when mixed schedules have no Gist credentials', function () {
+ const policy = resolveCronArtifactSyncPolicy({
+ settings: {},
+ artifacts: [
+ { name: 'cache-refresh', sync: true, upload: false },
+ { name: 'gist-upload', sync: true },
+ ],
+ });
+
+ expect(policy).to.deep.equal({
+ canUpload: false,
+ shouldRun: true,
+ });
+ expect(
+ shouldSkipCronArtifactWithoutUploadCredentials(
+ { name: 'cache-refresh', upload: false },
+ policy,
+ ),
+ ).to.equal(false);
+ expect(
+ shouldSkipCronArtifactWithoutUploadCredentials(
+ { name: 'gist-upload' },
+ policy,
+ ),
+ ).to.equal(true);
+ });
+
+ it('allows every synced artifact when Gist credentials exist', function () {
+ const policy = resolveCronArtifactSyncPolicy({
+ settings: { gistToken: 'token' },
+ artifacts: [
+ { name: 'cache-refresh', sync: true, upload: false },
+ { name: 'gist-upload', sync: true },
+ ],
+ });
+
+ expect(policy).to.deep.equal({
+ canUpload: true,
+ shouldRun: true,
+ });
+ expect(
+ shouldSkipCronArtifactWithoutUploadCredentials(
+ { name: 'gist-upload' },
+ policy,
+ ),
+ ).to.equal(false);
+ });
+});
diff --git a/backend/src/test/utils/download.spec.js b/backend/src/test/utils/download.spec.js
new file mode 100644
index 0000000000..b8d165fcf4
--- /dev/null
+++ b/backend/src/test/utils/download.spec.js
@@ -0,0 +1,167 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import {
+ HEADERS_RESOURCE_CACHE_KEY,
+ RESOURCE_CACHE_KEY,
+ SETTINGS_KEY,
+} from '@/constants';
+
+let $;
+let openApi;
+let download;
+let resourceCache;
+let headersResourceCache;
+let originalRead;
+let originalWrite;
+let originalInfo;
+let originalError;
+let originalHTTP;
+let originalENV;
+let state;
+let tempDir;
+let previousDataBasePath;
+let capturedUrls;
+let errorLogs;
+
+describe('download github proxy regex', function () {
+ before(function () {
+ previousDataBasePath = process.env.SUB_STORE_DATA_BASE_PATH;
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sub-store-download-'));
+ process.env.SUB_STORE_DATA_BASE_PATH = tempDir;
+
+ ({ default: $ } = require('@/core/app'));
+ openApi = require('@/vendor/open-api');
+ ({ default: resourceCache } = require('@/utils/resource-cache'));
+ ({ default: headersResourceCache } = require(
+ '@/utils/headers-resource-cache'
+ ));
+ ({ default: download } = require('@/utils/download'));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ originalHTTP = openApi.HTTP;
+ originalENV = openApi.ENV;
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+
+ if (openApi) {
+ openApi.HTTP = originalHTTP;
+ openApi.ENV = originalENV;
+ }
+
+ if (previousDataBasePath == null) {
+ delete process.env.SUB_STORE_DATA_BASE_PATH;
+ } else {
+ process.env.SUB_STORE_DATA_BASE_PATH = previousDataBasePath;
+ }
+
+ if (tempDir) {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+
+ beforeEach(function () {
+ capturedUrls = [];
+ errorLogs = [];
+ state = {
+ [SETTINGS_KEY]: {
+ githubProxy: 'https://ghproxy.test',
+ githubProxyRegex: 'raw\\.githubusercontent\\.com',
+ },
+ [RESOURCE_CACHE_KEY]: '{}',
+ [HEADERS_RESOURCE_CACHE_KEY]: '{}',
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = () => {};
+ $.error = (message) => {
+ errorLogs.push(message);
+ };
+
+ openApi.ENV = () => ({
+ isNode: true,
+ isStash: false,
+ isLoon: false,
+ isShadowRocket: false,
+ isQX: false,
+ isSurge: false,
+ isGUIforCores: false,
+ isEgern: false,
+ isLanceX: false,
+ });
+ openApi.HTTP = () => ({
+ get: async ({ url }) => {
+ capturedUrls.push(url);
+ return {
+ body: 'test-body',
+ headers: {},
+ statusCode: 200,
+ };
+ },
+ });
+
+ resourceCache.revokeAll();
+ headersResourceCache.revokeAll();
+ });
+
+ it('prefixes matching download urls with the github proxy', async function () {
+ await download(
+ 'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ );
+
+ expect(capturedUrls).to.deep.equal([
+ 'https://ghproxy.test/https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ ]);
+ });
+
+ it('keeps download urls unchanged when the regex does not match', async function () {
+ await download('https://example.com/archive.txt');
+
+ expect(capturedUrls).to.deep.equal([
+ 'https://example.com/archive.txt',
+ ]);
+ });
+
+ it('matches regex patterns case-insensitively by default', async function () {
+ state[SETTINGS_KEY].githubProxyRegex = '^https://RAW\\.GITHUBUSERCONTENT\\.COM';
+
+ await download(
+ 'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ );
+
+ expect(capturedUrls).to.deep.equal([
+ 'https://ghproxy.test/https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ ]);
+ });
+
+ it('skips proxy prefixing when the regex is invalid', async function () {
+ state[SETTINGS_KEY].githubProxyRegex = '[';
+
+ await download(
+ 'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ );
+
+ expect(capturedUrls).to.deep.equal([
+ 'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
+ ]);
+ expect(errorLogs).to.have.length(1);
+ expect(errorLogs[0]).to.contain('GitHub 加速代理匹配正则无效');
+ });
+});
diff --git a/backend/src/test/utils/flow.spec.js b/backend/src/test/utils/flow.spec.js
new file mode 100644
index 0000000000..ac9fe64813
--- /dev/null
+++ b/backend/src/test/utils/flow.spec.js
@@ -0,0 +1,151 @@
+import { expect } from 'chai';
+import { after, before, beforeEach, describe, it } from 'mocha';
+
+import { HEADERS_RESOURCE_CACHE_KEY, SETTINGS_KEY } from '@/constants';
+
+let $;
+let openApi;
+let getFlowHeaders;
+let headersResourceCache;
+let originalRead;
+let originalWrite;
+let originalInfo;
+let originalError;
+let originalHTTP;
+let originalENV;
+let state;
+let capturedRequests;
+let infoLogs;
+
+describe('flow headers requests', function () {
+ before(function () {
+ ({ default: $ } = require('@/core/app'));
+ openApi = require('@/vendor/open-api');
+ ({ getFlowHeaders } = require('@/utils/flow'));
+ ({ default: headersResourceCache } = require(
+ '@/utils/headers-resource-cache'
+ ));
+
+ originalRead = $.read.bind($);
+ originalWrite = $.write.bind($);
+ originalInfo = $.info.bind($);
+ originalError = $.error.bind($);
+ originalHTTP = openApi.HTTP;
+ originalENV = openApi.ENV;
+ });
+
+ after(function () {
+ if ($) {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.error = originalError;
+ }
+
+ if (openApi) {
+ openApi.HTTP = originalHTTP;
+ openApi.ENV = originalENV;
+ }
+ });
+
+ beforeEach(function () {
+ capturedRequests = [];
+ infoLogs = [];
+ state = {
+ [SETTINGS_KEY]: {
+ defaultFlowUserAgent: 'DefaultFlowUA',
+ },
+ [HEADERS_RESOURCE_CACHE_KEY]: '{}',
+ };
+
+ $.read = (key) => state[key];
+ $.write = (data, key) => {
+ state[key] = data;
+ return true;
+ };
+ $.info = (message) => {
+ infoLogs.push(message);
+ };
+ $.error = () => {};
+
+ openApi.ENV = () => ({
+ isNode: true,
+ isStash: false,
+ isLoon: false,
+ isShadowRocket: false,
+ isQX: false,
+ isSurge: false,
+ isGUIforCores: false,
+ isEgern: false,
+ isLanceX: false,
+ });
+ openApi.HTTP = () => ({
+ head: async (options) => {
+ capturedRequests.push({ method: 'HEAD', options });
+ return {
+ headers: {
+ 'subscription-userinfo':
+ 'upload=1; download=2; total=10',
+ },
+ };
+ },
+ get: async (options) => {
+ capturedRequests.push({ method: 'GET', options });
+ return {
+ body: 'upload=1; download=2; total=10',
+ headers: {},
+ statusCode: 200,
+ };
+ },
+ });
+
+ headersResourceCache.revokeAll();
+ });
+
+ it('uses flowHeaders from the subscription URL for flow requests', async function () {
+ await getFlowHeaders(
+ 'https://example.com/sub',
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ JSON.stringify({
+ 'X-Flow-Token': 'token-1',
+ 'User-Agent': 'CustomFlowUA',
+ }),
+ );
+
+ expect(capturedRequests[0].method).to.equal('HEAD');
+ expect(capturedRequests[0].options.headers).to.deep.include({
+ 'x-flow-token': 'token-1',
+ 'user-agent': 'CustomFlowUA',
+ });
+ expect(infoLogs[0]).to.include(
+ '{"user-agent":"CustomFlowUA","x-flow-token":"token-1"}',
+ );
+ });
+
+ it('uses headers embedded in a flow URL such as subUserinfo links', async function () {
+ await getFlowHeaders(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ `https://example.com/info#headers=${encodeURIComponent(
+ JSON.stringify({ 'X-Sub-Token': 'token-2' }),
+ )}`,
+ );
+
+ expect(capturedRequests[0].method).to.equal('GET');
+ expect(capturedRequests[0].options.url).to.equal(
+ 'https://example.com/info',
+ );
+ expect(capturedRequests[0].options.headers).to.deep.include({
+ 'x-sub-token': 'token-2',
+ 'user-agent': 'DefaultFlowUA',
+ });
+ expect(infoLogs[0]).to.include(
+ '{"user-agent":"DefaultFlowUA","x-sub-token":"token-2"}',
+ );
+ });
+});
diff --git a/backend/src/test/utils/gist.spec.js b/backend/src/test/utils/gist.spec.js
new file mode 100644
index 0000000000..5bb7e25842
--- /dev/null
+++ b/backend/src/test/utils/gist.spec.js
@@ -0,0 +1,411 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import $ from '@/core/app';
+import { SETTINGS_KEY } from '@/constants';
+import Gist, {
+ describeGistApiErrorResponse,
+ getGithubGistBaseURL,
+ hasGistSyncCredentials,
+} from '@/utils/gist';
+import {
+ normalizeArtifactSyncBatchSize,
+ syncToGist,
+} from '@/restful/artifacts';
+import { uploadArtifactBatches } from '@/restful/sync';
+
+describe('Gist GitHub API URL', function () {
+ it('uses the default GitHub API URL when unset', function () {
+ expect(getGithubGistBaseURL()).to.equal('https://api.github.com');
+ });
+
+ it('uses the default GitHub API URL when blank', function () {
+ expect(
+ getGithubGistBaseURL({
+ githubApiUrl: ' ',
+ }),
+ ).to.equal('https://api.github.com');
+ });
+
+ it('applies GitHub proxy only to the default GitHub API URL', function () {
+ expect(
+ getGithubGistBaseURL({
+ githubProxy: 'https://proxy.example.com/',
+ }),
+ ).to.equal('https://proxy.example.com/https://api.github.com');
+ });
+
+ it('does not apply GitHub proxy to a custom GitHub API URL', function () {
+ expect(
+ getGithubGistBaseURL({
+ githubApiUrl: 'https://litegist.example.com/api/',
+ githubProxy: 'https://proxy.example.com/',
+ }),
+ ).to.equal('https://litegist.example.com/api');
+ });
+
+ it('includes HTTP status when describing Gist API errors', function () {
+ expect(
+ describeGistApiErrorResponse({
+ statusCode: 500,
+ body: JSON.stringify({
+ message:
+ 'Internal Server Error: Error: D1 query budget exceeded',
+ }),
+ }),
+ ).to.equal(
+ 'ERROR: HTTP 500: Internal Server Error: Error: D1 query budget exceeded',
+ );
+ });
+
+ it('allows token-only Gist-compatible sync settings', function () {
+ expect(
+ hasGistSyncCredentials({
+ gistToken: 'token',
+ githubUser: '',
+ }),
+ ).to.equal(true);
+ });
+
+ it('skips sync when the token is missing', function () {
+ expect(
+ hasGistSyncCredentials({
+ githubUser: 'xream',
+ }),
+ ).to.equal(false);
+ });
+
+ it('normalizes artifact sync batch size', function () {
+ expect(normalizeArtifactSyncBatchSize()).to.equal(10);
+ expect(normalizeArtifactSyncBatchSize('')).to.equal(10);
+ expect(normalizeArtifactSyncBatchSize('0')).to.equal(10);
+ expect(normalizeArtifactSyncBatchSize('2.9')).to.equal(2);
+ });
+
+ it('keeps an existing Gist alive with a fallback file when a delete would empty it', async function () {
+ const manager = Object.create(Gist.prototype);
+ let patchBody;
+
+ manager.syncPlatform = '';
+ manager.locate = async () => ({
+ id: 'gist-id',
+ files: {
+ artifact: {
+ filename: 'artifact',
+ },
+ },
+ });
+ manager.http = {
+ patch: async ({ body }) => {
+ patchBody = JSON.parse(body);
+ return {
+ body: JSON.stringify({
+ files: {
+ '.sub-store-placeholder': {
+ filename: '.sub-store-placeholder',
+ },
+ },
+ }),
+ };
+ },
+ };
+
+ const response = await manager.upload(
+ {
+ artifact: {
+ content: '',
+ },
+ },
+ {
+ emptyFileFallback: {
+ filename: '.sub-store-placeholder',
+ content: 'placeholder',
+ },
+ },
+ );
+
+ expect(patchBody.files.artifact).to.equal(null);
+ expect(patchBody.files['.sub-store-placeholder']).to.deep.equal({
+ content: 'placeholder',
+ });
+ expect(response.subStoreUploadMeta.emptyFileFallback).to.deep.equal({
+ status: 'created',
+ filename: '.sub-store-placeholder',
+ });
+ });
+
+ it('removes the fallback file when a real file is uploaded later', async function () {
+ const manager = Object.create(Gist.prototype);
+ let patchBody;
+
+ manager.syncPlatform = '';
+ manager.locate = async () => ({
+ id: 'gist-id',
+ files: {
+ '.sub-store-placeholder': {
+ filename: '.sub-store-placeholder',
+ },
+ },
+ });
+ manager.http = {
+ patch: async ({ body }) => {
+ patchBody = JSON.parse(body);
+ return {
+ body: JSON.stringify({
+ files: {
+ artifact: {
+ filename: 'artifact',
+ },
+ },
+ }),
+ };
+ },
+ };
+
+ const response = await manager.upload(
+ {
+ artifact: {
+ content: 'real content',
+ },
+ },
+ {
+ emptyFileFallback: {
+ filename: '.sub-store-placeholder',
+ content: 'placeholder',
+ },
+ },
+ );
+
+ expect(patchBody.files.artifact).to.deep.equal({
+ content: 'real content',
+ });
+ expect(patchBody.files['.sub-store-placeholder']).to.equal(null);
+ expect(response.subStoreUploadMeta.emptyFileFallback).to.deep.equal({
+ status: 'removed',
+ filename: '.sub-store-placeholder',
+ });
+ });
+
+ it('passes the artifact placeholder fallback to syncToGist by default', async function () {
+ const originalRead = $.read.bind($);
+ const originalWrite = $.write.bind($);
+ const originalInfo = $.info.bind($);
+ const originalUpload = Gist.prototype.upload;
+ let capturedOptions;
+ let writtenSettings;
+ const infoMessages = [];
+
+ $.read = (key) => {
+ if (key === SETTINGS_KEY) {
+ return {
+ gistToken: 'token',
+ };
+ }
+ return originalRead(key);
+ };
+ $.write = (data, key) => {
+ if (key === SETTINGS_KEY) {
+ writtenSettings = data;
+ return true;
+ }
+ return originalWrite(data, key);
+ };
+ $.info = (message) => {
+ infoMessages.push(message);
+ };
+ Gist.prototype.upload = async function (_, options) {
+ capturedOptions = options;
+ return {
+ body: JSON.stringify({
+ html_url: 'https://gist.example.com/sub-store',
+ files: {},
+ }),
+ };
+ };
+
+ try {
+ await syncToGist({
+ artifact: {
+ content: 'real content',
+ },
+ });
+ } finally {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ Gist.prototype.upload = originalUpload;
+ }
+
+ expect(capturedOptions.emptyFileFallback).to.deep.equal({
+ filename: '.sub-store-placeholder',
+ content:
+ 'Sub-Store placeholder\nThis file keeps the Gist alive when all sync configuration files are deleted.',
+ });
+ expect(writtenSettings.artifactStore).to.equal(
+ 'https://gist.example.com/sub-store',
+ );
+ expect(writtenSettings.artifactStoreStatus).to.equal('VALID');
+ expect(infoMessages).to.include(
+ '准备同步 Gist: 文件数 1, 总大小 12 B, 最大文件 artifact (12 B)',
+ );
+ });
+
+ it('continues uploading artifact batches after one batch fails', async function () {
+ const originalRead = $.read.bind($);
+ const originalWrite = $.write.bind($);
+ const originalInfo = $.info.bind($);
+ const originalLog = $.log.bind($);
+ const originalError = $.error.bind($);
+ const originalUpload = Gist.prototype.upload;
+ const writes = [];
+
+ $.read = (key) => {
+ if (key === SETTINGS_KEY) {
+ return {
+ gistToken: 'token',
+ artifactSyncBatchSize: 1,
+ };
+ }
+ return originalRead(key);
+ };
+ $.write = (data, key) => {
+ if (key === SETTINGS_KEY) {
+ writes.push(data);
+ return true;
+ }
+ return originalWrite(data, key);
+ };
+ $.info = () => {};
+ $.log = () => {};
+ $.error = () => {};
+ Gist.prototype.upload = async function (files) {
+ const filename = Object.keys(files)[0];
+ if (filename === 'b') {
+ throw new Error('batch failed');
+ }
+ return {
+ body: JSON.stringify({
+ html_url: 'https://gist.example.com/sub-store',
+ files: {
+ [filename]: {
+ raw_url: `https://gist.example.com/raw/hash/${filename}`,
+ },
+ },
+ }),
+ };
+ };
+
+ const allArtifacts = [
+ { name: 'a', sync: true, source: 'sub-a' },
+ { name: 'b', sync: true, source: 'sub-b' },
+ { name: 'c', sync: true, source: 'sub-c' },
+ ];
+ const invalid = [];
+
+ try {
+ const uploaded = await uploadArtifactBatches({
+ allArtifacts,
+ files: {
+ a: { content: 'A' },
+ b: { content: 'B' },
+ c: { content: 'C' },
+ },
+ valid: ['a', 'b', 'c'],
+ invalid,
+ });
+
+ expect(uploaded).to.deep.equal(['a', 'c']);
+ expect(invalid).to.deep.equal(['b']);
+ expect(allArtifacts[0].url).to.equal(
+ 'https://gist.example.com/raw/a',
+ );
+ expect(allArtifacts[1].url).to.equal(undefined);
+ expect(allArtifacts[2].url).to.equal(
+ 'https://gist.example.com/raw/c',
+ );
+ expect(writes).to.have.length(2);
+ } finally {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.log = originalLog;
+ $.error = originalError;
+ Gist.prototype.upload = originalUpload;
+ }
+ });
+
+ it('skips artifact batches for items that disabled upload', async function () {
+ const originalRead = $.read.bind($);
+ const originalWrite = $.write.bind($);
+ const originalInfo = $.info.bind($);
+ const originalLog = $.log.bind($);
+ const originalError = $.error.bind($);
+ const originalUpload = Gist.prototype.upload;
+ let capturedFiles;
+
+ $.read = (key) => {
+ if (key === SETTINGS_KEY) {
+ return {
+ gistToken: 'token',
+ artifactSyncBatchSize: 10,
+ };
+ }
+ return originalRead(key);
+ };
+ $.write = (data, key) => {
+ if (key === SETTINGS_KEY) {
+ return true;
+ }
+ return originalWrite(data, key);
+ };
+ $.info = () => {};
+ $.log = () => {};
+ $.error = () => {};
+ Gist.prototype.upload = async function (files) {
+ capturedFiles = files;
+ return {
+ body: JSON.stringify({
+ html_url: 'https://gist.example.com/sub-store',
+ files: {
+ b: {
+ raw_url: 'https://gist.example.com/raw/hash/b',
+ },
+ },
+ }),
+ };
+ };
+
+ const allArtifacts = [
+ { name: 'a', sync: true, source: 'sub-a', upload: false },
+ { name: 'b', sync: true, source: 'sub-b' },
+ ];
+ const invalid = [];
+
+ try {
+ const uploaded = await uploadArtifactBatches({
+ allArtifacts,
+ files: {
+ a: { content: 'A' },
+ b: { content: 'B' },
+ },
+ valid: ['a', 'b'],
+ invalid,
+ });
+
+ expect(Object.keys(capturedFiles)).to.deep.equal(['b']);
+ expect(uploaded).to.deep.equal(['b']);
+ expect(invalid).to.deep.equal([]);
+ expect(allArtifacts[0].url).to.equal(undefined);
+ expect(allArtifacts[1].url).to.equal(
+ 'https://gist.example.com/raw/b',
+ );
+ } finally {
+ $.read = originalRead;
+ $.write = originalWrite;
+ $.info = originalInfo;
+ $.log = originalLog;
+ $.error = originalError;
+ Gist.prototype.upload = originalUpload;
+ }
+ });
+});
diff --git a/backend/src/utils/archive.js b/backend/src/utils/archive.js
new file mode 100644
index 0000000000..ff7bfca310
--- /dev/null
+++ b/backend/src/utils/archive.js
@@ -0,0 +1,200 @@
+import $ from '@/core/app';
+import {
+ ARTIFACTS_KEY,
+ COLLECTIONS_KEY,
+ FILES_KEY,
+ ARCHIVES_KEY,
+ SUBS_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+import { findByName } from '@/utils/database';
+import {
+ RequestInvalidError,
+ ResourceNotFoundError,
+} from '@/restful/errors';
+
+function cloneSnapshot(value) {
+ if (value === undefined) {
+ return undefined;
+ }
+ return JSON.parse(JSON.stringify(value));
+}
+
+function ensureArchiveStore() {
+ const current = $.read(ARCHIVES_KEY);
+ if (Array.isArray(current)) {
+ return current;
+ }
+
+ const entries = [];
+ $.write(entries, ARCHIVES_KEY);
+ return entries;
+}
+
+function getArchiveEntries() {
+ return ensureArchiveStore();
+}
+
+function writeArchiveEntries(entries) {
+ $.write(entries, ARCHIVES_KEY);
+}
+
+function createArchiveId() {
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
+}
+
+function insertArchiveEntry(entry) {
+ const entries = ensureArchiveStore();
+ const nextEntry = {
+ id: createArchiveId(),
+ archivedAt: Date.now(),
+ ...entry,
+ };
+ entries.unshift(nextEntry);
+ writeArchiveEntries(entries);
+ return nextEntry;
+}
+
+function getArchiveEntry(id) {
+ return ensureArchiveStore().find((entry) => entry.id === id);
+}
+
+function getRequiredArchiveEntry(id) {
+ const entry = getArchiveEntry(id);
+ if (!entry) {
+ throw new ResourceNotFoundError(
+ 'ARCHIVE_ENTRY_NOT_FOUND',
+ `Archive entry ${id} does not exist`,
+ );
+ }
+ return entry;
+}
+
+function removeArchiveEntry(id) {
+ const entries = ensureArchiveStore();
+ const index = entries.findIndex((entry) => entry.id === id);
+ if (index === -1) {
+ throw new ResourceNotFoundError(
+ 'ARCHIVE_ENTRY_NOT_FOUND',
+ `Archive entry ${id} does not exist`,
+ );
+ }
+ const [removed] = entries.splice(index, 1);
+ writeArchiveEntries(entries);
+ return removed;
+}
+
+function sortArchiveEntries(ids) {
+ if (!Array.isArray(ids)) {
+ throw new RequestInvalidError(
+ 'INVALID_SORT_PAYLOAD',
+ 'Archive sort payload must be an array of ids',
+ );
+ }
+
+ const entries = ensureArchiveStore();
+ const orderMap = new Map(ids.map((id, index) => [id, index]));
+ entries.sort((a, b) => {
+ const left = orderMap.has(a.id)
+ ? orderMap.get(a.id)
+ : Number.MAX_SAFE_INTEGER;
+ const right = orderMap.has(b.id)
+ ? orderMap.get(b.id)
+ : Number.MAX_SAFE_INTEGER;
+ return left - right;
+ });
+ writeArchiveEntries(entries);
+ return entries;
+}
+
+function buildArchiveEntry(itemType, snapshot, overrides = {}) {
+ return insertArchiveEntry({
+ itemType,
+ name: snapshot.name,
+ displayName: snapshot.displayName,
+ remark: snapshot.remark,
+ tag: cloneSnapshot(snapshot.tag),
+ snapshot: cloneSnapshot(snapshot),
+ ...overrides,
+ });
+}
+
+function archiveSubscription(name) {
+ const allSubs = $.read(SUBS_KEY) || [];
+ const sub = findByName(allSubs, name);
+ if (!sub) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Subscription ${name} does not exist!`,
+ );
+ }
+ return buildArchiveEntry('sub', sub);
+}
+
+function archiveCollection(name) {
+ const allCollections = $.read(COLLECTIONS_KEY) || [];
+ const collection = findByName(allCollections, name);
+ if (!collection) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Collection ${name} does not exist!`,
+ );
+ }
+ return buildArchiveEntry('col', collection);
+}
+
+function archiveFile(name) {
+ const allFiles = $.read(FILES_KEY) || [];
+ const file = findByName(allFiles, name);
+ if (!file) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `File ${name} does not exist!`,
+ );
+ }
+ return buildArchiveEntry('file', file);
+}
+
+function archiveArtifact(name) {
+ const allArtifacts = $.read(ARTIFACTS_KEY) || [];
+ const artifact = findByName(allArtifacts, name);
+ if (!artifact) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Artifact ${name} does not exist!`,
+ );
+ }
+ return buildArchiveEntry('artifact', artifact);
+}
+
+function archiveShare(token, type, name) {
+ const allTokens = $.read(TOKENS_KEY) || [];
+ const share = allTokens.find(
+ (item) => item.token === token && item.type === type && item.name === name,
+ );
+ if (!share) {
+ throw new ResourceNotFoundError(
+ 'RESOURCE_NOT_FOUND',
+ `Share ${type}/${name}/${token} does not exist!`,
+ );
+ }
+ return buildArchiveEntry('share', share, {
+ shareType: share.type,
+ });
+}
+
+export {
+ archiveArtifact,
+ archiveCollection,
+ archiveFile,
+ archiveShare,
+ archiveSubscription,
+ getArchiveEntries,
+ getArchiveEntry,
+ getRequiredArchiveEntry,
+ insertArchiveEntry,
+ removeArchiveEntry,
+ sortArchiveEntries,
+ writeArchiveEntries,
+ ensureArchiveStore,
+};
diff --git a/backend/src/utils/artifact-cron.js b/backend/src/utils/artifact-cron.js
new file mode 100644
index 0000000000..064abc3cef
--- /dev/null
+++ b/backend/src/utils/artifact-cron.js
@@ -0,0 +1,151 @@
+import $ from '@/core/app';
+import { ARTIFACTS_KEY } from '@/constants';
+import { RequestInvalidError } from '@/restful/errors';
+
+const artifactCronJobs = new Map();
+const runningArtifactCronNames = new Set();
+let syncArtifactByName;
+
+function getArtifactCron(artifact) {
+ const cron = artifact?.cron == null ? '' : `${artifact.cron}`.trim();
+ return cron;
+}
+
+function hasArtifactCron(artifact) {
+ return getArtifactCron(artifact).length > 0;
+}
+
+function getArtifactDisplayName(artifact) {
+ const displayName = artifact?.displayName ?? artifact?.['display-name'];
+ return displayName == null ? '' : `${displayName}`.trim();
+}
+
+function formatArtifactLogName(artifact) {
+ if (!artifact || typeof artifact !== 'object') return `${artifact}`;
+
+ const name = artifact.name == null ? '' : `${artifact.name}`;
+ const displayName = getArtifactDisplayName(artifact);
+ if (!displayName || displayName === name) return name;
+
+ return `${name} (${displayName})`;
+}
+
+function shouldSyncArtifactInGlobalCron(artifact) {
+ return !hasArtifactCron(artifact);
+}
+
+function validateArtifactCron(cron) {
+ if (!cron || !$.env.isNode) return;
+
+ try {
+ const { CronTime } = eval(`require("cron")`);
+ new CronTime(cron);
+ } catch (e) {
+ throw new RequestInvalidError(
+ 'INVALID_ARTIFACT_CRON',
+ `Artifact cron ${cron} is invalid.`,
+ `Reason: ${e.message ?? e}`,
+ );
+ }
+}
+
+function normalizeArtifactCron(artifact) {
+ if (!artifact || typeof artifact !== 'object') return artifact;
+
+ const cron = getArtifactCron(artifact);
+ if (!cron) {
+ delete artifact.cron;
+ return artifact;
+ }
+
+ validateArtifactCron(cron);
+ artifact.cron = cron;
+ return artifact;
+}
+
+function stopArtifactCronJobs() {
+ artifactCronJobs.forEach((job) => job.stop());
+ artifactCronJobs.clear();
+}
+
+async function runArtifactCron(name, logName, cron) {
+ if (runningArtifactCronNames.has(name)) {
+ $.info(
+ `[ARTIFACT CRON] ${logName} ${cron} skipped: previous run is still running`,
+ );
+ return;
+ }
+
+ runningArtifactCronNames.add(name);
+ try {
+ $.info(`[ARTIFACT CRON] ${logName} ${cron} started`);
+ await syncArtifactByName(name);
+ $.info(`[ARTIFACT CRON] ${logName} ${cron} finished`);
+ } catch (e) {
+ $.error(
+ `[ARTIFACT CRON] ${logName} ${cron} error: ${e.message ?? e}`,
+ );
+ } finally {
+ runningArtifactCronNames.delete(name);
+ }
+}
+
+function scheduleArtifactCron(artifact) {
+ const cron = getArtifactCron(artifact);
+ const name = artifact.name;
+ const logName = formatArtifactLogName(artifact);
+ const { CronJob } = eval(`require("cron")`);
+
+ try {
+ const job = new CronJob(
+ cron,
+ async function () {
+ await runArtifactCron(name, logName, cron);
+ },
+ null,
+ true,
+ );
+ artifactCronJobs.set(name, job);
+ $.info(`[ARTIFACT CRON] ${logName} ${cron} scheduled`);
+ } catch (e) {
+ $.error(
+ `[ARTIFACT CRON] ${logName} ${cron} schedule error: ${
+ e.message ?? e
+ }`,
+ );
+ }
+}
+
+function refreshArtifactCronJobs() {
+ if (!$.env.isNode || !syncArtifactByName) return;
+
+ stopArtifactCronJobs();
+
+ const storedArtifacts = $.read(ARTIFACTS_KEY);
+ const artifacts = Array.isArray(storedArtifacts) ? storedArtifacts : [];
+ artifacts
+ .filter((artifact) => artifact.sync && artifact.source)
+ .filter(hasArtifactCron)
+ .forEach(scheduleArtifactCron);
+}
+
+function startArtifactCronJobs(handler) {
+ if (!$.env.isNode) return;
+
+ syncArtifactByName = handler;
+ refreshArtifactCronJobs();
+}
+
+export {
+ formatArtifactLogName,
+ getArtifactDisplayName,
+ getArtifactCron,
+ hasArtifactCron,
+ normalizeArtifactCron,
+ refreshArtifactCronJobs,
+ runArtifactCron,
+ shouldSyncArtifactInGlobalCron,
+ startArtifactCronJobs,
+ stopArtifactCronJobs,
+ validateArtifactCron,
+};
diff --git a/backend/src/utils/artifact-sync-policy.js b/backend/src/utils/artifact-sync-policy.js
new file mode 100644
index 0000000000..3dc029a748
--- /dev/null
+++ b/backend/src/utils/artifact-sync-policy.js
@@ -0,0 +1,34 @@
+import { hasGistSyncCredentials } from '@/utils/gist';
+
+function shouldUploadArtifactForCron(artifact) {
+ return artifact?.upload !== false;
+}
+
+function resolveCronArtifactSyncPolicy({ artifacts = [], settings = {} } = {}) {
+ const hasUploadCredentials = hasGistSyncCredentials(settings);
+ const shouldSync = artifacts.some((artifact) => artifact.sync);
+ const shouldProduceWithUpload = artifacts.some(
+ (artifact) => artifact.sync && shouldUploadArtifactForCron(artifact),
+ );
+ const shouldProduceWithoutUpload = artifacts.some(
+ (artifact) => artifact.sync && !shouldUploadArtifactForCron(artifact),
+ );
+
+ return {
+ canUpload: hasUploadCredentials || !shouldProduceWithUpload,
+ shouldRun:
+ shouldSync && (hasUploadCredentials || shouldProduceWithoutUpload),
+ };
+}
+
+function shouldSkipCronArtifactWithoutUploadCredentials(
+ artifact,
+ { canUpload },
+) {
+ return !canUpload && shouldUploadArtifactForCron(artifact);
+}
+
+export {
+ resolveCronArtifactSyncPolicy,
+ shouldSkipCronArtifactWithoutUploadCredentials,
+};
diff --git a/backend/src/utils/create-item-position.js b/backend/src/utils/create-item-position.js
new file mode 100644
index 0000000000..b1ac61696d
--- /dev/null
+++ b/backend/src/utils/create-item-position.js
@@ -0,0 +1,9 @@
+import { SETTINGS_KEY } from '@/constants';
+import $ from '@/core/app';
+
+export function getCreateItemPosition() {
+ const createItemPosition =
+ $.read(SETTINGS_KEY)?.appearanceSetting?.createItemPosition;
+
+ return createItemPosition === 'top' ? 'top' : 'bottom';
+}
diff --git a/backend/src/utils/database.js b/backend/src/utils/database.js
new file mode 100644
index 0000000000..95b8b6188b
--- /dev/null
+++ b/backend/src/utils/database.js
@@ -0,0 +1,26 @@
+export function findByName(list, name, field = 'name') {
+ return list.find((item) => item[field] === name);
+}
+
+export function findIndexByName(list, name, field = 'name') {
+ return list.findIndex((item) => item[field] === name);
+}
+
+export function deleteByName(list, name, field = 'name') {
+ const idx = findIndexByName(list, name, field);
+ list.splice(idx, 1);
+}
+
+export function updateByName(list, name, newItem, field = 'name') {
+ const idx = findIndexByName(list, name, field);
+ list[idx] = newItem;
+}
+
+export function insertByPosition(list, item, position = 'bottom') {
+ if (position === 'top') {
+ list.unshift(item);
+ return;
+ }
+
+ list.push(item);
+}
diff --git a/backend/src/utils/debug-logs.js b/backend/src/utils/debug-logs.js
new file mode 100644
index 0000000000..a32a9b014f
--- /dev/null
+++ b/backend/src/utils/debug-logs.js
@@ -0,0 +1,333 @@
+import { DEFAULT_LOGS_MAX_COUNT, LOGS_KEY, SETTINGS_KEY } from '@/constants';
+
+const DEFAULT_LOG_LIMIT = 200;
+const MAX_LOGS_MAX_COUNT = 10000;
+const LOGS_MAX_COUNT_CACHE_TTL_MS = 1000;
+const LOG_LEVELS = ['log', 'info', 'warn', 'error', 'debug'];
+const DEFAULT_LOG_SCOPE = 'unknown';
+const SUB_STORE_BANNER_VERSION_RE = /^Sub-Store\s+--\s+v\d+(?:\.\d+){0,3}$/;
+const SUB_STORE_BANNER_SEPARATOR_RE = /^\u2505{8,}$/;
+const SUB_STORE_LOG_PREFIX_RE =
+ /^\[sub-store\]\s+(?:LOG|INFO|WARN|ERROR|DEBUG):\s*/;
+const LOG_WITH_SCOPE_AND_LEVEL_PREFIX_RE =
+ /^\[[^\]\r\n]+\]\s+(?:LOG|INFO|WARN|ERROR|DEBUG):\s*/i;
+const LOG_WITH_SCOPE_PREFIX_RE = /^\[([^\]\r\n]+)\]\s*/;
+const DEFAULT_IGNORED_SINGLE_LINE_LOG_MESSAGE_RES = [
+ /^(?:.+\sParser|Rule parser \[.+\]) is activated!?$/,
+ // /^Pre-processor \[Fallback Base64 Pre-processor\] activated$/,
+ /^Pre-processor \[.+\] activated!?$/,
+ /^Fallback Base64 Pre-processor error: decoded line does not start with protocol$/,
+];
+
+let consoleCaptureInstalled = false;
+let isAppendingConsoleLog = false;
+let cachedLogsMaxCount = null;
+let cachedLogsMaxCountExpiresAt = 0;
+
+export function normalizeLogLimit(value, fallback = DEFAULT_LOG_LIMIT) {
+ const normalizedFallback = normalizePositiveInteger(
+ fallback,
+ DEFAULT_LOG_LIMIT,
+ MAX_LOGS_MAX_COUNT,
+ );
+ return normalizePositiveInteger(
+ value,
+ normalizedFallback,
+ MAX_LOGS_MAX_COUNT,
+ );
+}
+
+export function normalizeLogsMaxCount(
+ value,
+ fallback = DEFAULT_LOGS_MAX_COUNT,
+) {
+ const normalizedFallback = normalizeNonNegativeInteger(
+ fallback,
+ DEFAULT_LOGS_MAX_COUNT,
+ MAX_LOGS_MAX_COUNT,
+ );
+ return normalizeNonNegativeInteger(
+ value,
+ normalizedFallback,
+ MAX_LOGS_MAX_COUNT,
+ );
+}
+
+export function formatLogArguments(args) {
+ return Array.from(args || [])
+ .map(formatLogValue)
+ .join(' ');
+}
+
+export function readLogEntries($) {
+ try {
+ const raw = $.read(LOGS_KEY);
+ if (!raw) return [];
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
+ if (!Array.isArray(parsed)) return [];
+ return parsed.filter(isLogEntry);
+ } catch {
+ return [];
+ }
+}
+
+export function persistLogEntries($, logs) {
+ $.write(JSON.stringify(Array.isArray(logs) ? logs : []), LOGS_KEY);
+}
+
+export function appendLogEntry($, level, args) {
+ try {
+ const maxCount = getCachedLogsMaxCount($);
+ if (maxCount === 0) return;
+
+ const message = formatLogArguments(args);
+ if (shouldIgnoreLogMessage(message)) return;
+
+ const logs = readLogEntries($);
+ const normalizedLevel = normalizeLogLevel(level);
+ logs.push({
+ id: createLogId(),
+ time: Date.now(),
+ level: normalizedLevel,
+ message: normalizeStoredLogMessage(message, normalizedLevel),
+ });
+ persistLogEntries($, logs.slice(-maxCount));
+ } catch {
+ // Logging must never break the caller or recursively log failures.
+ }
+}
+
+export function clearLogSettingsCache() {
+ cachedLogsMaxCount = null;
+ cachedLogsMaxCountExpiresAt = 0;
+}
+
+export function getLogEntries($, query = {}) {
+ const settings = $.read(SETTINGS_KEY) || {};
+ const maxCount = normalizeLogsMaxCount(settings.logsMaxCount);
+ if (maxCount === 0) {
+ return {
+ logs: [],
+ total: 0,
+ maxCount,
+ };
+ }
+
+ const limit = normalizeLogLimit(query.limit, maxCount);
+ const keyword = `${query.keyword || ''}`.trim();
+ const isRegex = normalizeBooleanQuery(query.regex);
+ const ignoreCase = normalizeBooleanQuery(query.ignoreCase);
+ const keywordMatcher = createKeywordMatcher(keyword, {
+ isRegex,
+ ignoreCase,
+ });
+ let logs = readLogEntries($);
+ const sanitizedLogs = logs.filter((entry) => !shouldIgnoreLogEntry(entry));
+ const shouldPersistSanitizedLogs = sanitizedLogs.length !== logs.length;
+ logs = sanitizedLogs;
+
+ if (logs.length > maxCount) {
+ logs = logs.slice(-maxCount);
+ persistLogEntries($, logs);
+ } else if (shouldPersistSanitizedLogs) {
+ persistLogEntries($, logs);
+ }
+
+ if (keywordMatcher) {
+ logs = logs.filter((entry) => keywordMatcher(getSearchText(entry)));
+ }
+
+ return {
+ logs: logs.slice(-limit).reverse(),
+ total: logs.length,
+ maxCount,
+ };
+}
+
+export function clearLogEntries($) {
+ persistLogEntries($, []);
+}
+
+export function installConsoleLogCapture($) {
+ if (consoleCaptureInstalled || typeof console === 'undefined') return;
+ consoleCaptureInstalled = true;
+
+ LOG_LEVELS.forEach((level) => {
+ const original = console[level];
+ if (typeof original !== 'function') return;
+ console[level] = function (...args) {
+ original.apply(console, args);
+ if (isAppendingConsoleLog) return;
+ try {
+ isAppendingConsoleLog = true;
+ appendLogEntry($, level, args);
+ } finally {
+ isAppendingConsoleLog = false;
+ }
+ };
+ });
+}
+
+function normalizePositiveInteger(value, fallback, max) {
+ const number = Number(value);
+ if (!isFinite(number) || number <= 0) return fallback;
+ return Math.min(Math.floor(number), max);
+}
+
+function getCachedLogsMaxCount($) {
+ const now = Date.now();
+ if (cachedLogsMaxCount !== null && now < cachedLogsMaxCountExpiresAt) {
+ return cachedLogsMaxCount;
+ }
+
+ const settings = $.read(SETTINGS_KEY) || {};
+ cachedLogsMaxCount = normalizeLogsMaxCount(settings.logsMaxCount);
+ cachedLogsMaxCountExpiresAt = now + LOGS_MAX_COUNT_CACHE_TTL_MS;
+ return cachedLogsMaxCount;
+}
+
+function normalizeNonNegativeInteger(value, fallback, max) {
+ if (value === null || value === undefined || value === '') return fallback;
+
+ const number = Number(value);
+ if (!isFinite(number) || number < 0) return fallback;
+ return Math.min(Math.floor(number), max);
+}
+
+function normalizeBooleanQuery(value) {
+ return (
+ value === true ||
+ value === 1 ||
+ value === '1' ||
+ value === 'true' ||
+ value === 'on'
+ );
+}
+
+function normalizeLogLevel(level) {
+ return LOG_LEVELS.includes(level) ? level : 'log';
+}
+
+function normalizeStoredLogMessage(message, level) {
+ const rawMessage = `${message || ''}`;
+ if (LOG_WITH_SCOPE_AND_LEVEL_PREFIX_RE.test(rawMessage)) {
+ return rawMessage;
+ }
+
+ const normalizedLevel = normalizeLogLevel(level).toUpperCase();
+ const scopedMatch = rawMessage.match(LOG_WITH_SCOPE_PREFIX_RE);
+ if (scopedMatch) {
+ const scope = scopedMatch[1].trim() || DEFAULT_LOG_SCOPE;
+ const content = rawMessage.slice(scopedMatch[0].length).trimStart();
+ return `[${scope}] ${normalizedLevel}: ${content}`;
+ }
+
+ return `[${DEFAULT_LOG_SCOPE}] ${normalizedLevel}: ${rawMessage}`;
+}
+
+function createKeywordMatcher(keyword, { isRegex, ignoreCase }) {
+ if (!keyword) return null;
+ if (isRegex) {
+ const regexp = new RegExp(keyword, ignoreCase ? 'i' : '');
+ return (text) => regexp.test(text);
+ }
+
+ const needle = ignoreCase ? keyword.toLowerCase() : keyword;
+ return (text) => {
+ const haystack = ignoreCase ? text.toLowerCase() : text;
+ return haystack.includes(needle);
+ };
+}
+
+function getSearchText(entry) {
+ return `${entry.level || ''}\n${entry.message || ''}`;
+}
+
+function shouldIgnoreLogEntry(entry) {
+ return shouldIgnoreLogMessage(entry.message);
+}
+
+function shouldIgnoreLogMessage(message) {
+ const lines = getNormalizedLogMessageLines(message);
+
+ if (lines.length === 1) {
+ return (
+ SUB_STORE_BANNER_VERSION_RE.test(lines[0]) ||
+ shouldIgnoreSingleLineLogMessage(lines[0])
+ );
+ }
+
+ return isSubStoreBannerLog(lines);
+}
+
+function getNormalizedLogMessageLines(message) {
+ const lines = `${message || ''}`
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean);
+
+ if (lines.length === 0) return lines;
+
+ const firstLineWithoutPrefix = lines[0]
+ .replace(LOG_WITH_SCOPE_AND_LEVEL_PREFIX_RE, '')
+ .trim();
+ if (firstLineWithoutPrefix !== lines[0]) {
+ if (firstLineWithoutPrefix) {
+ lines[0] = firstLineWithoutPrefix;
+ } else {
+ lines.shift();
+ }
+ }
+
+ return lines;
+}
+
+function isSubStoreBannerLog(lines) {
+ if (lines.length < 3) return false;
+ if (!SUB_STORE_BANNER_SEPARATOR_RE.test(lines[0])) return false;
+ if (!SUB_STORE_BANNER_SEPARATOR_RE.test(lines[lines.length - 1])) {
+ return false;
+ }
+
+ return lines
+ .slice(1, -1)
+ .some((line) => SUB_STORE_BANNER_VERSION_RE.test(line));
+}
+
+function shouldIgnoreSingleLineLogMessage(line) {
+ const normalizedLine = line.replace(SUB_STORE_LOG_PREFIX_RE, '');
+ return DEFAULT_IGNORED_SINGLE_LINE_LOG_MESSAGE_RES.some((regexp) =>
+ regexp.test(normalizedLine),
+ );
+}
+
+function formatLogValue(value) {
+ if (value instanceof Error) {
+ return value.stack || value.message || String(value);
+ }
+ if (typeof value === 'string') return value;
+ if (value == null) return String(value);
+ if (typeof value === 'object') {
+ try {
+ return JSON.stringify(value);
+ } catch {
+ return String(value);
+ }
+ }
+ return String(value);
+}
+
+function isLogEntry(entry) {
+ return (
+ entry &&
+ typeof entry === 'object' &&
+ typeof entry.id === 'string' &&
+ typeof entry.time === 'number' &&
+ typeof entry.level === 'string' &&
+ typeof entry.message === 'string'
+ );
+}
+
+function createLogId() {
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
+}
diff --git a/backend/src/utils/dns.js b/backend/src/utils/dns.js
new file mode 100644
index 0000000000..11a55cc567
--- /dev/null
+++ b/backend/src/utils/dns.js
@@ -0,0 +1,54 @@
+import $ from '@/core/app';
+import dnsPacket from 'dns-packet';
+import { Buffer } from 'buffer';
+import { isIPv4 } from '@/utils';
+
+export async function doh({ url, domain, type = 'A', timeout, edns }) {
+ const buf = dnsPacket.encode({
+ type: 'query',
+ id: 0,
+ flags: dnsPacket.RECURSION_DESIRED,
+ questions: [
+ {
+ type,
+ name: domain,
+ },
+ ],
+ additionals: [
+ {
+ type: 'OPT',
+ name: '.',
+ udpPayloadSize: 4096,
+ flags: 0,
+ options: [
+ {
+ code: 'CLIENT_SUBNET',
+ ip: edns,
+ sourcePrefixLength: isIPv4(edns) ? 24 : 56,
+ scopePrefixLength: 0,
+ },
+ ],
+ },
+ ],
+ });
+
+ const b64 = Buffer.from(buf).toString('base64');
+ const b64url = b64
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, '');
+
+ const res = await $.http.get({
+ url: `${url}?dns=${encodeURIComponent(b64url)}`,
+ headers: {
+ Accept: 'application/dns-message',
+ // 'Content-Type': 'application/dns-message',
+ },
+ // body: buf,
+ 'binary-mode': true,
+ encoding: null, // 使用 null 编码以确保响应是原始二进制数据
+ timeout,
+ });
+
+ return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));
+}
diff --git a/backend/src/utils/download.js b/backend/src/utils/download.js
new file mode 100644
index 0000000000..75b9fdd54b
--- /dev/null
+++ b/backend/src/utils/download.js
@@ -0,0 +1,430 @@
+import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
+import { HTTP, ENV } from '@/vendor/open-api';
+import { hex_md5 } from '@/vendor/md5';
+import { getPolicyDescriptor } from '@/utils';
+import resourceCache from '@/utils/resource-cache';
+import headersResourceCache from '@/utils/headers-resource-cache';
+import {
+ getFlowField,
+ getFlowHeaders,
+ parseFlowHeaders,
+ validCheck,
+} from '@/utils/flow';
+import $ from '@/core/app';
+import { findByName } from '@/utils/database';
+import { produceArtifact } from '@/restful/sync';
+import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
+import { ProxyUtils } from '@/core/proxy-utils';
+
+const clashPreprocessor = PROXY_PREPROCESSORS.find(
+ (processor) => processor.name === 'Clash Pre-processor',
+);
+
+const tasks = new Map();
+
+function buildDownloadRegex(pattern = '') {
+ const trimmed = `${pattern}`.trim();
+ if (!trimmed) return null;
+ return new RegExp(trimmed, 'i');
+}
+
+function maybePrefixGithubProxyUrl(url, githubProxy, githubProxyRegex) {
+ if (!githubProxy || !githubProxyRegex || typeof url !== 'string') {
+ return url;
+ }
+
+ if (!/^https?:\/\//i.test(url)) {
+ return url;
+ }
+
+ const prefix = `${githubProxy}/`;
+ if (url.startsWith(prefix)) {
+ return url;
+ }
+
+ let regex;
+ try {
+ regex = buildDownloadRegex(githubProxyRegex);
+ } catch (e) {
+ $.error(`GitHub 加速代理匹配正则无效: ${e.message ?? e}`);
+ return url;
+ }
+
+ if (!regex?.test(url)) {
+ return url;
+ }
+
+ $.info(`GitHub 加速代理命中下载链接: ${url}`);
+ return `${githubProxy}/${url}`;
+}
+
+export default async function download(
+ rawUrl = '',
+ ua,
+ timeout,
+ customProxy,
+ skipCustomCache,
+ awaitCustomCache,
+ noCache,
+ preprocess,
+) {
+ let $arguments = {};
+ let url = rawUrl.replace(/#noFlow$/, '');
+ const rawArgs = url.split('#');
+ url = url.split('#')[0];
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
+ const {
+ githubProxy,
+ githubProxyRegex,
+ defaultProxy,
+ defaultUserAgent,
+ defaultTimeout,
+ cacheThreshold: defaultCacheThreshold,
+ } = $.read(SETTINGS_KEY);
+ const cacheThreshold = defaultCacheThreshold || 1024;
+ let proxy = customProxy || defaultProxy;
+ if ($.env.isNode) {
+ proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
+ }
+ const userAgent = ua || defaultUserAgent || 'clash.meta/v1.19.23';
+ let customHeaders;
+ if ($arguments?.headers) {
+ try {
+ const parsed = JSON.parse($arguments?.headers);
+ if (
+ parsed &&
+ typeof parsed === 'object' &&
+ !Array.isArray(parsed) &&
+ Object.keys(parsed).length > 0
+ ) {
+ const lowerCaseHeaders = { 'user-agent': userAgent };
+ for (const key in parsed) {
+ lowerCaseHeaders[key.toLowerCase()] = parsed[key];
+ }
+ customHeaders = lowerCaseHeaders;
+ }
+ } catch (e) {
+ $.error(`解析自定义 headers 失败: ${e}`);
+ }
+ }
+
+ const requestTimeout = timeout || defaultTimeout || 8000;
+ url = maybePrefixGithubProxyUrl(url, githubProxy, githubProxyRegex);
+ const id = hex_md5(
+ `${customHeaders ? JSON.stringify(customHeaders) : userAgent}${url}`,
+ );
+
+ if ($arguments?.cacheKey === true) {
+ $.error(`使用自定义缓存时 cacheKey 的值不能为空`);
+ $arguments.cacheKey = undefined;
+ }
+
+ const customCacheKey = $arguments?.cacheKey
+ ? `#sub-store-cached-custom-${$arguments?.cacheKey}`
+ : undefined;
+
+ if (customCacheKey && !skipCustomCache) {
+ const customCached = $.read(customCacheKey);
+ const cached = resourceCache.get(id);
+ if (!noCache && !$arguments?.noCache && cached) {
+ $.info(
+ `乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
+ );
+ return cached;
+ }
+ if (customCached) {
+ if (awaitCustomCache) {
+ $.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
+ try {
+ await download(
+ rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
+ ua,
+ timeout,
+ proxy,
+ true,
+ undefined,
+ undefined,
+ preprocess,
+ );
+ } catch (e) {
+ $.error(
+ `乐观缓存: URL ${url} 更新缓存发生错误 ${
+ e.message ?? e
+ }`,
+ );
+ $.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
+ resourceCache.set(id, customCached);
+ }
+ } else {
+ $.info(
+ `乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
+ );
+ download(
+ rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
+ ua,
+ timeout,
+ proxy,
+ true,
+ undefined,
+ undefined,
+ preprocess,
+ ).catch((e) => {
+ $.error(
+ `乐观缓存: URL ${url} 异步更新缓存发生错误 ${
+ e.message ?? e
+ }`,
+ );
+ });
+ }
+ return customCached;
+ }
+ }
+
+ const downloadUrlMatch = url
+ .split('#')[0]
+ .match(/^\/api\/(file|module)\/(.+)/);
+ if (downloadUrlMatch) {
+ let type = '';
+ try {
+ type = downloadUrlMatch?.[1];
+ let name = downloadUrlMatch?.[2];
+ if (name == null) {
+ throw new Error(`本地 ${type} URL 无效: ${url}`);
+ }
+ name = decodeURIComponent(name);
+ const key = type === 'module' ? MODULES_KEY : FILES_KEY;
+ const item = findByName($.read(key), name);
+ if (!item) {
+ throw new Error(`找不到 ${type}: ${name}`);
+ }
+
+ if (type === 'module') {
+ return item.content;
+ } else {
+ return await produceArtifact({
+ type: 'file',
+ name,
+ });
+ }
+ } catch (err) {
+ $.error(
+ `Error when loading ${type}: ${
+ url.split('#')[0]
+ }.\n Reason: ${err}`,
+ );
+ throw new Error(`无法加载 ${type}: ${url}`);
+ }
+ } else if (url?.startsWith('/')) {
+ try {
+ const fs = eval(`require("fs")`);
+ return fs.readFileSync(url.split('#')[0], 'utf8');
+ } catch (err) {
+ $.error(
+ `Error when reading local file: ${
+ url.split('#')[0]
+ }.\n Reason: ${err}`,
+ );
+ throw new Error(`无法从该路径读取文本内容: ${url}`);
+ }
+ }
+
+ if (!isNode && tasks.has(id)) {
+ return tasks.get(id);
+ }
+
+ const http = HTTP({
+ headers: {
+ ...(customHeaders || { 'User-Agent': userAgent }),
+ ...(isStash && proxy
+ ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
+ : {}),
+ ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
+ },
+ timeout: requestTimeout,
+ });
+
+ let result;
+
+ // try to find in app cache
+ const cached = resourceCache.get(id);
+ if (!noCache && !$arguments?.noCache && cached) {
+ $.info(
+ `使用缓存: ${url}, ${
+ customHeaders ? JSON.stringify(customHeaders) : userAgent
+ }`,
+ );
+ result = cached;
+ if (customCacheKey) {
+ $.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
+ $.write(cached, customCacheKey);
+ }
+ } else {
+ const insecure = $arguments?.insecure
+ ? isNode
+ ? { strictSSL: false }
+ : { insecure: true }
+ : undefined;
+ $.info(
+ `Downloading...\n${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent}`
+ }\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
+ );
+ try {
+ let { body, headers, statusCode } = await http.get({
+ url,
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ ...(insecure ? insecure : {}),
+ });
+ $.info(`statusCode: ${statusCode}`);
+ if (statusCode < 200 || statusCode >= 400) {
+ throw new Error(`statusCode: ${statusCode}`);
+ }
+
+ if (headers) {
+ const flowInfo = getFlowField(headers);
+ if (flowInfo) {
+ headersResourceCache.set(
+ id,
+ flowInfo,
+ $arguments?.headersCacheTtl
+ ? $arguments?.headersCacheTtl * 1000
+ : undefined,
+ );
+ }
+ }
+ if (body.replace(/\s/g, '').length === 0)
+ throw new Error(new Error('远程资源内容为空'));
+ if (preprocess) {
+ try {
+ if (clashPreprocessor.test(body)) {
+ body = clashPreprocessor.parse(body, true);
+ }
+ } catch (e) {
+ $.error(`Clash Pre-processor error: ${e}`);
+ }
+ }
+ let shouldCache = true;
+ if (cacheThreshold) {
+ const size = body.length / 1024;
+ if (size > cacheThreshold) {
+ $.info(
+ `资源大小 ${size.toFixed(
+ 2,
+ )} KB 超过了 ${cacheThreshold} KB, 不缓存`,
+ );
+ shouldCache = false;
+ }
+ }
+ if (preprocess) {
+ try {
+ const proxies = ProxyUtils.parse(body);
+ if (!Array.isArray(proxies) || proxies.length === 0) {
+ $.error(`URL ${url} 不包含有效节点, 不缓存`);
+ shouldCache = false;
+ }
+ } catch (e) {
+ $.error(
+ `URL ${url} 尝试解析节点失败 ${e.message ?? e}, 不缓存`,
+ );
+ shouldCache = false;
+ }
+ }
+ if (shouldCache) {
+ // console.log($arguments);
+ resourceCache.set(
+ id,
+ body,
+ $arguments?.cacheTtl
+ ? $arguments?.cacheTtl * 1000
+ : undefined,
+ );
+ if (customCacheKey) {
+ $.info(
+ `URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
+ );
+ $.write(body, customCacheKey);
+ }
+ }
+
+ result = body;
+ } catch (e) {
+ if (customCacheKey) {
+ const cached = $.read(customCacheKey);
+ if (cached) {
+ $.info(
+ `无法下载 URL ${url}: ${
+ e.message ?? e
+ }\n使用自定义缓存 ${$arguments?.cacheKey}`,
+ );
+ return cached;
+ }
+ }
+ throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
+ }
+ }
+
+ // 检查订阅有效性
+
+ if ($arguments?.validCheck) {
+ await validCheck(
+ parseFlowHeaders(
+ await getFlowHeaders(
+ url,
+ $arguments.flowUserAgent,
+ undefined,
+ proxy,
+ $arguments.flowUrl,
+ $arguments.flowHeaders,
+ ),
+ ),
+ );
+ }
+
+ if (!isNode) {
+ tasks.set(id, result);
+ }
+ return result;
+}
+
+export async function downloadFile(url, file) {
+ const undici = eval("require('undici')");
+ const fs = eval("require('fs')");
+ const { pipeline } = eval("require('stream/promises')");
+ const { Agent, interceptors, request } = undici;
+ $.info(`Downloading file...\nURL: ${url}\nFile: ${file}`);
+ const { body, statusCode } = await request(url, {
+ dispatcher: new Agent().compose(
+ interceptors.redirect({
+ maxRedirections: 3,
+ throwOnRedirect: true,
+ }),
+ ),
+ });
+ if (statusCode !== 200)
+ throw new Error(`Failed to download file from ${url}`);
+ const fileStream = fs.createWriteStream(file);
+ await pipeline(body, fileStream);
+ $.info(`File downloaded from ${url} to ${file}`);
+ return file;
+}
diff --git a/backend/src/utils/env.js b/backend/src/utils/env.js
new file mode 100644
index 0000000000..98edd5625f
--- /dev/null
+++ b/backend/src/utils/env.js
@@ -0,0 +1,79 @@
+import { version as substoreVersion } from '../../package.json';
+import { ENV } from '@/vendor/open-api';
+
+const {
+ isNode,
+ isQX,
+ isLoon,
+ isSurge,
+ isStash,
+ isShadowRocket,
+ isLanceX,
+ isEgern,
+ isGUIforCores,
+} = ENV();
+let backend = 'Node';
+if (isNode) {
+ backend = 'Node';
+} else if (isQX) {
+ backend = 'QX';
+} else if (isLoon) {
+ backend = 'Loon';
+} else if (isStash) {
+ backend = 'Stash';
+} else if (isShadowRocket) {
+ backend = 'Shadowrocket';
+} else if (isEgern) {
+ backend = 'Egern';
+} else if (isSurge) {
+ backend = 'Surge';
+} else if (isLanceX) {
+ backend = 'LanceX';
+} else if (isGUIforCores) {
+ backend = 'GUI.for.Cores';
+}
+
+let meta = {};
+let feature = {};
+
+try {
+ if (typeof $environment !== 'undefined') {
+ // eslint-disable-next-line no-undef
+ meta.env = $environment;
+ }
+ if (typeof $loon !== 'undefined') {
+ // eslint-disable-next-line no-undef
+ meta.loon = $loon;
+ }
+ if (typeof $script !== 'undefined') {
+ // eslint-disable-next-line no-undef
+ meta.script = $script;
+ }
+ if (typeof $Plugin !== 'undefined') {
+ // eslint-disable-next-line no-undef
+ meta.plugin = $Plugin;
+ }
+ if (isNode) {
+ meta.node = {
+ version: eval('process.version'),
+ argv: eval('process.argv'),
+ filename: eval('__filename'),
+ dirname: eval('__dirname'),
+ env: {},
+ };
+ const env = eval('process.env');
+ for (const key in env) {
+ if (/^SUB_STORE_/.test(key)) {
+ meta.node.env[key] = env[key];
+ }
+ }
+ }
+ // eslint-disable-next-line no-empty
+} catch (e) {}
+
+export default {
+ backend,
+ version: substoreVersion,
+ feature,
+ meta,
+};
diff --git a/backend/src/utils/flow.js b/backend/src/utils/flow.js
new file mode 100644
index 0000000000..2e3e36e007
--- /dev/null
+++ b/backend/src/utils/flow.js
@@ -0,0 +1,524 @@
+import { SETTINGS_KEY } from '@/constants';
+import { HTTP, ENV } from '@/vendor/open-api';
+import { hex_md5 } from '@/vendor/md5';
+import { getPolicyDescriptor } from '@/utils';
+import $ from '@/core/app';
+import headersResourceCache from '@/utils/headers-resource-cache';
+
+export function getFlowField(headers) {
+ const keys = Object.keys(headers);
+ let sub = '';
+ let webPage = '';
+ let planName = '';
+ for (let k of keys) {
+ const lower = k.toLowerCase();
+ if (lower === 'subscription-userinfo') {
+ sub = headers[k];
+ } else if (lower === 'profile-web-page-url') {
+ webPage = headers[k];
+ } else if (lower === 'plan-name') {
+ planName = headers[k];
+ }
+ }
+
+ return `${sub || ''}${
+ webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''
+ }${planName ? `; plan_name=${encodeURIComponent(planName)}` : ''}`;
+}
+export async function getFlowHeaders(
+ rawUrl,
+ ua,
+ timeout,
+ customProxy,
+ flowUrl,
+ flowHeaders,
+) {
+ let url = flowUrl || rawUrl || '';
+ let $arguments = {};
+ const rawArgs = url.split('#');
+ url = url.split('#')[0];
+ if (rawArgs.length > 1) {
+ try {
+ // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
+ $arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
+ } catch (e) {
+ for (const pair of rawArgs[1].split('&')) {
+ const key = pair.split('=')[0];
+ const value = pair.split('=')[1];
+ // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
+ $arguments[key] =
+ value == null || value === ''
+ ? true
+ : decodeURIComponent(value);
+ }
+ }
+ }
+ if ($arguments?.noFlow || !/^https?/.test(url)) {
+ return;
+ }
+ const { isStash, isLoon, isShadowRocket, isQX } = ENV();
+ const insecure = $arguments?.insecure
+ ? $.env.isNode
+ ? { strictSSL: false }
+ : { insecure: true }
+ : undefined;
+ const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
+ $.read(SETTINGS_KEY);
+ let proxy = customProxy || defaultProxy;
+ if ($.env.isNode) {
+ proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
+ }
+ const userAgent = ua || defaultFlowUserAgent || 'clash.meta/v1.19.23';
+ const requestTimeout = timeout || defaultTimeout || 8000;
+ let customHeaders;
+ const customHeadersArg =
+ flowHeaders || $arguments?.flowHeaders || $arguments?.headers;
+ if (customHeadersArg) {
+ try {
+ const parsed =
+ typeof customHeadersArg === 'string'
+ ? JSON.parse(customHeadersArg)
+ : customHeadersArg;
+ if (
+ parsed &&
+ typeof parsed === 'object' &&
+ !Array.isArray(parsed) &&
+ Object.keys(parsed).length > 0
+ ) {
+ const lowerCaseHeaders = { 'user-agent': userAgent };
+ for (const key in parsed) {
+ lowerCaseHeaders[key.toLowerCase()] = parsed[key];
+ }
+ customHeaders = lowerCaseHeaders;
+ }
+ } catch (e) {
+ $.error(
+ `解析自定义 ${
+ flowHeaders || $arguments?.flowHeaders
+ ? 'flowHeaders'
+ : 'headers'
+ } 失败: ${e}`,
+ );
+ }
+ }
+ const id = hex_md5(
+ `${customHeaders ? JSON.stringify(customHeaders) : userAgent}${url}`,
+ );
+ const cached = headersResourceCache.get(id);
+ let flowInfo;
+ if (!$arguments?.noCache && cached) {
+ $.info(
+ `使用缓存的流量信息: ${url}, ${
+ customHeaders ? JSON.stringify(customHeaders) : userAgent
+ }`,
+ );
+ flowInfo = cached;
+ } else {
+ const http = HTTP();
+ if (flowUrl) {
+ let flowUrlHeaders;
+ try {
+ $.info(
+ `使用 GET 方法从响应体获取流量信息: ${flowUrl}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}`,
+ );
+ const { headers, body, statusCode } = await http.get({
+ url,
+ headers: customHeaders || {
+ 'User-Agent': userAgent,
+ },
+ timeout: requestTimeout,
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ ...(insecure ? insecure : {}),
+ });
+ if (statusCode < 200 || statusCode >= 400) {
+ throw new Error(`statusCode: ${statusCode}`);
+ }
+ flowUrlHeaders = headers;
+ const parsed = parseFlowHeaders(body);
+ if (
+ Number.isFinite(parsed?.total) &&
+ Number.isFinite(parsed?.usage?.download) &&
+ Number.isFinite(parsed?.usage?.upload)
+ ) {
+ flowInfo = body;
+ } else {
+ throw new Error('响应体中未包含合法的流量信息');
+ }
+ } catch (e) {
+ $.error(
+ `使用 GET 方法从响应体获取流量信息失败: ${flowUrl}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
+ e.message ?? e
+ }`,
+ );
+ if (flowUrlHeaders) {
+ try {
+ const flowField = getFlowField(flowUrlHeaders);
+ const parsed = parseFlowHeaders(flowField);
+ if (
+ Number.isFinite(parsed?.total) &&
+ Number.isFinite(parsed?.usage?.download) &&
+ Number.isFinite(parsed?.usage?.upload)
+ ) {
+ $.info(
+ `使用 GET 方法从响应头获取流量信息成功: ${flowUrl}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}`,
+ );
+ flowInfo = flowField;
+ } else {
+ throw new Error('响应体中未包含合法的流量信息');
+ }
+ } catch (e) {
+ $.error(
+ `使用 GET 方法从响应头获取流量信息失败: ${flowUrl}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }
+ }
+ } else {
+ try {
+ $.info(
+ `使用 HEAD 方法从响应头获取流量信息: ${url}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}`,
+ );
+ const { headers } = await http.head({
+ url: url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)[0],
+ headers: {
+ ...(customHeaders || { 'User-Agent': userAgent }),
+ ...(isStash && proxy
+ ? {
+ 'X-Stash-Selected-Proxy':
+ encodeURIComponent(proxy),
+ }
+ : {}),
+ ...(isShadowRocket && proxy
+ ? { 'X-Surge-Policy': proxy }
+ : {}),
+ },
+ timeout: requestTimeout,
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ ...(insecure ? insecure : {}),
+ });
+ flowInfo = getFlowField(headers);
+ } catch (e) {
+ $.error(
+ `使用 HEAD 方法从响应头获取流量信息失败: ${url}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
+ e.message ?? e
+ }`,
+ );
+ }
+ if (!flowInfo) {
+ $.info(
+ `使用 GET 方法获取流量信息: ${url}, ${
+ customHeaders
+ ? JSON.stringify(customHeaders)
+ : `User-Agent: ${userAgent || ''}`
+ }, Insecure: ${!!insecure}, Proxy: ${proxy}`,
+ );
+ const { headers } = await http.get({
+ url: url
+ .split(/[\r\n]+/)
+ .map((i) => i.trim())
+ .filter((i) => i.length)[0],
+ headers: {
+ ...(customHeaders || { 'User-Agent': userAgent }),
+ ...(isStash && proxy
+ ? {
+ 'X-Stash-Selected-Proxy':
+ encodeURIComponent(proxy),
+ }
+ : {}),
+ ...(isShadowRocket && proxy
+ ? { 'X-Surge-Policy': proxy }
+ : {}),
+ },
+ timeout: requestTimeout,
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ ...(insecure ? insecure : {}),
+ });
+ flowInfo = getFlowField(headers);
+ }
+ }
+ if (flowInfo) {
+ flowInfo = flowInfo.trim();
+ }
+ if (flowInfo) {
+ headersResourceCache.set(
+ id,
+ flowInfo,
+ $arguments?.headersCacheTtl
+ ? $arguments?.headersCacheTtl * 1000
+ : undefined,
+ );
+ }
+ }
+
+ return flowInfo;
+}
+export function parseFlowHeaders(flowHeaders) {
+ if (!flowHeaders) return;
+ // unit is KB
+ const uploadMatch = flowHeaders.match(
+ /upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
+ );
+ const upload =
+ uploadMatch == null ? 0 : Number(uploadMatch[1] + uploadMatch[2]);
+
+ const downloadMatch = flowHeaders.match(
+ /download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
+ );
+ const download = Number(downloadMatch[1] + downloadMatch[2]);
+ const totalMatch = flowHeaders.match(
+ /total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
+ );
+ const total = Number(totalMatch[1] + totalMatch[2]);
+
+ // optional expire timestamp
+ const expireMatch = flowHeaders.match(
+ /expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
+ );
+ const expires = expireMatch
+ ? Number(expireMatch[1] + expireMatch[2])
+ : undefined;
+
+ const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
+ const remainingDays = remainingDaysMatch
+ ? Number(remainingDaysMatch[1])
+ : undefined;
+
+ const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
+ const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
+
+ const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
+ const planName = planNameMatch
+ ? decodeURIComponent(planNameMatch[1])
+ : undefined;
+
+ return {
+ expires,
+ total,
+ usage: { upload, download },
+ remainingDays,
+ appUrl,
+ planName,
+ };
+}
+
+export function flowTransfer(flow, unit = 'B') {
+ const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ let unitIndex = unitList.indexOf(unit);
+
+ return flow < 1024 || unitIndex === unitList.length - 1
+ ? { value: (Math.round(flow * 100) / 100).toString(), unit: unit }
+ : flowTransfer(flow / 1024, unitList[++unitIndex]);
+}
+
+export function validCheck(flow) {
+ if (!flow) {
+ throw new Error('没有流量信息');
+ }
+ if (flow?.expires && flow.expires * 1000 < Date.now()) {
+ const date = new Date(flow.expires * 1000).toLocaleDateString();
+ throw new Error(`订阅已过期: ${date}`);
+ }
+ if (flow?.total) {
+ const upload = flow.usage?.upload || 0;
+ const download = flow.usage?.download || 0;
+ if (flow.total - upload - download < 0) {
+ const current = upload + download;
+ const currT = flowTransfer(Math.abs(current));
+ currT.value = current < 0 ? '-' + currT.value : currT.value;
+ const totalT = flowTransfer(flow.total);
+ throw new Error(
+ `流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
+ );
+ }
+ }
+}
+
+export function getRmainingDays(opt = {}) {
+ try {
+ let { resetDay, startDate, cycleDays } = opt;
+ if (['string', 'number'].includes(typeof opt)) {
+ resetDay = opt;
+ }
+
+ if (startDate && cycleDays) {
+ cycleDays = parseInt(cycleDays);
+ if (isNaN(cycleDays) || cycleDays <= 0)
+ throw new Error('重置周期应为正整数');
+ if (!startDate || !Date.parse(startDate))
+ throw new Error('开始日期不合法');
+
+ const start = new Date(startDate);
+ const today = new Date();
+ start.setHours(0, 0, 0, 0);
+ today.setHours(0, 0, 0, 0);
+ if (start.getTime() > today.getTime())
+ throw new Error('开始日期应早于现在');
+
+ let resetDate = new Date(startDate);
+ resetDate.setDate(resetDate.getDate() + cycleDays);
+
+ while (resetDate < today) {
+ resetDate.setDate(resetDate.getDate() + cycleDays);
+ }
+
+ resetDate.setHours(0, 0, 0, 0);
+ const timeDiff = resetDate.getTime() - today.getTime();
+ const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
+
+ return daysDiff;
+ } else {
+ if (!resetDay) return;
+ resetDay = parseInt(resetDay);
+ if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
+ throw new Error('月重置日应为 1-31 之间的整数');
+ let now = new Date();
+ let today = now.getDate();
+ let month = now.getMonth();
+ let year = now.getFullYear();
+ let daysInMonth;
+
+ if (resetDay > today) {
+ daysInMonth = 0;
+ } else {
+ daysInMonth = new Date(year, month + 1, 0).getDate();
+ }
+
+ return daysInMonth - today + resetDay;
+ }
+ } catch (e) {
+ $.error(`getRmainingDays failed: ${e.message ?? e}`);
+ }
+}
+
+export function normalizeFlowHeader(flowHeaders, splitHeaders) {
+ try {
+ // 使用 Map 保持顺序并处理重复键
+ const kvMap = new Map();
+
+ flowHeaders
+ .split(';')
+ .map((p) => p.trim())
+ .filter(Boolean)
+ .forEach((pair) => {
+ const eqIndex = pair.indexOf('=');
+ if (eqIndex === -1) return;
+
+ const key = pair.slice(0, eqIndex).trim();
+ const encodedValue = pair.slice(eqIndex + 1).trim();
+
+ // 只保留第一个出现的 key
+ if (!kvMap.has(key)) {
+ try {
+ // 解码 URI 组件并保留原始值作为 fallback
+ let decodedValue = decodeURIComponent(encodedValue);
+ if (
+ [
+ 'upload',
+ 'download',
+ 'total',
+ 'expire',
+ 'reset_day',
+ ].includes(key)
+ ) {
+ try {
+ decodedValue = Number(decodedValue);
+ if (
+ ['expire', 'reset_day'].includes(key) &&
+ (decodedValue <= 0 ||
+ !Number.isFinite(decodedValue))
+ ) {
+ decodedValue = '';
+ } else if (
+ ['upload', 'download', 'total'].includes(
+ key,
+ ) &&
+ !Number.isFinite(decodedValue) // 有些机场后端会下发负数
+ ) {
+ decodedValue = 0;
+ } else {
+ decodedValue = decodedValue.toFixed(0);
+ }
+ } catch (e) {
+ $.error(
+ `Failed to convert value for key "${key}=${encodedValue}": ${
+ e.message ?? e
+ }`,
+ );
+ }
+ }
+ kvMap.set(key, decodedValue);
+ } catch (e) {
+ kvMap.set(key, encodedValue);
+ }
+ }
+ });
+ const subscriptionUserinfo = {};
+ const headers = {
+ 'subscription-userinfo': '',
+ 'profile-web-page-url': '',
+ 'plan-name': '',
+ };
+ kvMap.forEach((v, k) => {
+ if (splitHeaders && k === 'app_url') {
+ headers['profile-web-page-url'] = v;
+ } else if (splitHeaders && k === 'plan_name') {
+ headers['plan-name'] = v;
+ } else {
+ subscriptionUserinfo[k] = v;
+ }
+ });
+ if (Object.keys(subscriptionUserinfo).length > 0) {
+ headers['subscription-userinfo'] = Object.entries(
+ subscriptionUserinfo,
+ )
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
+ .join('; ');
+ }
+ return splitHeaders ? headers : headers['subscription-userinfo'];
+ } catch (e) {
+ $.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
+ return splitHeaders
+ ? {
+ 'subscription-userinfo': flowHeaders,
+ }
+ : flowHeaders;
+ }
+}
diff --git a/backend/src/utils/geo.js b/backend/src/utils/geo.js
new file mode 100644
index 0000000000..e1cb61c8be
--- /dev/null
+++ b/backend/src/utils/geo.js
@@ -0,0 +1,557 @@
+import $ from '@/core/app';
+
+const ISOFlags = {
+ '🏳️🌈': ['EXP', 'BAND'],
+ '🇸🇱': ['TEST', 'SOS'],
+ '🇲🇵': ['MP', 'MNP'],
+ '🇸🇴': ['SO', 'SOM'],
+ '🇦🇶': ['AQ', 'ATA'],
+ '🇦🇬': ['AG', 'ATG'],
+ '🇬🇱': ['GL', 'GRL'],
+ '🇿🇼': ['ZW', 'ZWE'],
+ '🇦🇼': ['AW', 'ABW'],
+ '🇲🇱': ['ML', 'MLI'],
+ '🇦🇩': ['AD', 'AND'],
+ '🇦🇪': ['AE', 'ARE'],
+ '🇦🇫': ['AF', 'AFG'],
+ '🇦🇱': ['AL', 'ALB'],
+ '🇦🇲': ['AM', 'ARM'],
+ '🇦🇷': ['AR', 'ARG'],
+ '🇦🇹': ['AT', 'AUT'],
+ '🇦🇺': ['AU', 'AUS'],
+ '🇦🇿': ['AZ', 'AZE'],
+ '🇧🇦': ['BA', 'BIH'],
+ '🇧🇩': ['BD', 'BGD'],
+ '🇧🇪': ['BE', 'BEL'],
+ '🇧🇬': ['BG', 'BGR'],
+ '🇧🇭': ['BH', 'BHR'],
+ '🇧🇴': ['BO', 'BOL'],
+ '🇧🇳': ['BN', 'BRN'],
+ '🇧🇷': ['BR', 'BRA'],
+ '🇧🇹': ['BT', 'BTN'],
+ '🇧🇾': ['BY', 'BLR'],
+ '🇨🇦': ['CA', 'CAN'],
+ '🇨🇫': ['CF', 'CAF'],
+ '🇨🇭': ['CH', 'CHE'],
+ '🇨🇱': ['CL', 'CHL'],
+ '🇨🇴': ['CO', 'COL'],
+ '🇨🇷': ['CR', 'CRI'],
+ '🇨🇾': ['CY', 'CYP'],
+ '🇨🇿': ['CZ', 'CZE'],
+ '🇩🇪': ['DE', 'DEU'],
+ '🇩🇰': ['DK', 'DNK'],
+ // 新增阿尔及利亚 ISO 代码
+ '🇩🇿': ['DZ', 'DZA'],
+ '🇪🇨': ['EC', 'ECU'],
+ '🇪🇪': ['EE', 'EST'],
+ '🇪🇬': ['EG', 'EGY'],
+ '🇪🇸': ['ES', 'ESP'],
+ '🇪🇺': ['EU'],
+ '🇫🇮': ['FI', 'FIN'],
+ '🇫🇷': ['FR', 'FRA'],
+ '🇬🇧': ['GB', 'GBR', 'UK'],
+ '🇬🇪': ['GE', 'GEO'],
+ '🇬🇷': ['GR', 'GRC'],
+ '🇬🇹': ['GT', 'GTM'],
+ '🇬🇺': ['GU', 'GUM'],
+ '🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
+ '🇭🇷': ['HR', 'HRV'],
+ '🇭🇺': ['HU', 'HUN'],
+ '🇮🇶': ['IQ', 'IRQ'], // 伊拉克
+ '🇯🇴': ['JO', 'JOR'],
+ '🇯🇵': ['JP', 'JPN', 'TYO'],
+ '🇰🇪': ['KE', 'KEN'],
+ '🇰🇬': ['KG', 'KGZ'],
+ '🇰🇭': ['KH', 'KGZ'],
+ '🇰🇵': ['KP', 'PRK'],
+ '🇰🇷': ['KR', 'KOR', 'SEL'],
+ '🇰🇿': ['KZ', 'KAZ'],
+ '🇮🇩': ['ID', 'IDN'],
+ '🇮🇪': ['IE', 'IRL'],
+ '🇮🇱': ['IL', 'ISR'],
+ '🇮🇲': ['IM', 'IMN'],
+ '🇮🇳': ['IN', 'IND'],
+ '🇮🇷': ['IR', 'IRN'],
+ '🇮🇸': ['IS', 'ISL'],
+ '🇮🇹': ['IT', 'ITA'],
+ '🇱🇦': ['LA', 'LAO'],
+ '🇱🇰': ['LK', 'LKA'],
+ '🇱🇸': ['LS', 'LSO'],
+ '🇱🇹': ['LT', 'LTU'],
+ '🇱🇺': ['LU', 'LUX'],
+ '🇱🇻': ['LV', 'LVA'],
+ '🇲🇦': ['MA', 'MAR'],
+ '🇲🇩': ['MD', 'MDA'],
+ '🇳🇬': ['NG', 'NGA'],
+ '🇲🇲': ['MM', 'MMR'],
+ '🇲🇰': ['MK', 'MKD'],
+ '🇲🇳': ['MN', 'MNG'],
+ '🇲🇴': ['MO', 'MAC', 'CTM'],
+ '🇲🇹': ['MT', 'MLT'],
+ '🇲🇽': ['MX', 'MEX'],
+ '🇲🇾': ['MY', 'MYS'],
+ '🇳🇱': ['NL', 'NLD', 'AMS'],
+ '🇳🇴': ['NO', 'NOR'],
+ '🇳🇵': ['NP', 'NPL'],
+ '🇳🇿': ['NZ', 'NZL'],
+ '🇴🇲': ['OM', 'OMN'], // 阿曼
+ '🇵🇦': ['PA', 'PAN'],
+ '🇵🇪': ['PE', 'PER'],
+ '🇵🇭': ['PH', 'PHL'],
+ '🇵🇰': ['PK', 'PAK'],
+ '🇵🇱': ['PL', 'POL'],
+ '🇵🇷': ['PR', 'PRI'],
+ '🇵🇹': ['PT', 'PRT'],
+ '🇵🇾': ['PY', 'PRY'],
+ '🇵🇬': ['PG', 'PNG'],
+ '🇶🇦': ['QA', 'QAT'],
+ '🇷🇴': ['RO', 'ROU'],
+ '🇷🇸': ['RS', 'SRB'],
+ '🇷🇪': ['RE', 'REU'],
+ '🇷🇺': ['RU', 'RUS'],
+ '🇸🇦': ['SA', 'SAU'],
+ '🇼🇸': ['WS', 'WSM'],
+ '🇸🇪': ['SE', 'SWE'],
+ '🇸🇬': ['SG', 'SGP'],
+ '🇸🇮': ['SI', 'SVN'],
+ '🇸🇰': ['SK', 'SVK'],
+ '🇹🇬': ['TG', 'TGO'], // 多哥
+ '🇹🇭': ['TH', 'THA'],
+ '🇹🇳': ['TN', 'TUN'],
+ '🇹🇷': ['TR', 'TUR'],
+ '🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
+ '🇺🇦': ['UA', 'UKR'],
+ '🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],
+ '🇺🇾': ['UY', 'URY'],
+ // 新增 梵蒂冈 ISO 代码
+ '🇻🇦': ['VA', 'VAT'],
+ '🇻🇪': ['VE', 'VEN'],
+ '🇻🇳': ['VN', 'VNM'],
+ '🇿🇦': ['ZA', 'ZAF', 'JNB'],
+ '🇨🇳': ['CN', 'CHN', 'BACK'],
+};
+// get proxy flag according to its name
+export function getFlag(name) {
+ // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
+ // flags from @surgioproject: https://github.com/surgioproject/surgio/blob/master/lib/misc/flag_cn.ts
+
+ // refer: https://zh.wikipedia.org/wiki/ISO_3166-1二位字母代码
+ // refer: https://zh.wikipedia.org/wiki/ISO_3166-1三位字母代码
+ const Flags = {
+ '🏳️🌈': ['流量', '时间', '过期', 'Bandwidth', 'Expire'],
+ '🇸🇱': ['应急', '测试节点'],
+ '🇲🇵': ['北马里亚纳', 'Northern Mariana Islands', 'Saipan', '塞班'],
+ '🇸🇴': ['Somalia', '索马里', '摩加迪沙', 'Mogadishu'],
+ '🇦🇶': ['Antarctica', '南极洲', '南极'],
+ '🇦🇬': ['Antigua and Barbuda', '安提瓜和巴布达'],
+ '🇬🇱': ['Greenland', '格陵兰岛', '格陵兰'],
+ '🇿🇼': ['Zimbabwe', '津巴布韦'],
+ '🇦🇼': ['Aruba', '阿鲁巴'],
+ '🇲🇱': ['Mali', '马里'],
+ '🇦🇩': ['Andorra', '安道尔'],
+ '🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜', 'Dubai'],
+ '🇦🇫': ['Afghanistan', '阿富汗'],
+ '🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'],
+ '🇦🇲': ['Armenia', '亚美尼亚'],
+ '🇦🇷': ['Argentina', '阿根廷'],
+ '🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'],
+ '🇼🇸': ['Samoa', '萨摩亚', '薩摩亞'],
+ '🇦🇺': [
+ 'Australia',
+ '澳大利亚',
+ '澳洲',
+ '墨尔本',
+ '悉尼',
+ '土澳',
+ '京澳',
+ '廣澳',
+ '滬澳',
+ '沪澳',
+ '广澳',
+ 'Sydney',
+ ],
+ '🇦🇿': ['Azerbaijan', '阿塞拜疆'],
+ '🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'],
+ '🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'],
+ '🇧🇪': ['Belgium', '比利时', '比利時'],
+ '🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
+ '🇧🇭': ['Bahrain', '巴林'],
+ '🇧🇷': ['Brazil', '巴西', '圣保罗'],
+ '🇧🇳': ['Brunei', '文莱', '汶萊'],
+ '🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
+ '🇧🇴': ['Bolivia', '玻利维亚'],
+ '🇧🇹': ['Bhutan', '不丹', '不丹王国'],
+ '🇨🇦': [
+ 'Canada',
+ '加拿大',
+ '蒙特利尔',
+ '温哥华',
+ '楓葉',
+ '枫叶',
+ '滑铁卢',
+ '多伦多',
+ 'Waterloo',
+ 'Toronto',
+ ],
+ '🇨🇫': ['Central African Republic', '中非共和国', '中非'],
+ '🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
+ '🇨🇱': ['Chile', '智利'],
+ '🇨🇴': ['Colombia', '哥伦比亚'],
+ '🇨🇷': ['Costa Rica', '哥斯达黎加'],
+ '🇨🇾': ['Cyprus', '塞浦路斯'],
+ // 补充 Czech / Czech Republic 匹配
+ '🇨🇿': ['Czechia', '捷克', 'Czech', 'Czech Republic'],
+ '🇩🇪': [
+ 'German',
+ '德国',
+ '德國',
+ '京德',
+ '滬德',
+ '廣德',
+ '沪德',
+ '广德',
+ '法兰克福',
+ 'Frankfurt',
+ '德意志',
+ ],
+ '🇩🇰': ['Denmark', '丹麦', '丹麥'],
+ // 新增 阿尔及利亚
+ '🇩🇿': ['Algeria', '阿尔及利亚', '阿爾及利亞'],
+ '🇪🇨': ['Ecuador', '厄瓜多尔'],
+ '🇪🇪': ['Estonia', '爱沙尼亚'],
+ '🇪🇬': ['Egypt', '埃及'],
+ '🇪🇸': ['Spain', '西班牙'],
+ '🇪🇺': ['European Union', '欧盟', '欧罗巴'],
+ '🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
+ '🇫🇷': ['France', '法国', '法國', '巴黎'],
+ '🇬🇧': [
+ 'Great Britain',
+ '英国',
+ 'England',
+ 'United Kingdom',
+ '伦敦',
+ '英',
+ 'London',
+ ],
+ '🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
+ '🇬🇷': ['Greece', '希腊', '希臘'],
+ '🇬🇺': ['Guam', '关岛', '關島'],
+ '🇬🇹': ['Guatemala', '危地马拉'],
+ '🇭🇰': [
+ 'Hongkong',
+ '香港',
+ 'Hong Kong',
+ 'HongKong',
+ 'HONG KONG',
+ '深港',
+ '沪港',
+ '呼港',
+ '穗港',
+ '京港',
+ '港',
+ ],
+ '🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'],
+ '🇭🇺': ['Hungary', '匈牙利'],
+ '🇮🇶': ['Iraq', '伊拉克', '巴格达', 'Baghdad'], // 伊拉克
+ '🇯🇴': ['Jordan', '约旦'],
+ '🇯🇵': [
+ 'Japan',
+ '日本',
+ '东京',
+ '大阪',
+ '埼玉',
+ '沪日',
+ '穗日',
+ '川日',
+ '中日',
+ '泉日',
+ '杭日',
+ '深日',
+ '辽日',
+ '广日',
+ '大坂',
+ 'Osaka',
+ 'Tokyo',
+ ],
+ '🇰🇪': ['Kenya', '肯尼亚'],
+ '🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'],
+ '🇰🇭': ['Cambodia', '柬埔寨'],
+ '🇰🇵': ['North Korea', '朝鲜'],
+ '🇰🇷': [
+ 'Korea',
+ '韩国',
+ '韓國',
+ '韩',
+ '韓',
+ '首尔',
+ '春川',
+ 'Chuncheon',
+ 'Seoul',
+ ],
+ '🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'],
+ '🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
+ '🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
+ '🇮🇱': ['Israel', '以色列'],
+ '🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
+ '🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
+ '🇮🇷': ['Iran', '伊朗'],
+ '🇮🇸': ['Iceland', '冰岛', '冰島'],
+ '🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
+ '🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
+ '🇱🇦': ['Laos', '老挝', '老撾'],
+ '🇱🇸': ['Lesotho', '莱索托'],
+ '🇱🇹': ['Lithuania', '立陶宛'],
+ '🇱🇺': ['Luxembourg', '卢森堡'],
+ '🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
+ '🇲🇦': ['Morocco', '摩洛哥'],
+ '🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
+ '🇲🇲': ['Myanmar', '缅甸', '緬甸'],
+ '🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
+ '🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
+ '🇲🇳': ['Mongolia', '蒙古'],
+ '🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
+ '🇲🇹': ['Malta', '马耳他'],
+ '🇲🇽': ['Mexico', '墨西哥'],
+ '🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
+ '🇳🇱': [
+ 'Netherlands',
+ '荷兰',
+ '荷蘭',
+ '尼德蘭',
+ '阿姆斯特丹',
+ 'Amsterdam',
+ ],
+ '🇳🇴': ['Norway', '挪威'],
+ '🇳🇵': ['Nepal', '尼泊尔'],
+ '🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
+ '🇴🇲': ['Oman', '阿曼', '马斯喀特'],
+ '🇵🇦': ['Panama', '巴拿马'],
+ '🇵🇪': ['Peru', '秘鲁', '祕魯'],
+ '🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
+ '🇵🇰': ['Pakistan', '巴基斯坦'],
+ '🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
+ '🇵🇷': ['Puerto Rico', '波多黎各'],
+ '🇵🇹': ['Portugal', '葡萄牙'],
+ '🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
+ '🇵🇾': ['Paraguay', '巴拉圭'],
+ '🇶🇦': ['Qatar', '卡塔尔', '卡塔爾'],
+ '🇷🇴': ['Romania', '罗马尼亚'],
+ '🇷🇸': ['Serbia', '塞尔维亚'],
+ '🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'],
+ '🇷🇺': [
+ 'Russia',
+ '俄罗斯',
+ '俄国',
+ '俄羅斯',
+ '伯力',
+ '莫斯科',
+ '圣彼得堡',
+ '西伯利亚',
+ '京俄',
+ '杭俄',
+ '廣俄',
+ '滬俄',
+ '广俄',
+ '沪俄',
+ 'Moscow',
+ ],
+ '🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
+ '🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
+ '🇸🇬': [
+ 'Singapore',
+ '新加坡',
+ '狮城',
+ '沪新',
+ '京新',
+ '中新',
+ '泉新',
+ '穗新',
+ '深新',
+ '杭新',
+ '广新',
+ '廣新',
+ '滬新',
+ ],
+ '🇸🇮': ['Slovenia', '斯洛文尼亚'],
+ '🇸🇰': ['Slovakia', '斯洛伐克'],
+ '🇹🇬': ['Togo', '多哥', '洛美', 'Lomé', 'Lome'], // 多哥
+ '🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
+ '🇹🇳': ['Tunisia', '突尼斯'],
+ '🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
+ '🇹🇼': [
+ 'Taiwan',
+ '台湾',
+ '臺灣',
+ '台灣',
+ '中華民國',
+ '中华民国',
+ '台北',
+ '台中',
+ '新北',
+ '彰化',
+ '台',
+ '臺',
+ 'Taipei',
+ 'Tai Wan',
+ ],
+ '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
+ '🇺🇸': [
+ 'United States',
+ '美国',
+ 'America',
+ '美',
+ '京美',
+ '波特兰',
+ '达拉斯',
+ '俄勒冈',
+ 'Oregon',
+ '凤凰城',
+ '费利蒙',
+ '硅谷',
+ '矽谷',
+ '拉斯维加斯',
+ '洛杉矶',
+ '圣何塞',
+ '圣克拉拉',
+ '西雅图',
+ '芝加哥',
+ '沪美',
+ '哥伦布',
+ '纽约',
+ 'New York',
+ 'Los Angeles',
+ 'San Jose',
+ 'Sillicon Valley',
+ 'Michigan',
+ '俄亥俄',
+ 'Ohio',
+ '马纳萨斯',
+ 'Manassas',
+ '弗吉尼亚',
+ 'Virginia',
+ ],
+ '🇺🇾': ['Uruguay', '乌拉圭'],
+ // 新增 梵蒂冈 及别名
+ '🇻🇦': ['Vatican', 'Vatican City', 'Holy See', '梵蒂冈', '梵蒂岡'],
+ '🇻🇪': ['Venezuela', '委内瑞拉'],
+ '🇻🇳': ['Vietnam', '越南', '胡志明'],
+ '🇿🇦': ['South Africa', '南非'],
+ '🇨🇳': [
+ 'China',
+ '中国',
+ '中國',
+ '回国',
+ '回國',
+ '国内',
+ '國內',
+ '华东',
+ '华西',
+ '华南',
+ '华北',
+ '华中',
+ '江苏',
+ '北京',
+ '上海',
+ '广州',
+ '深圳',
+ '杭州',
+ '徐州',
+ '青岛',
+ '宁波',
+ '镇江',
+ ],
+ };
+
+ // 原旗帜或空
+ let Flag =
+ name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
+ '🏴☠️';
+ //console.log(`oldFlag = ${Flag}`)
+ // 旗帜匹配
+ for (let flag of Object.keys(Flags)) {
+ const keywords = Flags[flag];
+ //console.log(`keywords = ${keywords}`)
+ if (
+ // 不精确匹配(只要包含就算,忽略大小写)
+ keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
+ ) {
+ if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
+ return (Flag = '🇨🇳');
+ }
+ return (Flag = flag);
+ }
+ }
+ // ISO旗帜匹配
+ for (let flag of Object.keys(ISOFlags)) {
+ const keywords = ISOFlags[flag];
+ //console.log(`keywords = ${keywords}`)
+ if (
+ // 精确匹配(两侧均有分割)
+ keywords.some((keyword) =>
+ RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
+ )
+ ) {
+ const isCN2 =
+ flag == '🇨🇳' &&
+ RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
+ if (!isCN2) {
+ return (Flag = flag);
+ }
+ }
+ }
+
+ //console.log(`Final Flag = ${Flag}`)
+ return Flag;
+}
+
+export function getISO(name) {
+ return ISOFlags[getFlag(name)]?.[0];
+}
+
+// remove flag
+export function removeFlag(str) {
+ return str
+ .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
+ .trim();
+}
+
+export class MMDB {
+ constructor({ country, asn } = {}) {
+ if ($.env.isNode) {
+ const Reader = eval(`require("@maxmind/geoip2-node")`).Reader;
+ const fs = eval("require('fs')");
+ const countryFile =
+ country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
+ const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');
+ // $.info(
+ // `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(
+ // countryFile,
+ // )}`,
+ // );
+ if (countryFile) {
+ this.countryReader = Reader.openBuffer(
+ fs.readFileSync(countryFile),
+ );
+ }
+ // $.info(
+ // `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(
+ // asnFile,
+ // )}`,
+ // );
+ if (asnFile) {
+ if (!fs.existsSync(asnFile))
+ throw new Error('GeoLite2 ASN MMDB does not exist');
+ this.asnReader = Reader.openBuffer(fs.readFileSync(asnFile));
+ }
+ }
+ }
+ geoip(ip) {
+ return this.countryReader?.country(ip)?.country?.isoCode;
+ }
+ ipaso(ip) {
+ return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
+ }
+ ipasn(ip) {
+ return this.asnReader?.asn(ip)?.autonomousSystemNumber;
+ }
+}
diff --git a/backend/src/utils/gist.js b/backend/src/utils/gist.js
new file mode 100644
index 0000000000..8131ae2015
--- /dev/null
+++ b/backend/src/utils/gist.js
@@ -0,0 +1,416 @@
+import { HTTP, ENV } from '@/vendor/open-api';
+import { getPolicyDescriptor } from '@/utils';
+import $ from '@/core/app';
+import { SETTINGS_KEY } from '@/constants';
+
+const DEFAULT_GITHUB_API_URL = 'https://api.github.com';
+
+function describeGistApiErrorResponse(resp) {
+ let body;
+ try {
+ body = JSON.parse(resp.body);
+ } catch (e) {
+ //
+ }
+ const message =
+ body?.message?.error ??
+ body?.error ??
+ body?.message ??
+ resp.body ??
+ 'Unknown error';
+ return `ERROR: HTTP ${resp.statusCode}: ${message}`;
+}
+
+function normalizeApiUrl(url, fallback = DEFAULT_GITHUB_API_URL) {
+ const normalizedUrl = String(url ?? '').trim() || fallback;
+
+ return normalizedUrl.replace(/\/+$/, '');
+}
+
+export function getGithubGistBaseURL({ githubApiUrl, githubProxy } = {}) {
+ const normalizedGithubApiUrl = normalizeApiUrl(githubApiUrl);
+ const isCustomGithubApiUrl =
+ normalizedGithubApiUrl !== DEFAULT_GITHUB_API_URL;
+ const normalizedGithubProxy = String(githubProxy || '')
+ .trim()
+ .replace(/\/+$/, '');
+
+ if (isCustomGithubApiUrl) {
+ return normalizedGithubApiUrl;
+ }
+
+ return `${
+ normalizedGithubProxy ? `${normalizedGithubProxy}/` : ''
+ }${DEFAULT_GITHUB_API_URL}`;
+}
+
+export function hasGistSyncCredentials(settings = {}) {
+ return Boolean(settings?.gistToken);
+}
+
+export { describeGistApiErrorResponse };
+
+/**
+ * Gist backup
+ */
+export default class Gist {
+ constructor({ token, key, syncPlatform }) {
+ const { isStash, isLoon, isShadowRocket, isQX } = ENV();
+ const {
+ defaultProxy,
+ githubApiTimeout,
+ githubProxy,
+ githubApiUrl,
+ } = $.read(SETTINGS_KEY);
+ const githubApiRequestTimeout = githubApiTimeout || 10000;
+ const githubGistBaseURL = getGithubGistBaseURL({
+ githubApiUrl,
+ githubProxy,
+ });
+ let proxy = defaultProxy;
+ if ($.env.isNode) {
+ proxy =
+ proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
+ }
+
+ if (syncPlatform === 'gitlab') {
+ this.headers = {
+ 'PRIVATE-TOKEN': `${token}`,
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
+ };
+ this.http = HTTP({
+ baseURL: 'https://gitlab.com/api/v4',
+ headers: {
+ ...this.headers,
+ ...(isStash && proxy
+ ? {
+ 'X-Stash-Selected-Proxy':
+ encodeURIComponent(proxy),
+ }
+ : {}),
+ ...(isShadowRocket && proxy
+ ? { 'X-Surge-Policy': proxy }
+ : {}),
+ },
+
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ timeout: githubApiRequestTimeout,
+
+ events: {
+ onResponse: (resp) => {
+ if (/^[45]/.test(String(resp.statusCode))) {
+ return Promise.reject(
+ describeGistApiErrorResponse(resp),
+ );
+ } else {
+ return resp;
+ }
+ },
+ },
+ });
+ } else {
+ this.headers = {
+ Authorization: `token ${token}`,
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
+ };
+ this.http = HTTP({
+ baseURL: githubGistBaseURL,
+ headers: {
+ ...this.headers,
+ ...(isStash && proxy
+ ? {
+ 'X-Stash-Selected-Proxy':
+ encodeURIComponent(proxy),
+ }
+ : {}),
+ ...(isShadowRocket && proxy
+ ? { 'X-Surge-Policy': proxy }
+ : {}),
+ },
+
+ ...(proxy ? { proxy } : {}),
+ ...(isLoon && proxy ? { node: proxy } : {}),
+ ...(isQX && proxy ? { opts: { policy: proxy } } : {}),
+ ...(proxy ? getPolicyDescriptor(proxy) : {}),
+ timeout: githubApiRequestTimeout,
+
+ events: {
+ onResponse: (resp) => {
+ if (/^[45]/.test(String(resp.statusCode))) {
+ return Promise.reject(
+ describeGistApiErrorResponse(resp),
+ );
+ } else {
+ return resp;
+ }
+ },
+ },
+ });
+ }
+
+ this.key = key;
+ this.syncPlatform = syncPlatform;
+ }
+
+ async locate() {
+ if (this.syncPlatform === 'gitlab') {
+ return this.http.get('/snippets').then((response) => {
+ const gists = JSON.parse(response.body);
+
+ for (let g of gists) {
+ if (g.title === this.key) {
+ return g;
+ }
+ }
+ return;
+ });
+ } else {
+ return this.http
+ .get('/gists?per_page=100&page=1')
+ .then((response) => {
+ const gists = JSON.parse(response.body);
+ $.info(`获取到当前 GitHub 用户的 gist: ${gists.length} 个`);
+ for (let g of gists) {
+ if (g.description === this.key) {
+ return g;
+ }
+ }
+ return;
+ });
+ }
+ }
+
+ async upload(input, options = {}) {
+ if (Object.keys(input).length === 0) {
+ return Promise.reject('未提供需上传的文件');
+ }
+
+ const gist = await this.locate();
+
+ let files = input;
+ const emptyFileFallback = options.emptyFileFallback;
+ const hasEmptyFileFallback = Boolean(emptyFileFallback?.filename);
+ const uploadMeta = {};
+
+ const attachUploadMeta = (request) =>
+ request.then((response) => {
+ if (Object.keys(uploadMeta).length > 0) {
+ response.subStoreUploadMeta = uploadMeta;
+ }
+ return response;
+ });
+
+ const applyEmptyFileFallback = ({ actions, existingFiles, result }) => {
+ if (!hasEmptyFileFallback) return;
+
+ const filename = emptyFileFallback.filename;
+ const content = emptyFileFallback.content ?? '';
+ const realFileKeys = Object.keys(result).filter(
+ (key) => key !== filename,
+ );
+
+ if (Object.keys(result).length === 0) {
+ result[filename] = { content };
+ uploadMeta.emptyFileFallback = {
+ status: 'created',
+ filename,
+ };
+
+ if (this.syncPlatform === 'gitlab') {
+ actions.push({
+ action: existingFiles[filename] ? 'update' : 'create',
+ file_path: filename,
+ content,
+ });
+ } else {
+ files[filename] = { content };
+ }
+ } else if (result[filename] && realFileKeys.length > 0) {
+ delete result[filename];
+ uploadMeta.emptyFileFallback = {
+ status: 'removed',
+ filename,
+ };
+
+ if (this.syncPlatform === 'gitlab') {
+ actions.push({
+ action: 'delete',
+ file_path: filename,
+ });
+ } else {
+ files[filename] = null;
+ }
+ } else if (result[filename] && realFileKeys.length === 0) {
+ uploadMeta.emptyFileFallback = {
+ status: 'retained',
+ filename,
+ };
+ }
+ };
+
+ if (gist?.id) {
+ if (this.syncPlatform === 'gitlab') {
+ gist.files = gist.files.reduce((acc, item) => {
+ acc[item.path] = item;
+ return acc;
+ }, {});
+ }
+ // console.log(`files`, files);
+ // console.log(`gist`, gist.files);
+ let actions = [];
+ const result = { ...gist.files };
+ Object.keys(files).map((key) => {
+ if (result[key]) {
+ if (
+ files[key].content == null ||
+ files[key].content === ''
+ ) {
+ delete result[key];
+ files[key] = null;
+ actions.push({
+ action: 'delete',
+ file_path: key,
+ });
+ } else {
+ result[key] = files[key];
+ actions.push({
+ action: 'update',
+ file_path: key,
+ content: files[key].content,
+ });
+ }
+ } else {
+ if (
+ files[key].content == null ||
+ files[key].content === ''
+ ) {
+ delete result[key];
+ delete files[key];
+ } else {
+ result[key] = files[key];
+ actions.push({
+ action: 'create',
+ file_path: key,
+ content: files[key].content,
+ });
+ }
+ }
+ });
+ // console.log(`result`, result);
+ // console.log(`files`, files);
+ // console.log(`actions`, actions);
+
+ applyEmptyFileFallback({
+ actions,
+ existingFiles: gist.files,
+ result,
+ });
+
+ if (this.syncPlatform === 'gitlab') {
+ if (Object.keys(result).length === 0) {
+ return Promise.reject(
+ '本次操作将导致所有文件的内容都为空, 无法更新 snippet',
+ );
+ }
+ if (Object.keys(result).length > 10) {
+ return Promise.reject(
+ '本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
+ );
+ }
+ files = actions;
+ return attachUploadMeta(
+ this.http.put({
+ headers: {
+ ...this.headers,
+ 'Content-Type': 'application/json',
+ },
+ url: `/snippets/${gist.id}`,
+ body: JSON.stringify({ files }),
+ }),
+ );
+ } else {
+ if (Object.keys(result).length === 0) {
+ return Promise.reject(
+ '本次操作将导致所有文件的内容都为空, 无法更新 gist',
+ );
+ }
+ return attachUploadMeta(
+ this.http.patch({
+ url: `/gists/${gist.id}`,
+ body: JSON.stringify({ files }),
+ }),
+ );
+ }
+ } else {
+ files = Object.entries(files).reduce((acc, [key, file]) => {
+ if (file.content !== null && file.content !== '') {
+ acc[key] = file;
+ }
+ return acc;
+ }, {});
+ if (this.syncPlatform === 'gitlab') {
+ if (Object.keys(files).length === 0) {
+ return Promise.reject(
+ '所有文件的内容都为空, 无法创建 snippet',
+ );
+ }
+ files = Object.keys(files).map((key) => ({
+ file_path: key,
+ content: files[key].content,
+ }));
+ return attachUploadMeta(
+ this.http.post({
+ headers: {
+ ...this.headers,
+ 'Content-Type': 'application/json',
+ },
+ url: '/snippets',
+ body: JSON.stringify({
+ title: this.key,
+ visibility: 'private',
+ files,
+ }),
+ }),
+ );
+ } else {
+ if (Object.keys(files).length === 0) {
+ return Promise.reject(
+ '所有文件的内容都为空, 无法创建 gist',
+ );
+ }
+ return attachUploadMeta(
+ this.http.post({
+ url: '/gists',
+ body: JSON.stringify({
+ description: this.key,
+ public: false,
+ files,
+ }),
+ }),
+ );
+ }
+ }
+ }
+
+ async download(filename) {
+ const gist = await this.locate();
+ if (gist?.id) {
+ try {
+ const { files } = await this.http
+ .get(`/gists/${gist.id}`)
+ .then((resp) => JSON.parse(resp.body));
+ const url = files[filename].raw_url;
+ return await this.http.get(url).then((resp) => resp.body);
+ } catch (err) {
+ return Promise.reject(err);
+ }
+ } else {
+ return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
+ }
+ }
+}
diff --git a/backend/src/utils/headers-resource-cache.js b/backend/src/utils/headers-resource-cache.js
new file mode 100644
index 0000000000..3bfee3fa53
--- /dev/null
+++ b/backend/src/utils/headers-resource-cache.js
@@ -0,0 +1,103 @@
+import $ from '@/core/app';
+import {
+ HEADERS_RESOURCE_CACHE_KEY,
+ DEFAULT_HEADERS_CACHE_TTL,
+ SETTINGS_KEY,
+} from '@/constants';
+
+class ResourceCache {
+ constructor() {
+ if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
+ $.write('{}', HEADERS_RESOURCE_CACHE_KEY);
+ }
+ try {
+ this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
+ } catch (e) {
+ $.error(
+ `解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
+ e?.message ?? e
+ }`,
+ );
+ this.resourceCache = {};
+ $.write('{}', HEADERS_RESOURCE_CACHE_KEY);
+ }
+ this._cleanup();
+ }
+
+ _cleanup(prefix, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ let clear = false;
+ const now = Date.now();
+ Object.entries(this.resourceCache).forEach((entry) => {
+ const [id, cached] = entry;
+ const shouldDelete =
+ !cached.time || cached.time < now + resolvedTTL;
+ if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {
+ delete this.resourceCache[id];
+ clear = true;
+ }
+ });
+ if (clear) this._persist();
+ }
+
+ revokeAll() {
+ this.resourceCache = {};
+ this._persist();
+ }
+
+ _persist() {
+ $.write(JSON.stringify(this.resourceCache), HEADERS_RESOURCE_CACHE_KEY);
+ }
+
+ gettime(id) {
+ const time = this.resourceCache[id] && this.resourceCache[id].time;
+ if (time && new Date().getTime() <= time) {
+ return this.resourceCache[id].time;
+ }
+ return null;
+ }
+
+ get(id, ttl, remove) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ const cached = this.resourceCache[id];
+ const time = cached && cached.time;
+ if (time) {
+ if (Date.now() + resolvedTTL <= time) return cached.data;
+ if (remove) {
+ delete this.resourceCache[id];
+ this._persist();
+ }
+ }
+ return null;
+ }
+
+ set(id, value, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? getTTL();
+ this.resourceCache[id] = {
+ time: Date.now() + resolvedTTL,
+ data: value,
+ };
+ this._persist();
+ }
+}
+
+function normalizeTTL(ttl) {
+ const value = Number(ttl);
+ if (!isFinite(value)) return null;
+ if (value > 0) return value;
+ return null;
+}
+
+function getTTL() {
+ const settings = $.read(SETTINGS_KEY);
+ let ttl = settings?.headersCacheTtl;
+ if (ttl) {
+ ttl = Number(ttl);
+ if (isFinite(ttl) && ttl > 0) {
+ return ttl * 1000;
+ }
+ }
+ return DEFAULT_HEADERS_CACHE_TTL;
+}
+
+export default new ResourceCache();
diff --git a/backend/src/utils/index.js b/backend/src/utils/index.js
new file mode 100644
index 0000000000..f78146ccc9
--- /dev/null
+++ b/backend/src/utils/index.js
@@ -0,0 +1,176 @@
+import * as ipAddress from 'ip-address';
+// source: https://stackoverflow.com/a/36760050
+const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
+
+// source: https://ihateregex.io/expr/ipv6/
+const IPV6_REGEX =
+ /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
+
+function isIPv4(ip) {
+ return IPV4_REGEX.test(ip);
+}
+
+function isIPv6(ip) {
+ return IPV6_REGEX.test(ip);
+}
+
+function isValidPortNumber(port) {
+ return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
+ port,
+ );
+}
+
+function isNotBlank(str) {
+ return typeof str === 'string' && str.trim().length > 0;
+}
+
+function getIfNotBlank(str, defaultValue) {
+ return isNotBlank(str) ? str : defaultValue;
+}
+
+function isPresent(obj) {
+ return typeof obj !== 'undefined' && obj !== null;
+}
+
+function getIfPresent(obj, defaultValue) {
+ return isPresent(obj) ? obj : defaultValue;
+}
+
+function getPolicyDescriptor(str) {
+ if (!str) return {};
+ return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
+ ? {
+ 'policy-descriptor': str,
+ }
+ : {
+ policy: str,
+ };
+}
+
+// const utf8ArrayToStr =
+// typeof TextDecoder !== 'undefined'
+// ? (v) => new TextDecoder().decode(new Uint8Array(v))
+// : (function () {
+// var charCache = new Array(128); // Preallocate the cache for the common single byte chars
+// var charFromCodePt = String.fromCodePoint || String.fromCharCode;
+// var result = [];
+
+// return function (array) {
+// var codePt, byte1;
+// var buffLen = array.length;
+
+// result.length = 0;
+
+// for (var i = 0; i < buffLen; ) {
+// byte1 = array[i++];
+
+// if (byte1 <= 0x7f) {
+// codePt = byte1;
+// } else if (byte1 <= 0xdf) {
+// codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
+// } else if (byte1 <= 0xef) {
+// codePt =
+// ((byte1 & 0x0f) << 12) |
+// ((array[i++] & 0x3f) << 6) |
+// (array[i++] & 0x3f);
+// } else if (String.fromCodePoint) {
+// codePt =
+// ((byte1 & 0x07) << 18) |
+// ((array[i++] & 0x3f) << 12) |
+// ((array[i++] & 0x3f) << 6) |
+// (array[i++] & 0x3f);
+// } else {
+// codePt = 63; // Cannot convert four byte code points, so use "?" instead
+// i += 3;
+// }
+
+// result.push(
+// charCache[codePt] ||
+// (charCache[codePt] = charFromCodePt(codePt)),
+// );
+// }
+
+// return result.join('');
+// };
+// })();
+
+function getRandomInt(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function getRandomPort(portString) {
+ let portParts = portString.split(/,|\//);
+ let randomPart = portParts[Math.floor(Math.random() * portParts.length)];
+ if (randomPart.includes('-')) {
+ let [min, max] = randomPart.split('-').map(Number);
+ return getRandomInt(min, max);
+ } else {
+ return Number(randomPart);
+ }
+}
+
+function numberToString(value) {
+ return Number.isSafeInteger(value)
+ ? String(value)
+ : BigInt(value).toString();
+}
+
+function isValidUUID(uuid) {
+ return (
+ typeof uuid === 'string' &&
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
+ uuid,
+ )
+ );
+}
+
+function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
+ const d = date instanceof Date ? date : new Date(date);
+
+ if (isNaN(d.getTime())) {
+ return '';
+ }
+
+ const pad = (num) => String(num).padStart(2, '0');
+
+ const replacements = {
+ YYYY: d.getFullYear(),
+ MM: pad(d.getMonth() + 1),
+ DD: pad(d.getDate()),
+ HH: pad(d.getHours()),
+ mm: pad(d.getMinutes()),
+ ss: pad(d.getSeconds()),
+ };
+
+ return format.replace(
+ /YYYY|MM|DD|HH|mm|ss/g,
+ (match) => replacements[match],
+ );
+}
+
+function isPlainObject(obj) {
+ return (
+ obj !== null &&
+ typeof obj === 'object' &&
+ [null, Object.prototype].includes(Object.getPrototypeOf(obj))
+ );
+}
+export {
+ isPlainObject,
+ formatDateTime,
+ isValidUUID,
+ ipAddress,
+ isIPv4,
+ isIPv6,
+ isValidPortNumber,
+ isNotBlank,
+ getIfNotBlank,
+ isPresent,
+ getIfPresent,
+ // utf8ArrayToStr,
+ getPolicyDescriptor,
+ getRandomPort,
+ numberToString,
+};
diff --git a/backend/src/utils/logical.js b/backend/src/utils/logical.js
new file mode 100644
index 0000000000..c58cc5771c
--- /dev/null
+++ b/backend/src/utils/logical.js
@@ -0,0 +1,17 @@
+function AND(...args) {
+ return args.reduce((a, b) => a.map((c, i) => b[i] && c));
+}
+
+function OR(...args) {
+ return args.reduce((a, b) => a.map((c, i) => b[i] || c));
+}
+
+function NOT(array) {
+ return array.map((c) => !c);
+}
+
+function FULL(length, bool) {
+ return [...Array(length).keys()].map(() => bool);
+}
+
+export { AND, OR, NOT, FULL };
diff --git a/backend/src/utils/migration.js b/backend/src/utils/migration.js
new file mode 100644
index 0000000000..2542980d03
--- /dev/null
+++ b/backend/src/utils/migration.js
@@ -0,0 +1,140 @@
+import {
+ SUBS_KEY,
+ COLLECTIONS_KEY,
+ SCHEMA_VERSION_KEY,
+ ARTIFACTS_KEY,
+ RULES_KEY,
+ FILES_KEY,
+ TOKENS_KEY,
+} from '@/constants';
+import $ from '@/core/app';
+
+export default function migrate() {
+ migrateV2();
+}
+
+function migrateV2() {
+ const version = $.read(SCHEMA_VERSION_KEY);
+ if (!version) doMigrationV2();
+
+ // write the current version
+ if (version !== '2.0') {
+ $.write('2.0', SCHEMA_VERSION_KEY);
+ }
+}
+
+function doMigrationV2() {
+ $.info('Start migrating...');
+ // 1. migrate subscriptions
+ const subs = $.read(SUBS_KEY) || {};
+ const newSubs = Object.values(subs).map((sub) => {
+ // set default source to remote
+ sub.source = sub.source || 'remote';
+
+ migrateDisplayName(sub);
+ migrateProcesses(sub);
+ return sub;
+ });
+ $.write(newSubs, SUBS_KEY);
+
+ // 2. migrate collections
+ const collections = $.read(COLLECTIONS_KEY) || {};
+ const newCollections = Object.values(collections).map((collection) => {
+ delete collection.ua;
+ migrateDisplayName(collection);
+ migrateProcesses(collection);
+ return collection;
+ });
+ $.write(newCollections, COLLECTIONS_KEY);
+
+ // 3. migrate artifacts
+ const artifacts = $.read(ARTIFACTS_KEY) || {};
+ const newArtifacts = Object.values(artifacts);
+ $.write(newArtifacts, ARTIFACTS_KEY);
+
+ // 4. migrate rules
+ const rules = $.read(RULES_KEY) || {};
+ const newRules = Object.values(rules);
+ $.write(newRules, RULES_KEY);
+
+ // 5. migrate files
+ const files = $.read(FILES_KEY) || {};
+ const newFiles = Object.values(files);
+ $.write(newFiles, FILES_KEY);
+
+ // 6. migrate tokens
+ const tokens = $.read(TOKENS_KEY) || {};
+ const newTokens = Object.values(tokens);
+ $.write(newTokens, TOKENS_KEY);
+
+ // 7. delete builtin rules
+ delete $.cache.builtin;
+ $.info('Migration complete!');
+
+ function migrateDisplayName(item) {
+ const displayName = item['display-name'];
+ if (displayName) {
+ item.displayName = displayName;
+ delete item['display-name'];
+ }
+ }
+
+ function migrateProcesses(item) {
+ const processes = item.process;
+ if (!processes || processes.length === 0) return;
+ const newProcesses = [];
+ const quickSettingOperator = {
+ type: 'Quick Setting Operator',
+ args: {
+ udp: 'DEFAULT',
+ tfo: 'DEFAULT',
+ scert: 'DEFAULT',
+ 'vmess aead': 'DEFAULT',
+ useless: 'DEFAULT',
+ },
+ };
+ for (const p of processes) {
+ if (!p.type) continue;
+ if (p.type === 'Useless Filter') {
+ quickSettingOperator.args.useless = 'ENABLED';
+ } else if (p.type === 'Set Property Operator') {
+ const { key, value } = p.args;
+ switch (key) {
+ case 'udp':
+ quickSettingOperator.args.udp = value
+ ? 'ENABLED'
+ : 'DISABLED';
+ break;
+ case 'tfo':
+ quickSettingOperator.args.tfo = value
+ ? 'ENABLED'
+ : 'DISABLED';
+ break;
+ case 'skip-cert-verify':
+ quickSettingOperator.args.scert = value
+ ? 'ENABLED'
+ : 'DISABLED';
+ break;
+ case 'aead':
+ quickSettingOperator.args['vmess aead'] = value
+ ? 'ENABLED'
+ : 'DISABLED';
+ break;
+ }
+ } else if (p.type.indexOf('Keyword') !== -1) {
+ // drop keyword operators and keyword filters
+ } else if (p.type === 'Flag Operator') {
+ // set default args
+ const add = typeof p.args === 'undefined' ? true : p.args;
+ p.args = {
+ mode: add ? 'add' : 'remove',
+ };
+ newProcesses.push(p);
+ } else {
+ newProcesses.push(p);
+ }
+ }
+ newProcesses.unshift(quickSettingOperator);
+ item.process = newProcesses;
+ }
+}
diff --git a/backend/src/utils/resource-cache.js b/backend/src/utils/resource-cache.js
new file mode 100644
index 0000000000..63bf5b8d01
--- /dev/null
+++ b/backend/src/utils/resource-cache.js
@@ -0,0 +1,103 @@
+import $ from '@/core/app';
+import {
+ RESOURCE_CACHE_KEY,
+ DEFAULT_CACHE_TTL,
+ SETTINGS_KEY,
+} from '@/constants';
+
+class ResourceCache {
+ constructor() {
+ if (!$.read(RESOURCE_CACHE_KEY)) {
+ $.write('{}', RESOURCE_CACHE_KEY);
+ }
+ try {
+ this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
+ } catch (e) {
+ $.error(
+ `解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
+ e?.message ?? e
+ }`,
+ );
+ this.resourceCache = {};
+ $.write('{}', RESOURCE_CACHE_KEY);
+ }
+ this._cleanup();
+ }
+
+ _cleanup(prefix, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ let clear = false;
+ const now = Date.now();
+ Object.entries(this.resourceCache).forEach((entry) => {
+ const [id, cached] = entry;
+ const shouldDelete =
+ !cached.time || cached.time < now + resolvedTTL;
+ if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {
+ delete this.resourceCache[id];
+ clear = true;
+ }
+ });
+ if (clear) this._persist();
+ }
+
+ revokeAll() {
+ this.resourceCache = {};
+ this._persist();
+ }
+
+ _persist() {
+ $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
+ }
+
+ gettime(id) {
+ const time = this.resourceCache[id] && this.resourceCache[id].time;
+ if (time && new Date().getTime() <= time) {
+ return this.resourceCache[id].time;
+ }
+ return null;
+ }
+
+ get(id, ttl, remove) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ const cached = this.resourceCache[id];
+ const time = cached && cached.time;
+ if (time) {
+ if (Date.now() + resolvedTTL <= time) return cached.data;
+ if (remove) {
+ delete this.resourceCache[id];
+ this._persist();
+ }
+ }
+ return null;
+ }
+
+ set(id, value, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? getTTL();
+ this.resourceCache[id] = {
+ time: Date.now() + resolvedTTL,
+ data: value,
+ };
+ this._persist();
+ }
+}
+
+function normalizeTTL(ttl) {
+ const value = Number(ttl);
+ if (!isFinite(value)) return null;
+ if (value > 0) return value;
+ return null;
+}
+
+function getTTL() {
+ const settings = $.read(SETTINGS_KEY);
+ let ttl = settings?.resourceCacheTtl;
+ if (ttl) {
+ ttl = Number(ttl);
+ if (isFinite(ttl) && ttl > 0) {
+ return ttl * 1000;
+ }
+ }
+ return DEFAULT_CACHE_TTL;
+}
+
+export default new ResourceCache();
diff --git a/backend/src/utils/rs.js b/backend/src/utils/rs.js
new file mode 100644
index 0000000000..bf9c465620
--- /dev/null
+++ b/backend/src/utils/rs.js
@@ -0,0 +1,11 @@
+import rs from 'jsrsasign';
+
+export function generateFingerprint(caStr) {
+ const hex = rs.pemtohex(caStr);
+ const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');
+ return fingerPrint.match(/.{2}/g).join(':').toUpperCase();
+}
+
+export default {
+ generateFingerprint,
+};
diff --git a/backend/src/utils/script-resource-cache.js b/backend/src/utils/script-resource-cache.js
new file mode 100644
index 0000000000..ef2c8ea0fd
--- /dev/null
+++ b/backend/src/utils/script-resource-cache.js
@@ -0,0 +1,103 @@
+import $ from '@/core/app';
+import {
+ SCRIPT_RESOURCE_CACHE_KEY,
+ DEFAULT_SCRIPT_CACHE_TTL,
+ SETTINGS_KEY,
+} from '@/constants';
+
+class ResourceCache {
+ constructor() {
+ if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
+ $.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
+ }
+ try {
+ this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
+ } catch (e) {
+ $.error(
+ `解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
+ e?.message ?? e
+ }`,
+ );
+ this.resourceCache = {};
+ $.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
+ }
+ this._cleanup();
+ }
+
+ _cleanup(prefix, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ let clear = false;
+ const now = Date.now();
+ Object.entries(this.resourceCache).forEach((entry) => {
+ const [id, cached] = entry;
+ const shouldDelete =
+ !cached.time || cached.time < now + resolvedTTL;
+ if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) {
+ delete this.resourceCache[id];
+ clear = true;
+ }
+ });
+ if (clear) this._persist();
+ }
+
+ revokeAll() {
+ this.resourceCache = {};
+ this._persist();
+ }
+
+ _persist() {
+ $.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
+ }
+
+ gettime(id) {
+ const time = this.resourceCache[id] && this.resourceCache[id].time;
+ if (time && new Date().getTime() <= time) {
+ return this.resourceCache[id].time;
+ }
+ return null;
+ }
+
+ get(id, ttl, remove) {
+ const resolvedTTL = normalizeTTL(ttl) ?? 0;
+ const cached = this.resourceCache[id];
+ const time = cached && cached.time;
+ if (time) {
+ if (Date.now() + resolvedTTL <= time) return cached.data;
+ if (remove) {
+ delete this.resourceCache[id];
+ this._persist();
+ }
+ }
+ return null;
+ }
+
+ set(id, value, ttl) {
+ const resolvedTTL = normalizeTTL(ttl) ?? getTTL();
+ this.resourceCache[id] = {
+ time: Date.now() + resolvedTTL,
+ data: value,
+ };
+ this._persist();
+ }
+}
+
+function normalizeTTL(ttl) {
+ const value = Number(ttl);
+ if (!isFinite(value)) return null;
+ if (value > 0) return value;
+ return null;
+}
+
+function getTTL() {
+ const settings = $.read(SETTINGS_KEY);
+ let ttl = settings?.scriptCacheTtl;
+ if (ttl) {
+ ttl = Number(ttl);
+ if (isFinite(ttl) && ttl > 0) {
+ return ttl * 1000;
+ }
+ }
+ return DEFAULT_SCRIPT_CACHE_TTL;
+}
+
+export default new ResourceCache();
diff --git a/backend/src/utils/user-agent.js b/backend/src/utils/user-agent.js
new file mode 100644
index 0000000000..f9c71a673b
--- /dev/null
+++ b/backend/src/utils/user-agent.js
@@ -0,0 +1,58 @@
+export function getUserAgentFromHeaders(headers) {
+ const keys = Object.keys(headers);
+ let UA = '';
+ let ua = '';
+ let accept = '';
+ for (let k of keys) {
+ const lower = k.toLowerCase();
+ if (lower === 'user-agent') {
+ UA = headers[k];
+ ua = UA.toLowerCase();
+ } else if (lower === 'accept') {
+ accept = headers[k];
+ }
+ }
+ return { UA, ua, accept };
+}
+
+export function getPlatformFromUserAgent({ ua, UA, accept }) {
+ if (UA.indexOf('Quantumult%20X') !== -1) {
+ return 'QX';
+ } else if (ua.indexOf('egern') !== -1) {
+ return 'Egern';
+ } else if (UA.indexOf('Surfboard') !== -1) {
+ return 'Surfboard';
+ } else if (UA.indexOf('Surge Mac') !== -1) {
+ return 'SurgeMac';
+ } else if (UA.indexOf('Surge') !== -1) {
+ return 'Surge';
+ } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
+ return 'Loon';
+ } else if (UA.indexOf('Shadowrocket') !== -1) {
+ return 'Shadowrocket';
+ } else if (UA.indexOf('Stash') !== -1) {
+ return 'Stash';
+ } else if (
+ ua === 'meta' ||
+ (ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||
+ ua.indexOf('clash-verge') !== -1 ||
+ ua.indexOf('flclash') !== -1
+ ) {
+ return 'ClashMeta';
+ } else if (ua.indexOf('clash') !== -1) {
+ return 'Clash';
+ } else if (ua.indexOf('v2ray') !== -1) {
+ return 'V2Ray';
+ } else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) {
+ return 'sing-box';
+ } else if (accept.indexOf('application/json') === 0) {
+ return 'JSON';
+ } else {
+ return 'V2Ray';
+ }
+}
+
+export function getPlatformFromHeaders(headers) {
+ const { UA, ua, accept } = getUserAgentFromHeaders(headers);
+ return getPlatformFromUserAgent({ ua, UA, accept });
+}
diff --git a/backend/src/utils/yaml.js b/backend/src/utils/yaml.js
new file mode 100644
index 0000000000..c09b8c4d5e
--- /dev/null
+++ b/backend/src/utils/yaml.js
@@ -0,0 +1,39 @@
+import YAML from 'static-js-yaml';
+
+function retry(fn, content, ...args) {
+ try {
+ return fn(content, ...args);
+ } catch (e) {
+ return fn(
+ dump(
+ fn(
+ content.replace(/!\s*/g, '__SubStoreJSYAMLString__'),
+ ...args,
+ ),
+ ).replace(/__SubStoreJSYAMLString__/g, ''),
+ ...args,
+ );
+ }
+}
+
+export function safeLoad(content, ...args) {
+ return retry(YAML.safeLoad, JSON.parse(JSON.stringify(content)), ...args);
+}
+export function load(content, ...args) {
+ return retry(YAML.load, JSON.parse(JSON.stringify(content)), ...args);
+}
+export function safeDump(content, ...args) {
+ return YAML.safeDump(JSON.parse(JSON.stringify(content)), ...args);
+}
+export function dump(content, ...args) {
+ return YAML.dump(JSON.parse(JSON.stringify(content)), ...args);
+}
+
+export default {
+ safeLoad,
+ load,
+ safeDump,
+ dump,
+ parse: safeLoad,
+ stringify: safeDump,
+};
diff --git a/backend/src/vendor/express.js b/backend/src/vendor/express.js
new file mode 100644
index 0000000000..54cd815310
--- /dev/null
+++ b/backend/src/vendor/express.js
@@ -0,0 +1,349 @@
+/* eslint-disable no-undef */
+import { ENV } from './open-api';
+
+export default function express({ substore: $, port, host }) {
+ const { isNode } = ENV();
+ const DEFAULT_HEADERS = {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
+ 'Access-Control-Allow-Headers':
+ 'Origin, X-Requested-With, Content-Type, Accept',
+ 'X-Powered-By': isNode
+ ? eval('process.env.SUB_STORE_X_POWERED_BY') || 'Sub-Store'
+ : 'Sub-Store',
+ };
+
+ // node support
+ if (isNode) {
+ const express_ = eval(`require("express")`);
+ const bodyParser = eval(`require("body-parser")`);
+ const app = express_();
+ const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';
+ $.info(`[BACKEND] body JSON limit: ${limit}`);
+ app.use(
+ bodyParser.json({
+ verify: rawBodySaver,
+ limit,
+ }),
+ );
+ app.use(
+ bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
+ );
+ app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
+ app.use((req, res, next) => {
+ const originalSetHeader = res.setHeader.bind(res);
+
+ res.setHeader = function (name, value) {
+ function normalize(v) {
+ if (typeof v !== 'string') return v;
+
+ if (['profile-web-page-url'].includes(name.toLowerCase())) {
+ try {
+ const url = new URL(v);
+
+ return url.href; // 自动 punycode + 标准化
+ } catch {
+ return v;
+ }
+ }
+
+ return v;
+ }
+
+ try {
+ if (Array.isArray(value)) {
+ value = value.map(normalize);
+ } else {
+ value = normalize(value);
+ }
+
+ return originalSetHeader(name, value);
+ } catch (err) {
+ console.log(`Invalid header ignored\n${name}: ${value}`);
+ return this;
+ }
+ };
+
+ next();
+ });
+ app.use((_, res, next) => {
+ res.set(DEFAULT_HEADERS);
+ next();
+ });
+
+ // adapter
+ app.start = () => {
+ app.get('*', function (req, res) {
+ res.status(404).end();
+ });
+ const listener = app.listen(port, host, () => {
+ const { address, port } = listener.address();
+ $.info(`[BACKEND] listening on ${address}:${port}`);
+ });
+ };
+ return app;
+ }
+
+ // route handlers
+ const handlers = [];
+
+ // http methods
+ const METHODS_NAMES = [
+ 'GET',
+ 'POST',
+ 'PUT',
+ 'DELETE',
+ 'PATCH',
+ 'OPTIONS',
+ "HEAD'",
+ 'ALL',
+ ];
+
+ // dispatch url to route
+ const dispatch = (request, start = 0) => {
+ let { method, url, headers, body } = request;
+ headers = formatHeaders(headers);
+ if (/json/i.test(headers['content-type'])) {
+ body = JSON.parse(body);
+ }
+
+ method = method.toUpperCase();
+ const { path, query } = extractURL(url);
+
+ // pattern match
+ let handler = null;
+ let i;
+ let longestMatchedPattern = 0;
+ for (i = start; i < handlers.length; i++) {
+ if (handlers[i].method === 'ALL' || method === handlers[i].method) {
+ const { pattern } = handlers[i];
+ if (patternMatched(pattern, path)) {
+ if (pattern.split('/').length > longestMatchedPattern) {
+ handler = handlers[i];
+ longestMatchedPattern = pattern.split('/').length;
+ }
+ }
+ }
+ }
+ if (handler) {
+ // dispatch to next handler
+ const next = () => {
+ dispatch(method, url, i);
+ };
+ const req = {
+ method,
+ url,
+ path,
+ query,
+ params: extractPathParams(handler.pattern, path),
+ headers,
+ body,
+ };
+ const res = Response();
+ const cb = handler.callback;
+
+ const errFunc = (err) => {
+ res.status(500).json({
+ status: 'failed',
+ message: `Internal Server Error: ${err}`,
+ });
+ };
+
+ if (cb.constructor.name === 'AsyncFunction') {
+ cb(req, res, next).catch(errFunc);
+ } else {
+ try {
+ cb(req, res, next);
+ } catch (err) {
+ errFunc(err);
+ }
+ }
+ } else {
+ // no route, return 404
+ const res = Response();
+ res.status(404).json({
+ status: 'failed',
+ message: 'ERROR: 404 not found',
+ });
+ }
+ };
+
+ const app = {};
+
+ // attach http methods
+ METHODS_NAMES.forEach((method) => {
+ app[method.toLowerCase()] = (pattern, callback) => {
+ // add handler
+ handlers.push({ method, pattern, callback });
+ };
+ });
+
+ // chainable route
+ app.route = (pattern) => {
+ const chainApp = {};
+ METHODS_NAMES.forEach((method) => {
+ chainApp[method.toLowerCase()] = (callback) => {
+ // add handler
+ handlers.push({ method, pattern, callback });
+ return chainApp;
+ };
+ });
+ return chainApp;
+ };
+
+ // start service
+ app.start = () => {
+ dispatch($request);
+ };
+
+ return app;
+
+ /************************************************
+ Utility Functions
+ *************************************************/
+ function rawBodySaver(req, res, buf, encoding) {
+ if (buf && buf.length) {
+ req.rawBody = buf.toString(encoding || 'utf8');
+ }
+ }
+
+ function Response() {
+ let statusCode = 200;
+ const { isQX, isLoon, isSurge, isGUIforCores } = ENV();
+ const headers = DEFAULT_HEADERS;
+ const STATUS_CODE_MAP = {
+ 200: 'HTTP/1.1 200 OK',
+ 201: 'HTTP/1.1 201 Created',
+ 302: 'HTTP/1.1 302 Found',
+ 307: 'HTTP/1.1 307 Temporary Redirect',
+ 308: 'HTTP/1.1 308 Permanent Redirect',
+ 404: 'HTTP/1.1 404 Not Found',
+ 500: 'HTTP/1.1 500 Internal Server Error',
+ };
+ return new (class {
+ status(code) {
+ statusCode = code;
+ return this;
+ }
+
+ send(body = '') {
+ const response = {
+ status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,
+ body,
+ headers,
+ };
+ if (isQX || isGUIforCores) {
+ $done(response);
+ } else if (isLoon || isSurge) {
+ $done({
+ response,
+ });
+ }
+ }
+
+ end() {
+ this.send();
+ }
+
+ html(data) {
+ this.set('Content-Type', 'text/html;charset=UTF-8');
+ this.send(data);
+ }
+
+ json(data) {
+ this.set('Content-Type', 'application/json;charset=UTF-8');
+ this.send(JSON.stringify(data));
+ }
+
+ set(key, val) {
+ headers[key] = val;
+ return this;
+ }
+
+ removeHeader(key) {
+ delete headers[key];
+ return this;
+ }
+ })();
+ }
+}
+
+function formatHeaders(headers) {
+ const result = {};
+ for (const k of Object.keys(headers)) {
+ result[k.toLowerCase()] = headers[k];
+ }
+ return result;
+}
+
+function patternMatched(pattern, path) {
+ if (pattern instanceof RegExp && pattern.test(path)) {
+ return true;
+ } else {
+ // root pattern, match all
+ if (pattern === '/') return true;
+ // normal string pattern
+ if (pattern.indexOf(':') === -1) {
+ const spath = path.split('/');
+ const spattern = pattern.split('/');
+ for (let i = 0; i < spattern.length; i++) {
+ if (spath[i] !== spattern[i]) {
+ return false;
+ }
+ }
+ return true;
+ } else if (extractPathParams(pattern, path)) {
+ // string pattern with path parameters
+ return true;
+ }
+ }
+ return false;
+}
+
+function extractURL(url) {
+ // extract path
+ const match = url.match(/https?:\/\/[^/]+(\/[^?]*)/) || [];
+ const path = match[1] || '/';
+
+ // extract query string
+ const split = url.indexOf('?');
+ const query = {};
+ if (split !== -1) {
+ let hashes = url.slice(url.indexOf('?') + 1).split('&');
+ for (let i = 0; i < hashes.length; i++) {
+ const hash = hashes[i].split('=');
+ query[hash[0]] = decodeURIComponent(hash[1]);
+ }
+ }
+ return {
+ path,
+ query,
+ };
+}
+
+function extractPathParams(pattern, path) {
+ if (pattern.indexOf(':') === -1) {
+ return null;
+ } else {
+ const params = {};
+ for (let i = 0, j = 0; i < pattern.length; i++, j++) {
+ if (pattern[i] === ':') {
+ let key = [];
+ let val = [];
+ while (pattern[++i] !== '/' && i < pattern.length) {
+ key.push(pattern[i]);
+ }
+ while (path[j] !== '/' && j < path.length) {
+ val.push(path[j++]);
+ }
+ params[key.join('')] = decodeURIComponent(val.join(''));
+ } else {
+ if (pattern[i] !== path[j]) {
+ return null;
+ }
+ }
+ }
+ return params;
+ }
+}
diff --git a/backend/src/vendor/md5.js b/backend/src/vendor/md5.js
new file mode 100644
index 0000000000..9bbcd08170
--- /dev/null
+++ b/backend/src/vendor/md5.js
@@ -0,0 +1,387 @@
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+export function hex_md5(s) {
+ return rstr2hex(rstr_md5(str2rstr_utf8(s)));
+}
+
+export function b64_md5(s) {
+ return rstr2b64(rstr_md5(str2rstr_utf8(s)));
+}
+
+export function any_md5(s, e) {
+ return rstr2any(rstr_md5(str2rstr_utf8(s)), e);
+}
+
+export function hex_hmac_md5(k, d) {
+ return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
+}
+
+export function b64_hmac_md5(k, d) {
+ return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
+}
+
+export function any_hmac_md5(k, d, e) {
+ return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e);
+}
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function md5_vm_test() {
+ return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72';
+}
+
+/*
+ * Calculate the MD5 of a raw string
+ */
+function rstr_md5(s) {
+ return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+}
+
+/*
+ * Calculate the HMAC-MD5, of a key and some data (raw strings)
+ */
+function rstr_hmac_md5(key, data) {
+ var bkey = rstr2binl(key);
+ if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
+
+ var ipad = Array(16),
+ opad = Array(16);
+ for (var i = 0; i < 16; i++) {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5c5c5c5c;
+ }
+
+ var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+ return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
+}
+
+/*
+ * Convert a raw string to a hex string
+ */
+function rstr2hex(input) {
+ try {
+ hexcase;
+ } catch (e) {
+ hexcase = 0;
+ }
+ var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef';
+ var output = '';
+ var x;
+ for (var i = 0; i < input.length; i++) {
+ x = input.charCodeAt(i);
+ output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f);
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to a base-64 string
+ */
+function rstr2b64(input) {
+ try {
+ b64pad;
+ } catch (e) {
+ b64pad = '';
+ }
+ var tab =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ var output = '';
+ var len = input.length;
+ for (var i = 0; i < len; i += 3) {
+ var triplet =
+ (input.charCodeAt(i) << 16) |
+ (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) |
+ (i + 2 < len ? input.charCodeAt(i + 2) : 0);
+ for (var j = 0; j < 4; j++) {
+ if (i * 8 + j * 6 > input.length * 8) output += b64pad;
+ else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f);
+ }
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to an arbitrary string encoding
+ */
+function rstr2any(input, encoding) {
+ var divisor = encoding.length;
+ var i, j, q, x, quotient;
+
+ /* Convert to an array of 16-bit big-endian values, forming the dividend */
+ var dividend = Array(Math.ceil(input.length / 2));
+ for (i = 0; i < dividend.length; i++) {
+ dividend[i] =
+ (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
+ }
+
+ /*
+ * Repeatedly perform a long division. The binary array forms the dividend,
+ * the length of the encoding is the divisor. Once computed, the quotient
+ * forms the dividend for the next step. All remainders are stored for later
+ * use.
+ */
+ var full_length = Math.ceil(
+ (input.length * 8) / (Math.log(encoding.length) / Math.log(2)),
+ );
+ var remainders = Array(full_length);
+ for (j = 0; j < full_length; j++) {
+ quotient = Array();
+ x = 0;
+ for (i = 0; i < dividend.length; i++) {
+ x = (x << 16) + dividend[i];
+ q = Math.floor(x / divisor);
+ x -= q * divisor;
+ if (quotient.length > 0 || q > 0) quotient[quotient.length] = q;
+ }
+ remainders[j] = x;
+ dividend = quotient;
+ }
+
+ /* Convert the remainders to the output string */
+ var output = '';
+ for (i = remainders.length - 1; i >= 0; i--)
+ output += encoding.charAt(remainders[i]);
+
+ return output;
+}
+
+/*
+ * Encode a string as utf-8.
+ * For efficiency, this assumes the input is valid utf-16.
+ */
+function str2rstr_utf8(input) {
+ var output = '';
+ var i = -1;
+ var x, y;
+
+ while (++i < input.length) {
+ /* Decode utf-16 surrogate pairs */
+ x = input.charCodeAt(i);
+ y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
+ if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) {
+ x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff);
+ i++;
+ }
+
+ /* Encode output as utf-8 */
+ if (x <= 0x7f) output += String.fromCharCode(x);
+ else if (x <= 0x7ff)
+ output += String.fromCharCode(
+ 0xc0 | ((x >>> 6) & 0x1f),
+ 0x80 | (x & 0x3f),
+ );
+ else if (x <= 0xffff)
+ output += String.fromCharCode(
+ 0xe0 | ((x >>> 12) & 0x0f),
+ 0x80 | ((x >>> 6) & 0x3f),
+ 0x80 | (x & 0x3f),
+ );
+ else if (x <= 0x1fffff)
+ output += String.fromCharCode(
+ 0xf0 | ((x >>> 18) & 0x07),
+ 0x80 | ((x >>> 12) & 0x3f),
+ 0x80 | ((x >>> 6) & 0x3f),
+ 0x80 | (x & 0x3f),
+ );
+ }
+ return output;
+}
+
+/*
+ * Encode a string as utf-16
+ */
+function str2rstr_utf16le(input) {
+ var output = '';
+ for (var i = 0; i < input.length; i++)
+ output += String.fromCharCode(
+ input.charCodeAt(i) & 0xff,
+ (input.charCodeAt(i) >>> 8) & 0xff,
+ );
+ return output;
+}
+
+function str2rstr_utf16be(input) {
+ var output = '';
+ for (var i = 0; i < input.length; i++)
+ output += String.fromCharCode(
+ (input.charCodeAt(i) >>> 8) & 0xff,
+ input.charCodeAt(i) & 0xff,
+ );
+ return output;
+}
+
+/*
+ * Convert a raw string to an array of little-endian words
+ * Characters >255 have their high-byte silently ignored.
+ */
+function rstr2binl(input) {
+ var output = Array(input.length >> 2);
+ for (var i = 0; i < output.length; i++) output[i] = 0;
+ for (var i = 0; i < input.length * 8; i += 8)
+ output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
+ return output;
+}
+
+/*
+ * Convert an array of little-endian words to a string
+ */
+function binl2rstr(input) {
+ var output = '';
+ for (var i = 0; i < input.length * 32; i += 8)
+ output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
+ return output;
+}
+
+/*
+ * Calculate the MD5 of an array of little-endian words, and a bit length.
+ */
+function binl_md5(x, len) {
+ /* append padding */
+ x[len >> 5] |= 0x80 << len % 32;
+ x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+
+ for (var i = 0; i < x.length; i += 16) {
+ var olda = a;
+ var oldb = b;
+ var oldc = c;
+ var oldd = d;
+
+ a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
+ d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
+ c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
+ b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
+ a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
+ d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
+ c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
+ b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
+ a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
+ d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
+ c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
+ b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
+ a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
+ d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
+ c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
+ b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
+
+ a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
+ d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
+ c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
+ b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
+ a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
+ d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
+ c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
+ b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
+ a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
+ d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
+ c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
+ b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
+ a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
+ d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
+ c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
+ b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
+
+ a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
+ d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
+ c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
+ b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
+ a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
+ d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
+ c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
+ b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
+ a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
+ d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
+ c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
+ b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
+ a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
+ d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
+ c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
+ b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
+
+ a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
+ d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
+ c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
+ b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
+ a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
+ d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
+ c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
+ b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
+ a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
+ d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
+ c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
+ b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
+ a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
+ d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
+ c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
+ b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ }
+ return Array(a, b, c, d);
+}
+
+/*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+function md5_cmn(q, a, b, x, s, t) {
+ return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
+}
+
+function md5_ff(a, b, c, d, x, s, t) {
+ return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
+}
+
+function md5_gg(a, b, c, d, x, s, t) {
+ return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
+}
+
+function md5_hh(a, b, c, d, x, s, t) {
+ return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+}
+
+function md5_ii(a, b, c, d, x, s, t) {
+ return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y) {
+ var lsw = (x & 0xffff) + (y & 0xffff);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xffff);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function bit_rol(num, cnt) {
+ return (num << cnt) | (num >>> (32 - cnt));
+}
diff --git a/backend/src/vendor/open-api.js b/backend/src/vendor/open-api.js
new file mode 100644
index 0000000000..cc519ec39e
--- /dev/null
+++ b/backend/src/vendor/open-api.js
@@ -0,0 +1,660 @@
+/* eslint-disable no-undef */
+import { installConsoleLogCapture } from '@/utils/debug-logs';
+
+const isQX = typeof $task !== 'undefined';
+const isLoon = typeof $loon !== 'undefined';
+// 可能有一些兼容环境依赖于这个, 先不改成 $environment.surge-version
+const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
+const isNode = eval(`typeof process !== "undefined"`); // eval is needed in order to avoid browserify processing
+const isStash =
+ 'undefined' !== typeof $environment && $environment['stash-version'];
+const isShadowRocket = 'undefined' !== typeof $rocket;
+const isEgern = 'undefined' !== typeof Egern && Egern.version;
+const isLanceX = 'undefined' != typeof $native;
+const isGUIforCores = typeof $Plugins !== 'undefined';
+import { Base64 } from 'js-base64';
+
+function isPlainObject(obj) {
+ return (
+ obj !== null &&
+ typeof obj === 'object' &&
+ [null, Object.prototype].includes(Object.getPrototypeOf(obj))
+ );
+}
+
+function parseSocks5Uri(uri) {
+ // eslint-disable-next-line no-unused-vars
+ let [__, username, password, server, port, query, name] = uri.match(
+ /^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
+ );
+ if (port) {
+ port = parseInt(port, 10);
+ } else {
+ $.error(`port is not present in line: ${uri}`);
+ throw new Error(`port is not present in line: ${uri}`);
+ }
+ return {
+ type: 5,
+ host: server,
+ port,
+
+ userId: username != null ? decodeURIComponent(username) : undefined,
+ password: password != null ? decodeURIComponent(password) : undefined,
+ };
+}
+export class OpenAPI {
+ constructor(name = 'untitled', debug = false) {
+ this.name = name;
+ this.debug = debug;
+
+ this.http = HTTP();
+ this.env = ENV();
+
+ if (isNode) {
+ const dotenv = eval(`require("dotenv")`);
+ dotenv.config();
+ }
+ this.node = (() => {
+ if (isNode) {
+ const fs = eval("require('fs')");
+
+ return {
+ fs,
+ };
+ } else {
+ return null;
+ }
+ })();
+ this.initCache();
+ installConsoleLogCapture(this);
+
+ const delay = (t, v) =>
+ new Promise(function (resolve) {
+ setTimeout(resolve.bind(null, v), t);
+ });
+
+ Promise.prototype.delay = async function (t) {
+ const v = await this;
+ return await delay(t, v);
+ };
+ }
+
+ // persistence
+ // initialize cache
+ initCache() {
+ if (isQX)
+ this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
+ if (isLoon || isSurge)
+ this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
+ if (isGUIforCores)
+ this.cache = JSON.parse(
+ $Plugins.SubStoreCache.get(this.name) || '{}',
+ );
+ if (isNode) {
+ // create a json for root cache
+ const basePath =
+ eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
+ let rootPath = `${basePath}/root.json`;
+ const backupRootPath = `${basePath}/root_${Date.now()}.json`;
+
+ this.log(`Root path: ${rootPath}`);
+ if (this.node.fs.existsSync(rootPath)) {
+ try {
+ this.root = JSON.parse(
+ this.node.fs.readFileSync(`${rootPath}`),
+ );
+ } catch (e) {
+ this.node.fs.copyFileSync(rootPath, backupRootPath);
+ this.error(
+ `Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,
+ );
+ }
+ }
+ if (!isPlainObject(this.root)) {
+ this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
+ flag: 'w',
+ });
+ this.root = {};
+ }
+
+ // create a json file with the given name if not exists
+ let fpath = `${basePath}/${this.name}.json`;
+ const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
+
+ this.log(`Data path: ${fpath}`);
+ if (this.node.fs.existsSync(fpath)) {
+ try {
+ this.cache = JSON.parse(
+ this.node.fs.readFileSync(`${fpath}`, 'utf-8'),
+ );
+ if (!isPlainObject(this.cache))
+ throw new Error('Invalid Data');
+ } catch (e) {
+ try {
+ const str = Base64.decode(
+ this.node.fs.readFileSync(`${fpath}`, 'utf-8'),
+ );
+ this.cache = JSON.parse(str);
+ this.node.fs.writeFileSync(fpath, str, {
+ flag: 'w',
+ });
+ if (!isPlainObject(this.cache))
+ throw new Error('Invalid Data');
+ } catch (e) {
+ this.node.fs.copyFileSync(fpath, backupPath);
+ this.error(
+ `Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,
+ );
+ }
+ }
+ }
+ if (!isPlainObject(this.cache)) {
+ this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
+ flag: 'w',
+ });
+ this.cache = {};
+ }
+ }
+ }
+
+ // store cache
+ persistCache() {
+ const data = JSON.stringify(this.cache, null, 2);
+ if (isQX) $prefs.setValueForKey(data, this.name);
+ if (isLoon || isSurge) $persistentStore.write(data, this.name);
+ if (isGUIforCores) $Plugins.SubStoreCache.set(this.name, data);
+ if (isNode) {
+ const basePath =
+ eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
+
+ this.node.fs.writeFileSync(
+ `${basePath}/${this.name}.json`,
+ data,
+ { flag: 'w' },
+ (err) => console.log(err),
+ );
+ this.node.fs.writeFileSync(
+ `${basePath}/root.json`,
+ JSON.stringify(this.root, null, 2),
+ { flag: 'w' },
+ (err) => console.log(err),
+ );
+ }
+ }
+
+ write(data, key) {
+ this.log(`SET ${key}`);
+ if (key.indexOf('#') !== -1) {
+ key = key.substr(1);
+ if (isSurge || isLoon) {
+ return $persistentStore.write(data, key);
+ }
+ if (isQX) {
+ return $prefs.setValueForKey(data, key);
+ }
+ if (isNode) {
+ this.root[key] = data;
+ }
+ if (isGUIforCores) {
+ return $Plugins.SubStoreCache.set(key, data);
+ }
+ } else {
+ this.cache[key] = data;
+ }
+ this.persistCache();
+ }
+
+ read(key) {
+ this.log(`READ ${key}`);
+ if (key.indexOf('#') !== -1) {
+ key = key.substr(1);
+ if (isSurge || isLoon) {
+ return $persistentStore.read(key);
+ }
+ if (isQX) {
+ return $prefs.valueForKey(key);
+ }
+ if (isNode) {
+ return this.root[key];
+ }
+ if (isGUIforCores) {
+ return $Plugins.SubStoreCache.get(key);
+ }
+ } else {
+ return this.cache[key];
+ }
+ }
+
+ delete(key) {
+ this.log(`DELETE ${key}`);
+ if (key.indexOf('#') !== -1) {
+ key = key.substr(1);
+ if (isSurge || isLoon) {
+ return $persistentStore.write(null, key);
+ }
+ if (isQX) {
+ return $prefs.removeValueForKey(key);
+ }
+ if (isNode) {
+ delete this.root[key];
+ }
+ if (isGUIforCores) {
+ return $Plugins.SubStoreCache.remove(key);
+ }
+ } else {
+ delete this.cache[key];
+ }
+ this.persistCache();
+ }
+
+ // notification
+ notify(title, subtitle = '', content = '', options = {}) {
+ const openURL = options['open-url'];
+ const mediaURL = options['media-url'];
+
+ if (isQX) $notify(title, subtitle, content, options);
+ if (isSurge) {
+ $notification.post(
+ title,
+ subtitle,
+ content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`,
+ {
+ url: openURL,
+ },
+ );
+ }
+ if (isLoon) {
+ let opts = {};
+ if (openURL) opts['openUrl'] = openURL;
+ if (mediaURL) opts['mediaUrl'] = mediaURL;
+ if (JSON.stringify(opts) === '{}') {
+ $notification.post(title, subtitle, content);
+ } else {
+ $notification.post(title, subtitle, content, opts);
+ }
+ }
+ if (isNode) {
+ const content_ =
+ content +
+ (openURL ? `\n点击跳转: ${openURL}` : '') +
+ (mediaURL ? `\n多媒体: ${mediaURL}` : '');
+ console.log(`[Notify] ${title}\n${subtitle}\n${content_}\n\n`);
+
+ let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
+ if (push) {
+ if (/^https?:\/\//.test(push)) {
+ // 处理 HTTP/HTTPS URL
+ const url = push
+ .replace(
+ '[推送标题]',
+ encodeURIComponent(title || 'Sub-Store'),
+ )
+ .replace(
+ '[推送内容]',
+ encodeURIComponent(
+ [subtitle, content_].map((i) => i).join('\n'),
+ ),
+ );
+ const $http = HTTP();
+ $http
+ .get({ url })
+ .then((resp) => {
+ console.log(
+ `[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
+ );
+ })
+ .catch((e) => {
+ console.log(
+ `[Push Service] URL: ${url}\nERROR: ${e}`,
+ );
+ });
+ } else {
+ const { execFile } = eval(`require("child_process")`);
+ execFile(
+ 'shoutrrr',
+ [
+ 'send',
+ '--url',
+ push,
+ '--message',
+ `${title}\n${subtitle}\n${content_}`,
+ ],
+ (error, stdout, stderr) => {
+ if (error) {
+ console.log(
+ `[Push Service] URL: ${push}\nERROR: ${error}`,
+ );
+ return;
+ }
+ if (stderr) {
+ console.log(
+ `[Push Service] URL: ${push}\nstderr: ${stderr}`,
+ );
+ }
+ console.log(
+ `[Push Service] URL: ${push}\nstdout: ${stdout}`,
+ );
+ },
+ );
+ }
+ }
+ }
+ if (isGUIforCores) {
+ $Plugins.Notify(title, subtitle + '\n' + content);
+ }
+ }
+
+ // other helper functions
+ log(msg) {
+ if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
+ }
+
+ info(msg) {
+ console.log(`[${this.name}] INFO: ${msg}`);
+ }
+
+ warn(msg) {
+ console.log(`[${this.name}] WARN: ${msg}`);
+ }
+
+ error(msg) {
+ console.log(`[${this.name}] ERROR: ${msg}`);
+ }
+
+ wait(millisec) {
+ return new Promise((resolve) => setTimeout(resolve, millisec));
+ }
+
+ done(value = {}) {
+ if (isQX || isLoon || isSurge || isGUIforCores) {
+ $done(value);
+ } else if (isNode) {
+ if (typeof $context !== 'undefined') {
+ $context.headers = value.headers;
+ $context.statusCode = value.statusCode;
+ $context.body = value.body;
+ }
+ }
+ }
+}
+
+export function ENV() {
+ return {
+ isQX,
+ isLoon,
+ isSurge,
+ isNode,
+ isStash,
+ isShadowRocket,
+ isEgern,
+ isLanceX,
+ isGUIforCores,
+ };
+}
+
+export function HTTP(defaultOptions = { baseURL: '' }) {
+ const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV();
+ const methods = [
+ 'GET',
+ 'POST',
+ 'PUT',
+ 'DELETE',
+ 'HEAD',
+ 'OPTIONS',
+ 'PATCH',
+ ];
+ const URL_REGEX =
+ /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
+
+ function send(method, options) {
+ options = typeof options === 'string' ? { url: options } : options;
+ const baseURL = defaultOptions.baseURL;
+ if (baseURL && !URL_REGEX.test(options.url || '')) {
+ options.url = baseURL ? baseURL + options.url : options.url;
+ }
+ options = { ...defaultOptions, ...options };
+ const timeout = options.timeout;
+ const events = {
+ ...{
+ onRequest: () => {},
+ onResponse: (resp) => resp,
+ onTimeout: () => {},
+ },
+ ...options.events,
+ };
+
+ events.onRequest(method, options);
+
+ if (options.node) {
+ // Surge & Loon allow connecting to a server using a specified proxy node
+ if (isSurge) {
+ const build = $environment['surge-build'];
+ if (build && parseInt(build) >= 2407) {
+ options['policy-descriptor'] = options.node;
+ delete options.node;
+ }
+ }
+ }
+
+ let worker;
+ if (isQX) {
+ worker = $task.fetch({
+ method,
+ url: options.url,
+ headers: options.headers,
+ body: options.body,
+ opts: options.opts,
+ });
+ } else if (isLoon || isSurge || isNode) {
+ worker = new Promise(async (resolve, reject) => {
+ const body = options.body;
+ const opts = JSON.parse(JSON.stringify(options));
+ opts.body = body;
+ opts.timeout = opts.timeout || 8000;
+ if (opts.timeout) {
+ opts.timeout++;
+ if (isNaN(opts.timeout)) {
+ opts.timeout = 8000;
+ }
+ if (!isNode) {
+ let unit = 'ms';
+ // 这些客户端单位为 s
+ if (isSurge || isStash || isShadowRocket) {
+ opts.timeout = Math.ceil(opts.timeout / 1000);
+ unit = 's';
+ }
+ // Loon 为 ms
+ // console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
+ }
+ }
+ if (isNode) {
+ const undici = eval("require('undici')");
+ const { socksDispatcher } = eval("require('fetch-socks')");
+ const defaultMaxHeaderSize = 32 * 1024;
+ const parsedMaxHeaderSize = Number.parseInt(
+ eval('process.env.SUB_STORE_MAX_HEADER_SIZE'),
+ 10,
+ );
+ const maxHeaderSize =
+ Number.isInteger(parsedMaxHeaderSize) &&
+ parsedMaxHeaderSize > 0
+ ? parsedMaxHeaderSize
+ : defaultMaxHeaderSize;
+ const {
+ ProxyAgent,
+ EnvHttpProxyAgent,
+ request,
+ interceptors,
+ } = undici;
+ const agentOpts = {
+ connect: {
+ rejectUnauthorized:
+ opts.strictSSL === false ||
+ opts.insecure === true ||
+ opts.rejectUnauthorized === false
+ ? false
+ : true,
+ },
+ bodyTimeout: opts.timeout,
+ headersTimeout: opts.timeout,
+ maxHeaderSize,
+ };
+ const tlsOptions = {
+ rejectUnauthorized:
+ agentOpts.connect.rejectUnauthorized,
+ };
+ opts.tls = {
+ ...(opts.tls || {}),
+ ...tlsOptions,
+ };
+ try {
+ const url = new URL(opts.url);
+ if (url.username || url.password) {
+ opts.headers = {
+ ...(opts.headers || {}),
+ Authorization: `Basic ${Buffer.from(
+ `${url.username || ''}:${
+ url.password || ''
+ }`,
+ ).toString('base64')}`,
+ };
+ }
+ let dispatcher;
+ if (!opts.proxy) {
+ const allProxy =
+ eval('process.env.all_proxy') ||
+ eval('process.env.ALL_PROXY');
+ if (allProxy && /^socks5:\/\//.test(allProxy)) {
+ opts.proxy = allProxy;
+ }
+ }
+ if (opts.proxy) {
+ if (/^socks5:\/\//.test(opts.proxy)) {
+ dispatcher = socksDispatcher(
+ parseSocks5Uri(opts.proxy),
+ {
+ ...agentOpts,
+ requestTls: tlsOptions,
+ },
+ );
+ } else {
+ dispatcher = new ProxyAgent({
+ ...agentOpts,
+ uri: opts.proxy,
+ requestTls: tlsOptions,
+ });
+ }
+ } else {
+ dispatcher = new EnvHttpProxyAgent({
+ ...agentOpts,
+ requestTls: tlsOptions,
+ });
+ }
+ const response = await request(opts.url, {
+ ...opts,
+ method: method.toUpperCase(),
+ dispatcher: dispatcher.compose(
+ interceptors.redirect({
+ maxRedirections: 3,
+ throwOnMaxRedirects: true,
+ }),
+ ),
+ });
+ resolve({
+ statusCode: response.statusCode,
+ headers: response.headers,
+ body:
+ opts.encoding === null
+ ? await response.body.arrayBuffer()
+ : await response.body.text(),
+ });
+ } catch (e) {
+ reject(e);
+ }
+ } else {
+ $httpClient[method.toLowerCase()](
+ opts,
+ (err, response, body) => {
+ // if (err) {
+ // console.log(err);
+ // } else {
+ // console.log({
+ // statusCode:
+ // response.status || response.statusCode,
+ // headers: response.headers,
+ // body,
+ // });
+ // }
+
+ if (err) reject(err);
+ else
+ resolve({
+ statusCode:
+ response.status || response.statusCode,
+ headers: response.headers,
+ body,
+ });
+ },
+ );
+ }
+ });
+ } else if (isGUIforCores) {
+ worker = new Promise(async (resolve, reject) => {
+ try {
+ const response = await $Plugins.Requests({
+ method,
+ url: options.url,
+ headers: options.headers,
+ body: options.body,
+ autoTransformBody: false,
+ options: {
+ Proxy: options.proxy,
+ Timeout: options.timeout
+ ? options.timeout / 1000
+ : 15,
+ },
+ });
+ resolve({
+ statusCode: response.status,
+ headers: response.headers,
+ body: response.body,
+ });
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ let timeoutid;
+
+ const timer = timeout
+ ? new Promise((_, reject) => {
+ // console.log(`[request timeout] ${timeout}ms`);
+ timeoutid = setTimeout(() => {
+ events.onTimeout();
+ return reject(
+ `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,
+ );
+ }, timeout);
+ })
+ : null;
+
+ return (
+ timer
+ ? Promise.race([timer, worker]).then((res) => {
+ if (typeof clearTimeout !== 'undefined') {
+ clearTimeout(timeoutid);
+ }
+ return res;
+ })
+ : worker
+ ).then((resp) => events.onResponse(resp));
+ }
+
+ const http = {};
+ methods.forEach(
+ (method) =>
+ (http[method.toLowerCase()] = (options) => send(method, options)),
+ );
+ return http;
+}
diff --git a/backend/sub-store.js b/backend/sub-store.js
deleted file mode 100644
index cbd9691b97..0000000000
--- a/backend/sub-store.js
+++ /dev/null
@@ -1,2420 +0,0 @@
-/**
- * Sub-Store v0.1 (Backend only)
- * @Author: Peng-YM
- * @Description:
- * 适用于QX,Loon,Surge的订阅管理工具。
- * - 功能
- * 1. 订阅转换,支持SS, SSR, V2RayN, QX, Loon, Surge格式的互相转换。
- * 2. 节点过滤,重命名,排序等。
- * 3. 订阅拆分,组合。
- */
-
-const $ = API("sub-store");
-
-// Constants
-const SUBS_KEY = "subs";
-const COLLECTIONS_KEY = "collections";
-
-// SOME INITIALIZATIONS
-if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
-if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
-
-// BACKEND API
-const $app = express();
-
-// subscriptions
-$app.get("/download/:name", downloadSub)
-
-$app.route("/sub/:name")
- .get(getSub)
- .patch(updateSub)
- .delete(deleteSub);
-
-$app.route("/sub")
- .get(getAllSubs)
- .post(newSub)
- .delete(deleteAllSubs);
-
-// collections
-$app.get("/download/collection/:name", downloadCollection);
-$app.route("/collection/:name")
- .get(getCollection)
- .patch(updateCollection)
- .delete(deleteCollection);
-$app.route("/collection")
- .get(getAllCollections)
- .post(newCollection)
- .delete(deleteAllCollections);
-
-$app.all("/", (req, res) => {
- res.send("Hello from Sub-Store! Made with ❤️ by Peng-YM.")
-});
-
-$app.start();
-
-// SOME CONSTANTS
-const FALL_BACK_TARGET = "Raw";
-const DEFAULT_SUPPORTED_PLATFORMS = {
- QX: true,
- Loon: true,
- Surge: true,
- Raw: true
-}
-const AVAILABLE_FILTERS = {
- "Keyword Filter": KeywordFilter,
- "Discard Keyword Filter": DiscardKeywordFilter,
- "Useless Filter": UselessFilter,
- "Region Filter": RegionFilter,
- "Regex Filter": RegexFilter,
- "Discard Regex Filter": DiscardRegexFilter,
- "Type Filter": TypeFilter,
- "Script Filter": ScriptFilter
-}
-
-const AVAILABLE_OPERATORS = {
- "Set Property Operator": SetPropertyOperator,
- "Flag Operator": FlagOperator,
- "Sort Operator": SortOperator,
- "Keyword Sort Operator": KeywordSortOperator,
- "Keyword Rename Operator": KeywordRenameOperator,
- "Keyword Delete Operator": KeywordDeleteOperator,
- "Regex Rename Operator": RegexRenameOperator,
- "Regex Delete Operator": RegexDeleteOperator,
- "Script Operator": ScriptOperator
-}
-
-/**************************** API -- Subscriptions ***************************************/
-// download subscription, for APP only
-async function downloadSub(req, res) {
- const {name} = req.params;
- const platform = getPlatformFromHeaders(req.headers);
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[name]) {
- const sub = allSubs[name];
- try {
- const output = await parseSub(sub, platform);
- res.send(output);
- } catch (err) {
- $.notify('[Sub-Store]', '❌ 无法获取订阅!', `错误信息:${err}`)
- res.status(500).json({
- status: "failed",
- message: err
- });
- }
- } else {
- res.status(404).json({
- status: "failed",
- message: `订阅${name}不存在!`
- });
- }
-}
-
-async function parseSub(sub, platform) {
- // download from url
- const raw = await $.http.get(sub.url).then(resp => resp.body).catch(err => {
- throw new Error(err);
- });
- console.log("=======================================================================");
- console.log(`Processing subscription: ${sub.name}, target platform ==> ${platform}.`);
- const $parser = ProxyParser(platform);
- let proxies = $parser.parse(raw);
-
- // filters
- const $filter = ProxyFilter();
- // operators
- const $operator = ProxyOperator();
-
- for (const item of sub.process || []) {
- if (item.type.indexOf("Script") !== -1) {
- if (item.args && item.args[0].indexOf("http") !== -1) {
- // if this is remote script
- item.args[0] = await $.http.get(item.args[0]).then(resp => resp.body).catch(err => {
- throw new Error(`Error when downloading remote script: ${item.args[0]}.\n Reason: ${err}`);
- });
- }
- }
- if (item.type.indexOf("Filter") !== -1) {
- const filter = AVAILABLE_FILTERS[item.type];
- if (filter) {
- $filter.addFilters(filter(...(item.args || [])));
- proxies = $filter.process(proxies);
- console.log(`Applying filter "${item.type}" with arguments:\n >>> ${item.args || "None"}`);
- }
- } else if (item.type.indexOf("Operator") !== -1) {
- const operator = AVAILABLE_OPERATORS[item.type];
- if (operator) {
- $operator.addOperators(operator(...(item.args || [])));
- proxies = $operator.process(proxies);
- console.log(`Applying operator "${item.type}" with arguments: \n >>> ${item.args || "None"}`);
- }
- }
- }
- return $parser.produce(proxies);
-}
-
-// Subscriptions
-async function getSub(req, res) {
- const {name} = req.params;
- const sub = $.read(SUBS_KEY)[name];
- if (sub) {
- res.json({
- status: "success",
- data: sub
- });
- } else {
- res.status(404).json({
- status: "failed",
- message: `未找到订阅:${name}!`
- });
- }
-}
-
-async function newSub(req, res) {
- const sub = req.body;
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[sub.name]) {
- res.status(500).json({
- status: "failed",
- message: `订阅${sub.name}已存在!`
- });
- }
- // validate name
- if (/^[\w-_]*$/.test(sub.name)) {
- allSubs[sub.name] = sub;
- $.write(allSubs, SUBS_KEY);
- res.status(201).json({
- status: "success",
- data: sub
- });
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
- })
- }
-}
-
-async function updateSub(req, res) {
- const {name} = req.params;
- let sub = req.body;
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[name]) {
- const newSub = {
- ...allSubs[name],
- ...sub
- };
- allSubs[name] = newSub;
- $.write(allSubs, SUBS_KEY);
- res.json({
- status: "success",
- data: newSub
- })
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅${name}不存在,无法更新!`
- });
- }
-}
-
-async function deleteSub(req, res) {
- const {name} = req.params;
- let allSubs = $.read(SUBS_KEY);
- delete allSubs[name];
- $.write(allSubs, SUBS_KEY);
- res.json({
- status: "success"
- });
-}
-
-async function getAllSubs(req, res) {
- const allSubs = $.read(SUBS_KEY);
- res.json({
- status: "success",
- data: Object.keys(allSubs)
- });
-}
-
-async function deleteAllSubs(req, res) {
- $.write({}, SUBS_KEY);
- res.json({
- status: "success"
- });
-}
-
-// Collections
-async function downloadCollection(req, res) {
- const {name} = req.params;
- const collection = $.read(COLLECTIONS_KEY)[name];
- const platform = getPlatformFromHeaders(req.headers);
- if (collection) {
- const subs = collection.subscriptions || [];
- const output = await Promise.all(subs.map(async id => {
- const sub = $.read(SUBS_KEY)[id];
- try {
- return parseSub(sub, platform);
- } catch (err) {
- console.log(`ERROR when process subscription: ${id}`);
- return "";
- }
- }));
- res.send(output.join("\n"));
- } else {
- $.notify('[Sub-Store]', `❌ 未找到订阅集:${name}!`)
- res.status(404).json({
- status: "failed",
- message: `❌ 未找到订阅集:${name}!`
- });
- }
-}
-
-async function getCollection(req, res) {
- const {name} = req.params;
- const collection = $.read(COLLECTIONS_KEY)[name];
- if (collection) {
- res.json({
- status: "success",
- data: collection
- });
- } else {
- res.status(404).json({
- status: "failed",
- message: `未找到订阅集:${name}!`
- });
- }
-}
-
-async function newCollection(req, res) {
- const collection = req.body;
- const allCol = $.read(COLLECTIONS_KEY);
- if (allCol[collection.name]) {
- res.status(500).json({
- status: "failed",
- message: `订阅集${collection.name}已存在!`
- });
- }
- // validate name
- if (/^[\w-_]*$/.test(collection.name)) {
- allCol[collection.name] = collection;
- $.write(allCol, COLLECTIONS_KEY);
- res.status(201).json({
- status: "success",
- data: collection
- });
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
- })
- }
-}
-
-async function updateCollection(req, res) {
- const {name} = req.params;
- let collection = req.body;
- const allCol = $.read(COLLECTIONS_KEY);
- if (allCol[name]) {
- const newCol = {
- ...allCol[name],
- ...collection
- };
- allCol[name] = newCol;
- $.write(allCol, COLLECTIONS_KEY);
- res.json({
- status: "success",
- data: newCol
- })
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅集${name}不存在,无法更新!`
- });
- }
-}
-
-async function deleteCollection(req, res) {
- const {name} = req.params;
- let allCol = $.read(COLLECTIONS_KEY);
- delete allCol[name];
- $.write(allCol, COLLECTIONS_KEY);
- res.json({
- status: "success"
- });
-}
-
-async function getAllCollections(req, res) {
- const allCols = $.read(COLLECTIONS_KEY);
- res.json({
- status: "success",
- data: Object.keys(allCols)
- });
-}
-
-async function deleteAllCollections(req, res) {
- $.write({}, COLLECTIONS_KEY);
- res.json({
- status: "success"
- });
-}
-
-/**************************** Proxy Handlers ***************************************/
-function ProxyParser(targetPlatform) {
- // parser collections
- const parsers = [];
- const producers = [];
-
- function addParsers(...args) {
- args.forEach(a => parsers.push(a()));
- }
-
- function addProducers(...args) {
- args.forEach(a => producers.push(a()))
- }
-
- function parse(raw) {
- raw = preprocessing(raw);
- const lines = raw.split("\n");
- const result = [];
- // convert to json format
- for (let line of lines) {
- line = line.trim();
- if (line.length === 0) continue; // skip empty line
- if (line.startsWith("#")) continue; // skip comments
- let matched = false;
- for (const p of parsers) {
- const {patternTest, func} = p;
-
- // some lines with weird format may produce errors!
- let patternMatched;
- try {
- patternMatched = patternTest(line);
- } catch (err) {
- patternMatched = false;
- }
-
- if (patternMatched) {
- matched = true;
- // run parser safely.
- try {
- const proxy = func(line);
- if (!proxy) {
- // failed to parse this line
- console.log(`ERROR: parser return nothing for \n${line}\n`);
- break;
- }
- // skip unsupported proxies
- // if proxy.supported is undefined, assume that all platforms are supported.
- if (typeof proxy.supported === 'undefined' || proxy.supported[targetPlatform]) {
- delete proxy.supported;
- result.push(proxy);
- break;
- }
- } catch (err) {
- console.log(`ERROR: Failed to parse line: \n ${line}\n Reason: ${err}`);
- }
- }
- }
- if (!matched) {
- console.log(`ERROR: Failed to find a rule to parse line: \n${line}\n`);
- }
- }
- if (result.length === 0) {
- throw new Error(`ERROR: Input does not contains any valid node for platform ${targetPlatform}`)
- }
- return result;
- }
-
- function produce(proxies) {
- for (const p of producers) {
- if (p.targetPlatform === targetPlatform) {
- return proxies.map(proxy => {
- try {
- return p.output(proxy)
- } catch (err) {
- console.log(`ERROR: cannot produce proxy: ${JSON.stringify(proxy)}\nReason: ${err}`);
- return "";
- }
- }).join("\n");
- }
- }
- throw new Error(`Cannot find any producer for target platform: ${targetPlatform}`);
- }
-
- // preprocess raw input
- function preprocessing(raw) {
- let output;
- if (raw.indexOf("DOCTYPE html") !== -1) {
- // HTML format, maybe a wrong URL!
- throw new Error("Invalid format HTML!");
- }
- // check if content is based64 encoded
- const Base64 = new Base64Code();
- const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v"];
- if (keys.some(k => raw.indexOf(k) !== -1)) {
- output = Base64.safeDecode(raw);
- } else {
- output = raw;
- }
- output = output.split("\n");
- for (let i = 0; i < output.length; i++) {
- output[i] = output[i].trim(); // trim lines
- }
- return output.join("\n");
- }
-
- // Parsers
- addParsers(
- // URI format parsers
- URI_SS, URI_SSR, URI_VMess, URI_Trojan,
- // Quantumult X platform
- QX_SS, QX_SSR, QX_VMess, QX_Trojan, QX_Http,
- // Loon platform
- Loon_SS, Loon_SSR, Loon_VMess, Loon_Trojan, Loon_Http,
- // Surge platform
- Surge_SS, Surge_VMess, Surge_Trojan, Surge_Http
- );
-
- // Producers
- addProducers(
- QX_Producer, Loon_Producer, Surge_Producer, Raw_Producer
- );
-
- return {
- parse, produce
- };
-}
-
-function ProxyFilter() {
- const filters = [];
-
- function addFilters(...args) {
- args.forEach(a => filters.push(a));
- }
-
- // select proxies
- function process(proxies) {
- let selected = FULL(proxies.length, true);
- for (const filter of filters) {
- try {
- selected = AND(selected, filter.func(proxies));
- } catch (err) {
- console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
- }
- }
- return proxies.filter((_, i) => selected[i])
- }
-
- return {
- process, addFilters
- }
-}
-
-function ProxyOperator() {
- const operators = [];
-
- function addOperators(...args) {
- args.forEach(a => operators.push(a));
- }
-
- // run all operators
- function process(proxies) {
- let output = clone(proxies);
- for (const op of operators) {
- try {
- const output_ = op.func(output);
- if (output_) output = output_;
- } catch (err) {
- // print log and skip this operator
- console.log(`ERROR: cannot apply operator ${op.name}! Reason: ${err}`);
- }
- }
- return output;
- }
-
- return {addOperators, process}
-}
-
-/**************************** URI Format ***************************************/
-// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
-// reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
-function URI_SS() {
- const patternTest = (line) => {
- return /^ss:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- // parse url
- let content = line.split("ss://")[1];
-
- const proxy = {
- name: decodeURIComponent(line.split("#")[1]),
- type: "ss",
- supported
- }
- content = content.split("#")[0]; // strip proxy name
-
- // handle IPV4 and IPV6
- const serverAndPort = content.match(/@([^\/]*)\//)[1];
- const portIdx = serverAndPort.lastIndexOf(":");
- proxy.server = serverAndPort.substring(0, portIdx);
- proxy.port = serverAndPort.substring(portIdx + 1);
-
- const userInfo = Base64.safeDecode(content.split("@")[0]).split(":");
- proxy.cipher = userInfo[0];
- proxy.password = userInfo[1];
-
- // handle obfs
- const idx = content.indexOf("?plugin=");
- if (idx !== -1) {
- const pluginInfo = ("plugin=" + decodeURIComponent(content.split("?plugin=")[1])).split(";");
- const params = {};
- for (const item of pluginInfo) {
- const [key, val] = item.split("=");
- if (key) params[key] = val || true; // some options like "tls" will not have value
- }
- switch (params.plugin) {
- case 'simple-obfs':
- proxy.plugin = 'obfs'
- proxy['plugin-opts'] = {
- mode: params.obfs,
- host: params['obfs-host']
- }
- break
- case 'v2ray-plugin':
- proxy.supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Loon: false,
- Surge: false
- }
- proxy.obfs = 'v2ray-plugin'
- proxy['plugin-opts'] = {
- mode: "websocket",
- host: params['obfs-host'],
- path: params.path || ""
- }
- break
- default:
- throw new Error(`Unsupported plugin option: ${params.plugin}`)
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-// Parse URI SSR format, such as ssr://xxx
-function URI_SSR() {
- const patternTest = (line) => {
- return /^ssr:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Surge: false
- }
-
- const func = (line) => {
- line = Base64.safeDecode(line.split("ssr://")[1]);
-
- // handle IPV6 & IPV4 format
- let splitIdx = line.indexOf(':origin');
- if (splitIdx === -1) {
- splitIdx = line.indexOf(":auth_");
- }
- const serverAndPort = line.substring(0, splitIdx);
- const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
- const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1);
-
- let params = line.substring(splitIdx + 1).split("/?")[0].split(":");
- let proxy = {
- type: "ssr",
- server,
- port,
- protocol: params[0],
- cipher: params[1],
- obfs: params[2],
- password: Base64.safeDecode(params[3]),
- supported
- }
- // get other params
- params = {};
- line = line.split("/?")[1].split("&");
- if (line.length > 1) {
- for (const item of line) {
- const [key, val] = item.split("=");
- params[key] = val;
- }
- }
- proxy = {
- ...proxy,
- name: Base64.safeDecode(params.remarks),
- "protocol-param": Base64.safeDecode(params.protoparam).replace(/\s/g, "") || "",
- "obfs-param": Base64.safeDecode(params.obfsparam).replace(/\s/g, "") || ""
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-// V2rayN URI VMess format
-// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
-function URI_VMess() {
- const patternTest = (line) => {
- return /^vmess:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- line = line.split("vmess://")[1];
- const params = JSON.parse(Base64.safeDecode(line));
- const proxy = {
- name: params.ps,
- type: "vmess",
- server: params.add,
- port: params.port,
- cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default.
- uuid: params.id,
- alterId: params.aid || 0,
- tls: JSON.parse(params.tls || "false"),
- supported
- }
- // handle obfs
- if (params.net === 'ws') {
- proxy.network = 'ws';
- proxy['ws-path'] = params.path;
- proxy['ws-headers'] = {
- Host: params.host || params.add
- }
- }
- return proxy
- }
- return {patternTest, func};
-}
-
-// Trojan URI format
-function URI_Trojan() {
- const patternTest = (line) => {
- return /^trojan:\/\//.test(line);
- }
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- // trojan forces to use 443 port
- if (line.indexOf(":443") === -1) {
- throw new Error("Trojan port should always be 443!");
- }
- line = line.split("trojan://")[1];
- const server = line.split("@")[1].split(":443")[0];
-
- return {
- name: `[Trojan] ${server}`, // trojan uri has no server tag!
- type: "trojan",
- server,
- port: 443,
- password: line.split("@")[0],
- supported
- }
- }
- return {patternTest, func};
-}
-
-/**************************** Quantumult X ***************************************/
-function QX_SS() {
- const patternTest = (line) => {
- return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") === -1;
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- name: params.tag,
- type: "ss",
- server: params.server,
- port: params.port,
- cipher: params.method,
- password: params.password,
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- supported: clone(DEFAULT_SUPPORTED_PLATFORMS)
- };
- // handle obfs options
- if (params.obfs) {
- proxy["plugin-opts"] = {
- host: params['obfs-host'] || proxy.server
- };
- switch (params.obfs) {
- case "http":
- case "tls":
- proxy.plugin = "obfs";
- proxy["plugin-opts"].mode = params.obfs;
- break;
- case "ws":
- case "wss":
- proxy["plugin-opts"] = {
- ...proxy["plugin-opts"],
- mode: "websocket",
- path: params['obfs-uri'],
- tls: params.obfs === 'wss'
- }
- proxy.plugin = "v2ray-plugin"
- // Surge and Loon lack support for v2ray-plugin obfs
- proxy.supported.Surge = false
- proxy.supported.Loon = false
- break;
- }
- }
- return proxy;
- };
- return {patternTest, func};
-}
-
-function QX_SSR() {
- const patternTest = (line) => {
- return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") !== -1;
- };
- const supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Surge: false
- }
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- name: params.tag,
- type: "ssr",
- server: params.server,
- port: params.port,
- cipher: params.method,
- password: params.password,
- protocol: params["ssr-protocol"],
- obfs: "plain", // default obfs
- "protocol-param": params['ssr-protocol-param'],
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- supported
- }
- // handle obfs options
- if (params.obfs) {
- proxy.obfs = params.obfs;
- proxy['obfs-param'] = params['obfs-host']
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function QX_VMess() {
- const patternTest = (line) => {
- return /^vmess\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line)
- const proxy = {
- type: "vmess",
- name: params.tag,
- server: params.server,
- port: params.port,
- cipher: params.method || 'none',
- uuid: params.password,
- alterId: 0,
- tls: params.obfs === 'over-tls' || params.obfs === 'wss',
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- if (proxy.tls) {
- proxy.sni = params['obfs-host'] || params.server;
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- }
- // handle ws headers
- if (params.obfs === 'ws' || params.obfs === 'wss') {
- proxy.network = 'ws';
- proxy['ws-path'] = params['obfs-uri'];
- proxy['ws-headers'] = {
- Host: params['obfs-host'] || params.server // if no host provided, use the same as server
- }
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-function QX_Trojan() {
- const patternTest = (line) => {
- return /^trojan\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- type: "trojan",
- name: params.tag,
- server: params.server,
- port: params.port,
- password: params.password,
- sni: params['tls-host'] || params.server,
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- return proxy;
- }
- return {patternTest, func}
-}
-
-function QX_Http() {
- const patternTest = (line) => {
- return /^http\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- type: "http",
- name: params.tag,
- server: params.server,
- port: params.port,
- username: params.username,
- password: params.password,
- tls: JSON.parse(params['over-tls'] || "false"),
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- if (proxy.tls) {
- proxy.sni = params['tls-host'] || proxy.server;
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-function getQXParams(line) {
- const groups = line.split(",");
- const params = {};
- const protocols = ["shadowsocks", "vmess", "http", "trojan"];
- groups.forEach((g) => {
- const [key, value] = g.split("=");
- if (protocols.indexOf(key) !== -1) {
- params.type = key;
- const conf = value.split(":");
- params.server = conf[0];
- params.port = conf[1];
- } else {
- params[key.trim()] = value.trim();
- }
- });
- return params;
-}
-
-/**************************** Loon ***************************************/
-function Loon_SS() {
- const patternTest = (line) => {
- return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocks';
- }
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "ss",
- server: params[1],
- port: params[2],
- cipher: params[3],
- password: params[4].replace(/"/g, "")
- }
- // handle obfs
- if (params.length > 5) {
- proxy.plugin = 'obfs';
- proxy['plugin-opts'] = {
- mode: proxy.obfs,
- host: params[6]
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Loon_SSR() {
- const patternTest = (line) => {
- return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocksr';
- }
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- supported.Surge = false;
- return {
- name: line.split("=")[0].trim(),
- type: "ssr",
- server: params[1],
- port: params[2],
- cipher: params[3],
- password: params[4].replace(/"/g, ""),
- protocol: params[5],
- "protocol-param": params[6].match(/{(.*)}/)[1],
- supported,
- obfs: params[7],
- 'obfs-param': params[8].match(/{(.*)}/)[1]
- }
- }
- return {patternTest, func};
-}
-
-function Loon_VMess() {
- const patternTest = (line) => {
- // distinguish between surge vmess
- return /^.*=\s*vmess/i.test(line.split(",")[0]) && line.indexOf("username") === -1;
- }
- const func = (line) => {
- let params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "vmess",
- server: params[1],
- port: params[2],
- cipher: params[3] || 'none',
- uuid: params[4].replace(/"/g, ""),
- alterId: 0,
- }
- // get transport options
- params = params.splice(5);
- for (const item of params) {
- const [key, val] = item.split(":");
- params[key] = val;
- }
- proxy.tls = JSON.parse(params['over-tls'] || 'false');
- if (proxy.tls) {
- proxy.sni = params['tls-name'] || proxy.server;
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
- }
- switch (params.transport) {
- case "tcp":
- break;
- case "ws":
- proxy.network = params.transport
- proxy['ws-path'] = params.path
- proxy['ws-headers'] = {
- Host: params.host
- }
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false')
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Loon_Trojan() {
- const patternTest = (line) => {
- return /^.*=\s*trojan/i.test(line.split(",")[0]) && line.indexOf("password") === -1;
- }
-
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "trojan",
- server: params[1],
- port: params[2],
- password: params[3].replace(/"/g, ""),
- sni: params[1], // default sni is the server itself
- scert: JSON.parse(params['skip-cert-verify'] || 'false')
- }
- // trojan sni
- if (params.length > 4) {
- const [key, val] = params[4].split(":");
- if (key === 'tls-name') proxy.sni = val;
- else throw new Error(`ERROR: unknown option ${key} for line: \n${line}`);
- }
- return proxy;
- }
-
- return {patternTest, func}
-}
-
-function Loon_Http() {
- const patternTest = (line) => {
- return /^.*=\s*http/i.test(line.split(",")[0])
- && line.split(",").length === 5
- && line.indexOf("username") === -1
- && line.indexOf("password") === -1
- }
-
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "http",
- server: params[1],
- port: params[2],
- tls: params[2] === "443", // port 443 is considered as https type
- username: (params[3] || "").replace(/"/g, ""),
- password: (params[4] || "").replace(/"/g, "")
- }
- if (proxy.tls) {
- proxy.sni = params['tls-name'] || proxy.server;
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
- }
-
- return proxy;
- }
- return {patternTest, func}
-}
-
-/**************************** Surge ***************************************/
-function Surge_SS() {
- const patternTest = (line) => {
- return /^.*=\s*ss/.test(line.split(",")[0]);
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "ss",
- server: params.server,
- port: params.port,
- cipher: params['encrypt-method'],
- password: params.password,
- tfo: JSON.parse(params.tfo || "false"),
- udp: JSON.parse(params['udp-relay'] || "false"),
- }
- // handle obfs
- if (params.obfs) {
- proxy.plugin = 'obfs';
- proxy['plugin-opts'] = {
- mode: params.obfs,
- host: params['obfs-host']
- }
- }
- return proxy;
- }
- return {patternTest, func}
-}
-
-function Surge_VMess() {
- const patternTest = (line) => {
- return /^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1;
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "vmess",
- server: params.server,
- port: params.port,
- uuid: params.username,
- alterId: 0, // surge does not have this field
- cipher: "none", // surge does not have this field
- tls: JSON.parse(params.tls || "false"),
- tfo: JSON.parse(params.tfo || "false"),
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
- proxy.sni = params['sni'] || params.server;
- }
- // use websocket
- if (JSON.parse(params.ws || "false")) {
- proxy.network = 'ws';
- proxy['ws-path'] = params['ws-path'];
- proxy['ws-headers'] = {
- Host: params.sni
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Surge_Trojan() {
- const patternTest = (line) => {
- return /^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1;
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- return {
- name: params.name,
- type: "trojan",
- server: params.server,
- port: params.port,
- password: params.password,
- sni: params.sni || params.server,
- tfo: JSON.parse(params.tfo || "false"),
- scert: JSON.parse(params['skip-cert-verify'] || "false"),
- }
- }
-
- return {patternTest, func};
-}
-
-function Surge_Http() {
- const patternTest = (line) => {
- return /^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().patternTest(line)
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "http",
- server: params.server,
- port: params.port,
- tls: JSON.parse(params.tls || "false"),
- tfo: JSON.parse(params.tfo || "false"),
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
- proxy.sni = params.sni || params.server;
- }
- if (params.username !== 'none') proxy.username = params.username;
- if (params.password !== 'none') proxy.password = params.password;
- return proxy;
- }
- return {patternTest, func}
-}
-
-function getSurgeParams(line) {
- const params = {};
- params.name = line.split("=")[0].trim();
- const segments = line.split(",");
- params.server = segments[1].trim();
- params.port = segments[2].trim();
- for (let i = 3; i < segments.length; i++) {
- const item = segments[i]
- if (item.indexOf("=") !== -1) {
- const [key, value] = item.split("=");
- params[key.trim()] = value.trim();
- }
- }
- return params;
-}
-
-/**************************** Output Functions ***************************************/
-function QX_Producer() {
- const targetPlatform = "QX";
- const output = (proxy) => {
- let obfs_opts;
- let tls_opts;
- switch (proxy.type) {
- case 'ss':
- obfs_opts = "";
- if (proxy.plugin === 'obfs') {
- obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`;
- }
- if (proxy.plugin === 'v2ray-plugin') {
- const {tls, host, path} = proxy['plugin-opts'];
- obfs_opts = `,obfs=${tls ? 'wss' : 'ws'},obfs-host=${host}${path ? ',obfs-uri=' + path : ""}`;
- }
- return `shadowsocks = ${proxy.server}:${proxy.port}, method=${proxy.cipher}, password=${proxy.password}${obfs_opts}${proxy.tfo ? ", fast-open=true" : ", fast-open=false"}${proxy.udp ? ", udp-relay=true" : ", udp-relay=false"}, tag=${proxy.name}`
- case 'ssr':
- return `shadowsocks=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.password},ssr-protocol=${proxy.protocol}${proxy['protocol-param'] ? ",ssr-protocol-param=" + proxy['protocol-param'] : ""}${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${proxy['obfs-param'] ? ",obfs-host=" + proxy['obfs-param'] : ""}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'vmess':
- obfs_opts = "";
- if (proxy.network === 'ws') {
- // websocket
- if (proxy.tls) {
- // ws-tls
- obfs_opts = `,obfs=wss,obfs-host=${proxy.sni}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""},tls-verification=${proxy.scert ? "false" : "true"}`;
- } else {
- // ws
- obfs_opts = `,obfs=ws,obfs-host=${proxy['ws-headers'].Host}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""}`;
- }
- } else {
- // tcp
- if (proxy.tls) {
- obfs_opts = `,obfs=over-tls,obfs-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}`;
- }
- }
- return `vmess=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.uuid}${obfs_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'trojan':
- return `trojan=${proxy.server}:${proxy.port},password=${proxy.password},tls-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'http':
- tls_opts = "";
- if (proxy.tls) {
- tls_opts = `,over-tls=true,tls-verification=${proxy.scert ? "false" : "true"},tls-host=${proxy.sni}`;
- }
- return `http=${proxy.server}:${proxy.port},username=${proxy.username},password=${proxy.password}${tls_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"},tag=${proxy.name}`;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output};
-}
-
-function Loon_Producer() {
- const targetPlatform = "Loon";
- const output = (proxy) => {
- let obfs_opts, tls_opts;
- switch (proxy.type) {
- case "ss":
- obfs_opts = ",,";
- if (proxy.plugin === 'obfs') {
- const {mode, host} = proxy['plugin-opts'];
- obfs_opts = `,${mode},${host}`
- }
- return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password}${obfs_opts}`;
- case "ssr":
- return `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password},${proxy.protocol},{${proxy['protocol-param']}},${proxy.obfs},{${proxy['obfs-param']}}`
- case "vmess":
- obfs_opts = "";
- if (proxy.network === 'ws') {
- const host = proxy['ws-headers'].Host;
- obfs_opts = `,transport:ws,host:${host},path:${proxy['ws-path']}`;
- } else {
- obfs_opts = `,transport:tcp`;
- }
- if (proxy.tls) {
- obfs_opts += `,tls-name=${proxy.sni},skip-cert-verify:${proxy.scert}`;
- }
- return `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},over-tls:${proxy.tls}${obfs_opts}`;
- case "trojan":
- return `${proxy.name}=trojan,${proxy.server},${proxy.port},${proxy.password},tls-name:${proxy.sni},skip-cert-verify:${proxy.scert}`;
- case "http":
- tls_opts = "";
- const base = `${proxy.name}=${proxy.tls ? 'http' : 'https'},${proxy.server},${proxy.port},${proxy.username || ""},${proxy.password || ""}`;
- if (proxy.tls) {
- // https
- tls_opts = `,skip-cert-verify:${proxy.scert},tls-name:${proxy.sni}`;
- return base + tls_opts;
- } else return base;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output}
-}
-
-function Surge_Producer() {
- const targetPlatform = "Surge";
- const output = (proxy) => {
- let obfs_opts, tls_opts;
- switch (proxy.type) {
- case 'ss':
- obfs_opts = "";
- if (proxy.plugin === "obfs") {
- obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`
- } else {
- throw new Error(`Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}`);
- }
- return `${proxy.name}=ss,${proxy.server},${proxy.port},encrypt-method=${proxy.cipher},password=${proxy.password}${obfs_opts},tfo=${proxy.tfo || 'false'},udp-relay=${proxy.udp || 'false'}`;
- case 'vmess':
- tls_opts = "";
- let config = `${proxy.name}=vmess,${proxy.server},${proxy.port},username=${proxy.uuid},tls=${proxy.tls},tfo=${proxy.tfo || "false"}`;
- if (proxy.network === 'ws') {
- const path = proxy['ws-path'];
- const host = proxy['ws-headers'].Host;
- config += `,ws=true${path ? ',ws-path=' + path : ""}${host ? ',ws-headers=HOST:' + host : ""}`;
- }
- if (proxy.tls) {
- config += `,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
- }
- return config;
- case 'trojan':
- return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${proxy.password},sni=${proxy.sni},tfo=${proxy.tfo || 'false'}`;
- case 'http':
- tls_opts = ",tls=false";
- if (proxy.tls) {
- tls_opts = `,tls=true,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
- }
- return `${proxy.name}=http,${proxy.server},${proxy.port}${proxy.username ? ",username=" + proxy.username : ""}${proxy.password ? ",password=" + proxy.password : ""}${tls_opts},tfo=${proxy.tfo || 'false'}`;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output};
-}
-
-function Raw_Producer() {
- const targetPlatform = "Raw";
- const output = (proxy) => {
- return JSON.stringify(proxy);
- }
- return {targetPlatform, output};
-}
-
-/**************************** Operators ***************************************/
-// force to set some properties (e.g., scert, udp, tfo, etc.)
-function SetPropertyOperator(key, val) {
- return {
- name: "Set Property Operator",
- func: proxies => {
- return proxies.map(p => {
- p[key] = val;
- return p;
- })
- }
- }
-}
-
-// add or remove flag for proxies
-function FlagOperator(type) {
- return {
- name: "Flag Operator",
- func: proxies => {
- return proxies.map(proxy => {
- switch (type) {
- case 0:
- // no flag
- proxy.name = removeFlag(proxy.name);
- break
- case 1:
- // get flag
- const newFlag = getFlag(proxy.name);
- // remove old flag
- proxy.name = removeFlag(proxy.name);
- proxy.name = newFlag + " " + proxy.name;
- proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳");
- break;
- default:
- throw new Error("Unknown flag type: " + type);
- }
- return proxy;
- })
- }
- }
-}
-
-// sort proxies according to their names
-function SortOperator(order = 'asc') {
- return {
- name: "Sort Operator",
- func: proxies => {
- switch (order) {
- case "asc":
- case 'desc':
- return proxies.sort((a, b) => {
- let res = (a.name > b.name) ? 1 : -1;
- res *= order === 'desc' ? -1 : 1;
- return res
- })
- case 'random':
- return shuffle(proxies);
- default:
- throw new Error("Unknown sort option: " + order);
- }
- }
- }
-}
-
-// sort by keywords
-function KeywordSortOperator(...keywords) {
- return {
- name: "Keyword Sort Operator",
- func: proxies => proxies.sort((a, b) => {
- const oA = getKeywordOrder(keywords, a.name);
- const oB = getKeywordOrder(keywords, b.name);
- if (oA && !oB) return -1;
- if (oB && !oA) return 1;
- if (oA && oB) return oA < oB ? -1 : 1;
- if ((!oA && !oB) || (oA && oB && oA === oB)) return a.name < b.name ? -1 : 1; // fallback to normal sort
- })
- }
-}
-
-function getKeywordOrder(keywords, str) {
- let order = null;
- for (let i = 0; i < keywords.length; i++) {
- if (str.indexOf(keywords[i]) !== -1) {
- order = i + 1; // plus 1 is important! 0 will be treated as false!!!
- break;
- }
- }
- return order;
-}
-
-// rename by keywords
-// keywords: [{old: "old", now: "now"}]
-function KeywordRenameOperator(...keywords) {
- return {
- name: "Keyword Rename Operator",
- func: proxies => {
- return proxies.map(proxy => {
- for (const {old, now} of keywords) {
- proxy.name = proxy.name.replace(old, now);
- }
- return proxy;
- })
- }
- }
-}
-
-// rename by regex
-// keywords: [{expr: "string format regex", now: "now"}]
-function RegexRenameOperator(...regex) {
- return {
- name: "Regex Rename Operator",
- func: proxies => {
- return proxies.map(proxy => {
- for (const {expr, now} of regex) {
- proxy.name = proxy.name.replace(new RegExp(expr, "g"), now);
- }
- return proxy;
- })
- }
- }
-}
-
-// delete keywords operator
-// keywords: ['a', 'b', 'c']
-function KeywordDeleteOperator(...keywords) {
- const keywords_ = keywords.map(k => {
- return {
- old: k,
- now: ""
- }
- })
- return {
- name: "Keyword Delete Operator",
- func: KeywordRenameOperator(keywords_).func
- }
-}
-
-// delete regex operator
-// regex: ['a', 'b', 'c']
-function RegexDeleteOperator(...regex) {
- const regex_ = regex.map(r => {
- return {
- expr: r,
- now: ""
- }
- });
- return {
- name: "Regex Delete Operator",
- func: RegexRenameOperator(regex_).func
- }
-}
-
-// use base64 encoded script to rename
-/** Example script
- function func(proxies) {
- // do something
- return proxies;
- }
-
- WARNING:
- 1. This function name should be `func`!
- 2. Always declare variable before using it!
- */
-function ScriptOperator(script) {
- return {
- name: "Script Operator",
- func: (proxies) => {
- ;(function () {
- eval(script);
- return func(proxies);
- })();
- }
- }
-}
-
-/**************************** Filters ***************************************/
-// filter by keywords
-function KeywordFilter(...keywords) {
- return {
- name: "Keyword Filter",
- func: (proxies) => {
- return proxies.map(proxy => keywords.some(k => proxy.name.indexOf(k) !== -1));
- }
- }
-}
-
-function DiscardKeywordFilter(...keywords) {
- return {
- name: "Discard Keyword Filter",
- func: proxies => {
- const filter = KeywordFilter(keywords).func;
- return NOT(filter(proxies));
- }
- }
-}
-
-// filter useless proxies
-function UselessFilter() {
- const KEYWORDS = ["流量", "时间", "应急", "过期", "Bandwidth", "expire"];
- return {
- name: "Useless Filter",
- func: DiscardKeywordFilter(KEYWORDS).func
- }
-}
-
-// filter by regions
-function RegionFilter(...regions) {
- const REGION_MAP = {
- "HK": "🇭🇰",
- "TW": "🇹🇼",
- "US": "🇺🇸",
- "SG": "🇸🇬",
- "JP": "🇯🇵",
- "UK": "🇬🇧",
- "KR": "🇰🇷"
- };
- return {
- name: "Region Filter",
- func: (proxies) => {
- // this would be high memory usage
- return proxies.map(proxy => {
- const flag = getFlag(proxy.name);
- return regions.some(r => REGION_MAP[r] === flag);
- })
- }
- }
-}
-
-// filter by regex
-function RegexFilter(...regex) {
- return {
- name: "Regex Filter",
- func: (proxies) => {
- return proxies.map(proxy => regex.some(r => r.test(proxy.name)));
- }
- }
-}
-
-function DiscardRegexFilter(...regex) {
- return {
- name: "Discard Regex Filter",
- func: proxies => {
- const filter = RegexFilter(regex).func;
- return NOT(filter(proxies));
- }
- }
-}
-
-// filter by proxy types
-function TypeFilter(...types) {
- return {
- name: "Type Filter",
- func: (proxies) => {
- return proxies.map(proxy => types.some(t => proxy.type === t));
- }
- }
-}
-
-// use base64 encoded script to filter proxies
-/** Script Example
- function func(proxies) {
- const selected = FULL(proxies.length, true);
- // do something
- return selected;
- }
- WARNING:
- 1. This function name should be `func`!
- 2. Always declare variable before using it!
- */
-function ScriptFilter(script) {
- return {
- name: "Script Filter",
- func: (proxies) => {
- !(function () {
- eval(script);
- return filter(proxies);
- })();
- }
- }
-}
-
-/******************************** Utility Functions *********************************************/
-// get proxy flag according to its name
-function getFlag(name) {
- // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
- const flags = {
- "🏳️🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
- "🇦🇨": ["AC"],
- "🇦🇹": ["奥地利", "维也纳"],
- "🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
- "🇧🇪": ["BE", "比利时"],
- "🇧🇬": ["保加利亚", "Bulgaria"],
- "🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
- "🇨🇦": ["Canada", "Waterloo", "加拿大", "蒙特利尔", "温哥华", "楓葉", "枫叶", "滑铁卢", "多伦多"],
- "🇨🇭": ["瑞士", "苏黎世", "Switzerland"],
- "🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"],
- "🇩🇰": ["丹麦"],
- "🇪🇸": ["ES", "西班牙", "Spain"],
- "🇪🇺": ["EU", "欧盟", "欧罗巴"],
- "🇫🇮": ["Finland", "芬兰", "赫尔辛基"],
- "🇫🇷": ["FR", "France", "法国", "法國", "巴黎"],
- "🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"],
- "🇲🇴": ["MO", "Macao", "澳门", "CTM"],
- "🇭🇺": ["匈牙利", "Hungary"],
- "🇭🇰": ["HK", "Hongkong", "Hong Kong", "香港", "深港", "沪港", "呼港", "HKT", "HKBN", "HGC", "WTT", "CMI", "穗港", "京港", "港"],
- "🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"],
- "🇮🇪": ["Ireland", "爱尔兰", "都柏林"],
- "🇮🇳": ["India", "印度", "孟买", "Mumbai"],
- "🇰🇵": ["KP", "朝鲜"],
- "🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"],
- "🇱🇻": ["Latvia", "Latvija", "拉脱维亚"],
- "🇲🇽️": ["MEX", "MX", "墨西哥"],
- "🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"],
- "🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"],
- "🇵🇭": ["PH", "Philippines", "菲律宾"],
- "🇷🇴": ["RO", "罗马尼亚"],
- "🇷🇺": ["RU", "Russia", "俄罗斯", "俄羅斯", "伯力", "莫斯科", "圣彼得堡", "西伯利亚", "新西伯利亚", "京俄", "杭俄"],
- "🇸🇦": ["沙特", "迪拜"],
- "🇸🇪": ["SE", "Sweden"],
- "🇸🇬": ["SG", "Singapore", "新加坡", "狮城", "沪新", "京新", "泉新", "穗新", "深新", "杭新", "广新"],
- "🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"],
- "🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"],
- "🇹🇼": ["TW", "Taiwan", "台湾", "台北", "台中", "新北", "彰化", "CHT", "台", "HINET"],
- "🇺🇸": ["US", "USA", "America", "United States", "美国", "美", "京美", "波特兰", "达拉斯", "俄勒冈", "凤凰城", "费利蒙", "硅谷", "矽谷", "拉斯维加斯", "洛杉矶", "圣何塞", "圣克拉拉", "西雅图", "芝加哥", "沪美", "哥伦布", "纽约"],
- "🇻🇳": ["VN", "越南", "胡志明市"],
- "🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"],
- "🇿🇦": ["South Africa", "南非"],
- "🇦🇪": ["United Arab Emirates", "阿联酋"],
- "🇯🇵": ["JP", "Japan", "日", "日本", "东京", "大阪", "埼玉", "沪日", "穗日", "川日", "中日", "泉日", "杭日", "深日", "辽日", "广日"],
- "🇦🇷": ["AR", "阿根廷"],
- "🇳🇴": ["Norway", "挪威", "NO"],
- "🇨🇳": ["CN", "China", "回国", "中国", "江苏", "北京", "上海", "广州", "深圳", "杭州", "徐州", "青岛", "宁波", "镇江", "back"]
- };
- for (let k of Object.keys(flags)) {
- if (flags[k].some((item => name.indexOf(item) !== -1))) {
- return k;
- }
- }
- // no flag found
- const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0];
- return oldFlag || "🏴☠️";
-}
-
-// remove flag
-function removeFlag(str) {
- return str.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "").trim();
-}
-
-// clone an object
-function clone(obj) {
- return JSON.parse(JSON.stringify(obj))
-}
-
-// shuffle array
-function shuffle(array) {
- let currentIndex = array.length, temporaryValue, randomIndex;
-
- // While there remain elements to shuffle...
- while (0 !== currentIndex) {
-
- // Pick a remaining element...
- randomIndex = Math.floor(Math.random() * currentIndex);
- currentIndex -= 1;
-
- // And swap it with the current element.
- temporaryValue = array[currentIndex];
- array[currentIndex] = array[randomIndex];
- array[randomIndex] = temporaryValue;
- }
-
- return array;
-}
-
-// some logical functions for proxy filters
-function AND(...args) {
- return args.reduce((a, b) => a.map((c, i) => b[i] && c));
-}
-
-function OR(...args) {
- return args.reduce((a, b) => a.map((c, i) => b[i] || c))
-}
-
-function NOT(array) {
- return array.map(c => !c);
-}
-
-function FULL(length, bool) {
- return [...Array(length).keys()].map(() => bool);
-}
-
-// UUID
-// source: https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
-function UUID() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
-}
-
-// get platform form UA
-function getPlatformFromHeaders(headers) {
- const keys = Object.keys(headers);
- let UA = "";
- for (let k of keys) {
- if (k.match(/USER-AGENT/i)) {
- UA = headers[k];
- break;
- }
- }
- if (UA.indexOf("Quantumult%20X") !== -1) {
- return "QX";
- } else if (UA.indexOf("Surge") !== -1) {
- return "Surge";
- } else if (UA.indexOf("Decar") !== -1) {
- return "Loon";
- } else {
- // browser
- return FALL_BACK_TARGET;
- }
-}
-
-/*********************************** OpenAPI *************************************/
-// OpenAPI
-// prettier-ignore
-function ENV() {
- const isQX = typeof $task != "undefined";
- const isLoon = typeof $loon != "undefined";
- const isSurge = typeof $httpClient != "undefined" && !this.isLoon;
- const isJSBox = typeof require == "function" && typeof $jsbox != "undefined";
- const isNode = typeof require == "function" && !isJSBox;
- const isRequest = typeof $request !== "undefined";
- return {isQX, isLoon, isSurge, isNode, isJSBox, isRequest};
-}
-
-function HTTP(baseURL, defaultOptions = {}) {
- const {isQX, isLoon, isSurge} = ENV();
- const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"];
-
- function send(method, options) {
- options = typeof options === "string" ? {url: options} : options;
- options.url = baseURL ? baseURL + options.url : options.url;
- options = {...defaultOptions, ...options};
- const timeout = options.timeout;
- const events = {
- ...{
- onRequest: () => {
- },
- onResponse: (resp) => resp,
- onTimeout: () => {
- },
- },
- ...options.events,
- };
-
- events.onRequest(method, options);
-
- let worker;
- if (isQX) {
- worker = $task.fetch({method, ...options});
- } else {
- worker = new Promise((resolve, reject) => {
- const request = isSurge || isLoon ? $httpClient : require("request");
- request[method.toLowerCase()](options, (err, response, body) => {
- if (err) reject(err);
- else
- resolve({
- statusCode: response.status || response.statusCode,
- headers: response.headers,
- body,
- });
- });
- });
- }
-
- let timeoutid;
- const timer = timeout
- ? new Promise((_, reject) => {
- timeoutid = setTimeout(() => {
- events.onTimeout();
- return reject(
- `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`
- );
- }, timeout);
- })
- : null;
-
- return (timer
- ? Promise.race([timer, worker]).then((res) => {
- clearTimeout(timeoutid);
- return res;
- })
- : worker
- )
- .then((resp) => events.onResponse(resp))
- }
-
- const http = {};
- methods.forEach(
- (method) =>
- (http[method.toLowerCase()] = (options) => send(method, options))
- );
- return http;
-}
-
-function API(name = "untitled", debug = false) {
- const {isQX, isLoon, isSurge, isNode, isJSBox} = ENV();
- return new (class {
- constructor(name, debug) {
- this.name = name;
- this.debug = debug;
-
- this.http = HTTP();
- this.env = ENV();
-
- this.node = (() => {
- if (isNode) {
- const fs = require("fs");
-
- return {
- fs,
- };
- } else {
- return null;
- }
- })();
- this.initCache();
-
- const delay = (t, v) =>
- new Promise(function (resolve) {
- setTimeout(resolve.bind(null, v), t);
- });
-
- Promise.prototype.delay = function (t) {
- return this.then(function (v) {
- return delay(t, v);
- });
- };
- }
-
- // persistance
-
- // initialize cache
- initCache() {
- if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}");
- if (isLoon || isSurge)
- this.cache = JSON.parse($persistentStore.read(this.name) || "{}");
-
- if (isNode) {
- // create a json for root cache
- let fpath = "root.json";
- if (!this.node.fs.existsSync(fpath)) {
- this.node.fs.writeFileSync(
- fpath,
- JSON.stringify({}),
- {flag: "wx"},
- (err) => console.log(err)
- );
- }
- this.root = {};
-
- // create a json file with the given name if not exists
- fpath = `${this.name}.json`;
- if (!this.node.fs.existsSync(fpath)) {
- this.node.fs.writeFileSync(
- fpath,
- JSON.stringify({}),
- {flag: "wx"},
- (err) => console.log(err)
- );
- this.cache = {};
- } else {
- this.cache = JSON.parse(
- this.node.fs.readFileSync(`${this.name}.json`)
- );
- }
- }
- }
-
- // store cache
- persistCache() {
- const data = JSON.stringify(this.cache);
- if (isQX) $prefs.setValueForKey(data, this.name);
- if (isLoon || isSurge) $persistentStore.write(data, this.name);
- if (isNode) {
- this.node.fs.writeFileSync(
- `${this.name}.json`,
- data,
- {flag: "w"},
- (err) => console.log(err)
- );
- this.node.fs.writeFileSync(
- "root.json",
- JSON.stringify(this.root),
- {flag: "w"},
- (err) => console.log(err)
- );
- }
- }
-
- write(data, key) {
- this.log(`SET ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- $persistentStore.write(data, key);
- }
- if (isQX) {
- $prefs.setValueForKey(data, key);
- }
- if (isNode) {
- this.root[key] = data;
- }
- } else {
- this.cache[key] = data;
- }
- this.persistCache();
- }
-
- read(key) {
- this.log(`READ ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- return $persistentStore.read(key);
- }
- if (isQX) {
- return $prefs.valueForKey(key);
- }
- if (isNode) {
- return this.root[key];
- }
- } else {
- return this.cache[key];
- }
- }
-
- delete(key) {
- this.log(`DELETE ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- $persistentStore.write(null, key);
- }
- if (isQX) {
- $prefs.removeValueForKey(key);
- }
- if (isNode) {
- delete this.root[key];
- }
- } else {
- delete this.cache[key];
- }
- this.persistCache();
- }
-
- // notification
- notify(title, subtitle = "", content = "", options = {}) {
- const openURL = options["open-url"];
- const mediaURL = options["media-url"];
-
- const content_ =
- content +
- (openURL ? `\n点击跳转: ${openURL}` : "") +
- (mediaURL ? `\n多媒体: ${mediaURL}` : "");
-
- if (isQX) $notify(title, subtitle, content, options);
- if (isSurge) $notification.post(title, subtitle, content_);
- if (isLoon) $notification.post(title, subtitle, content, openURL);
- if (isNode) {
- if (isJSBox) {
- const push = require("push");
- push.schedule({
- title: title,
- body: (subtitle ? subtitle + "\n" : "") + content_,
- });
- } else {
- console.log(`${title}\n${subtitle}\n${content_}\n\n`);
- }
- }
- }
-
- // other helper functions
- log(msg) {
- if (this.debug) console.log(msg);
- }
-
- info(msg) {
- console.log(msg);
- }
-
- error(msg) {
- console.log("ERROR: " + msg);
- }
-
- wait(millisec) {
- return new Promise((resolve) => setTimeout(resolve, millisec));
- }
-
- done(value = {}) {
- if (isQX || isLoon || isSurge) {
- $done(value);
- } else if (isNode && !isJSBox) {
- if (typeof $context !== "undefined") {
- $context.headers = value.headers;
- $context.statusCode = value.statusCode;
- $context.body = value.body;
- }
- }
- }
- })(name, debug);
-}
-
-/*********************************** Mini Express *************************************/
-function express(port = 3000) {
- const {isNode} = ENV();
-
- // node support
- if (isNode) {
- const express_ = require("express");
- const bodyParser = require("body-parser");
- const app = express_();
- app.use(bodyParser.json({verify: rawBodySaver}));
- app.use(bodyParser.urlencoded({verify: rawBodySaver, extended: true}));
- app.use(bodyParser.raw({verify: rawBodySaver, type: '*/*'}));
-
- // adapter
- app.start = () => {
- app.listen(port, () => {
- console.log(`Express started on port: ${port}`);
- })
- }
- return app;
- }
-
- // route handlers
- const handlers = [];
-
- // http methods
- const METHODS_NAMES = [
- "GET",
- "POST",
- "PUT",
- "DELETE",
- "PATCH",
- "OPTIONS",
- "HEAD'",
- "ALL",
- ];
-
- // dispatch url to route
- const dispatch = (request, start = 0) => {
- let {method, url, headers, body} = request;
- method = method.toUpperCase();
- const {path, query} = extractURL(url);
- let handler = null;
- let i;
-
- for (i = start; i < handlers.length; i++) {
- if (handlers[i].method === "ALL" || method === handlers[i].method) {
- const {pattern} = handlers[i];
- if (patternMatched(pattern, path)) {
- handler = handlers[i];
- break;
- }
- }
- }
- if (handler) {
- // dispatch to next handler
- const next = () => {
- dispatch(method, url, i);
- };
- const req = {
- method, url, path, query,
- params: extractPathParams(handler.pattern, path),
- headers, body
- };
- const res = Response();
- handler.callback(req, res, next).catch(err => {
- res.status(500).json({
- status: "failed",
- message: err
- });
- });
- } else {
- // no route, return 404
- const res = Response();
- res.status("404").json({
- status: "failed",
- message: "ERROR: 404 not found"
- });
- }
- };
-
- const app = {};
-
- // attach http methods
- METHODS_NAMES.forEach((method) => {
- app[method.toLowerCase()] = (pattern, callback) => {
- // add handler
- handlers.push({method, pattern, callback});
- };
- });
-
- // chainable route
- app.route = (pattern) => {
- const chainApp = {};
- METHODS_NAMES.forEach((method) => {
- chainApp[method.toLowerCase()] = (callback) => {
- // add handler
- handlers.push({method, pattern, callback});
- return chainApp;
- };
- });
- return chainApp;
- };
-
- // start service
- app.start = () => {
- dispatch($request);
- };
-
- return app;
-
- /************************************************
- Utility Functions
- *************************************************/
- function rawBodySaver(req, res, buf, encoding) {
- if (buf && buf.length) {
- req.rawBody = buf.toString(encoding || 'utf8');
- }
- }
-
- function Response() {
- let statusCode = "200";
- const {isQX, isLoon, isSurge} = ENV();
- const headers = {
- "Content-Type": "text/plain;charset=UTF-8",
- };
- return new (class {
- status(code) {
- statusCode = code;
- return this;
- }
-
- send(body = "") {
- const response = {
- status: statusCode,
- body,
- headers,
- };
- if (isQX) {
- $done(...response);
- } else if (isLoon || isSurge) {
- $done({
- response,
- });
- }
- }
-
- end() {
- this.send();
- }
-
- html(data) {
- this.set("Content-Type", "text/html;charset=UTF-8");
- this.send(data);
- }
-
- json(data) {
- this.set("Content-Type", "application/json;charset=UTF-8");
- this.send(JSON.stringify(data));
- }
-
- set(key, val) {
- headers[key] = val;
- return this;
- }
- })();
- }
-
- function patternMatched(pattern, path) {
- if (pattern instanceof RegExp && pattern.test(path)) {
- return true;
- } else {
- // root pattern, match all
- if (pattern === "/") return true;
- // normal string pattern
- if (pattern.indexOf(":") === -1) {
- const spath = path.split("/");
- const spattern = pattern.split("/");
- for (let i = 0; i < spattern.length; i++) {
- if (spath[i] !== spattern[i]) {
- return false;
- }
- }
- return true;
- }
- // string pattern with path parameters
- else if (extractPathParams(pattern, path)) {
- return true;
- }
- }
- return false;
- }
-
- function extractURL(url) {
- // extract path
- const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
- const path = match[1] || "/";
-
- // extract query string
- const split = url.indexOf("?");
- const query = {};
- if (split !== -1) {
- let hashes = url.slice(url.indexOf("?") + 1).split("&");
- for (let i = 0; i < hashes.length; i++) {
- hash = hashes[i].split("=");
- query[hash[0]] = hash[1];
- }
- }
- return {
- path,
- query,
- };
- }
-
- function extractPathParams(pattern, path) {
- if (pattern.indexOf(":") === -1) {
- return null;
- } else {
- const params = {};
- for (let i = 0, j = 0; i < pattern.length; i++, j++) {
- if (pattern[i] === ":") {
- let key = [];
- let val = [];
- while (pattern[++i] !== "/" && i < pattern.length) {
- key.push(pattern[i]);
- }
- while (path[j] !== "/" && j < path.length) {
- val.push(path[j++]);
- }
- params[key.join("")] = val.join("");
- } else {
- if (pattern[i] !== path[j]) {
- return null;
- }
- }
- }
- return params;
- }
- }
-}
-
-/******************************** Base 64 *********************************************/
-// Base64 Coding Library
-// https://github.com/dankogai/js-base64#readme
-// Under BSD License
-function Base64Code() {
- // constants
- const b64chars
- = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- const b64tab = function (bin) {
- const t = {};
- let i = 0;
- const l = bin.length;
- for (; i < l; i++) t[bin.charAt(i)] = i;
- return t;
- }(b64chars);
- const fromCharCode = String.fromCharCode;
- // encoder stuff
- const cb_utob = function (c) {
- let cc;
- if (c.length < 2) {
- cc = c.charCodeAt(0);
- return cc < 0x80 ? c
- : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
- + fromCharCode(0x80 | (cc & 0x3f)))
- : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
- + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
- + fromCharCode(0x80 | (cc & 0x3f)));
- } else {
- cc = 0x10000
- + (c.charCodeAt(0) - 0xD800) * 0x400
- + (c.charCodeAt(1) - 0xDC00);
- return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
- + fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
- + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
- + fromCharCode(0x80 | (cc & 0x3f)));
- }
- };
- const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
- const utob = function (u) {
- return u.replace(re_utob, cb_utob);
- };
- const cb_encode = function (ccc) {
- const padlen = [0, 2, 1][ccc.length % 3],
- ord = ccc.charCodeAt(0) << 16
- | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
- | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
- chars = [
- b64chars.charAt(ord >>> 18),
- b64chars.charAt((ord >>> 12) & 63),
- padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
- padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
- ];
- return chars.join('');
- };
- const btoa = function (b) {
- return b.replace(/[\s\S]{1,3}/g, cb_encode);
- };
- this.encode = function (u) {
- const isUint8Array = Object.prototype.toString.call(u) === '[object Uint8Array]';
- return isUint8Array ? u.toString('base64')
- : btoa(utob(String(u)));
- }
- const uriencode = function (u, urisafe) {
- return !urisafe
- ? _encode(u)
- : _encode(String(u)).replace(/[+\/]/g, function (m0) {
- return m0 === '+' ? '-' : '_';
- }).replace(/=/g, '');
- };
- const encodeURI = function (u) {
- return uriencode(u, true)
- };
- // decoder stuff
- const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
- const cb_btou = function (cccc) {
- switch (cccc.length) {
- case 4:
- const cp = ((0x07 & cccc.charCodeAt(0)) << 18)
- | ((0x3f & cccc.charCodeAt(1)) << 12)
- | ((0x3f & cccc.charCodeAt(2)) << 6)
- | (0x3f & cccc.charCodeAt(3)),
- offset = cp - 0x10000;
- return (fromCharCode((offset >>> 10) + 0xD800)
- + fromCharCode((offset & 0x3FF) + 0xDC00));
- case 3:
- return fromCharCode(
- ((0x0f & cccc.charCodeAt(0)) << 12)
- | ((0x3f & cccc.charCodeAt(1)) << 6)
- | (0x3f & cccc.charCodeAt(2))
- );
- default:
- return fromCharCode(
- ((0x1f & cccc.charCodeAt(0)) << 6)
- | (0x3f & cccc.charCodeAt(1))
- );
- }
- };
- const btou = function (b) {
- return b.replace(re_btou, cb_btou);
- };
- const cb_decode = function (cccc) {
- const len = cccc.length,
- padlen = len % 4,
- n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
- | (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
- | (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
- | (len > 3 ? b64tab[cccc.charAt(3)] : 0),
- chars = [
- fromCharCode(n >>> 16),
- fromCharCode((n >>> 8) & 0xff),
- fromCharCode(n & 0xff)
- ];
- chars.length -= [0, 0, 2, 1][padlen];
- return chars.join('');
- };
- const _atob = function (a) {
- return a.replace(/\S{1,4}/g, cb_decode);
- };
- const atob = function (a) {
- return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
- };
- const _decode = function (u) {
- return btou(_atob(u))
- };
- this.decode = function (a) {
- return _decode(
- String(a).replace(/[-_]/g, function (m0) {
- return m0 === '-' ? '+' : '/'
- })
- .replace(/[^A-Za-z0-9\+\/]/g, '')
- ).replace(/>/g, ">").replace(/</g, "<");
- };
- this.safeEncode = function (a) {
- return this.encode(a.replace(/\+/g, "-").replace(/\//g, "_"));
- }
- this.safeDecode = function (a) {
- return this.decode(a.replace(/-/g, "+").replace(/_/g, "/"));
- }
-}
\ No newline at end of file
diff --git a/collection.json b/collection.json
deleted file mode 100644
index 2c32851c7d..0000000000
--- a/collection.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "Surge",
- "subscriptions": ["AAEX", "Nexitally"]
-}
\ No newline at end of file
diff --git a/config/Egern.yaml b/config/Egern.yaml
new file mode 100644
index 0000000000..40405d6d41
--- /dev/null
+++ b/config/Egern.yaml
@@ -0,0 +1,42 @@
+name: Sub-Store
+description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *"
+icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
+compat_arguments:
+ ability: http-client-policy
+ cronexp: 55 23 * * *
+ sync: "Sub-Store Sync"
+ timeout: 120
+ engine: auto
+ produce: "# Sub-Store Produce"
+ produce_cronexp: 50 */6 * * *
+ sync_success_notify: true
+ produce_sub: "sub1,sub2"
+ produce_col: "col1,col2"
+compat_arguments_desc: '\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n8️⃣ sync_success_notify\n\n同步配置全部成功时是否发送完成通知\n默认为 true, 填 false/0/no/off 可关闭\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
+scriptings:
+ - http_request:
+ name: Sub-Store Core
+ match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
+ script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
+ body_required: true
+ - http_request:
+ name: Sub-Store Simple
+ match: ^https?:\/\/sub\.store
+ script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
+ body_required: true
+ - schedule:
+ name: "{{{sync}}}"
+ cron: "{{{cronexp}}}"
+ script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
+ arguments:
+ _compat.$argument: "sync_success_notify={{{sync_success_notify}}}"
+ - schedule:
+ name: "{{{produce}}}"
+ cron: "{{{produce_cronexp}}}"
+ script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
+ arguments:
+ _compat.$argument: "sub={{{produce_sub}}}&col={{{produce_col}}}"
+mitm:
+ hostnames:
+ includes:
+ - sub.store
diff --git a/config/Loon.plugin b/config/Loon.plugin
new file mode 100644
index 0000000000..fa937fb487
--- /dev/null
+++ b/config/Loon.plugin
@@ -0,0 +1,22 @@
+#!name=Sub-Store
+#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分, 可在插件设置中自定义. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
+#!openUrl=https://sub.store
+#!author=Peng-YM
+#!homepage=https://github.com/sub-store-org/Sub-Store
+#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
+
+[Argument]
+cron=input, "55 23 * * *", tag=定时参数, desc=这里需要输入符合CRON表达式的参数
+sync_success_notify=switch, true, tag=同步成功通知, desc=同步配置全部成功时是否发送完成通知
+
+[Rule]
+DOMAIN,sub-store.vercel.app,PROXY
+
+[MITM]
+hostname=sub.store
+
+[Script]
+http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
+http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
+
+cron {cron} script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync, argument=[{sync_success_notify}]
diff --git a/config/QX-Task.json b/config/QX-Task.json
new file mode 100644
index 0000000000..a03d93a7e1
--- /dev/null
+++ b/config/QX-Task.json
@@ -0,0 +1,7 @@
+{
+ "name": "Sub-Store",
+ "description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'",
+ "task": [
+ "55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
+ ]
+}
\ No newline at end of file
diff --git a/config/QX.snippet b/config/QX.snippet
new file mode 100644
index 0000000000..ca654339f1
--- /dev/null
+++ b/config/QX.snippet
@@ -0,0 +1,4 @@
+hostname=sub.store
+
+^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
+^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
\ No newline at end of file
diff --git a/config/README.md b/config/README.md
new file mode 100644
index 0000000000..37c3c668e8
--- /dev/null
+++ b/config/README.md
@@ -0,0 +1,69 @@
+# Sub-Store 配置指南
+
+## 查看更新说明:
+
+Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
+
+Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
+
+## 服务器/云平台/Docker/Android 版
+
+https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
+
+## App 版
+
+### 1. Loon
+
+安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
+
+资源解析器中使用 [https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js](https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js)
+
+### 2. Surge
+
+#### 关于 Surge 的格外说明
+
+Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)
+
+定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
+
+0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
+
+1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
+
+> 最新版 Surge 已删除 `ability: http-client-policy` 参数, 模块暂不做修改, 对测落地功能无影响
+
+2. 经典版, 不支持编辑参数, 固定带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者 cname 脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
+
+3. 经典版, 不支持编辑参数, 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
+
+### 3. QX
+
+订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
+
+定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
+
+### 4. Stash
+
+安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
+
+### 5. Shadowrocket
+
+安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。
+
+### 6. Egern
+
+安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。
+
+## 使用 Sub-Store
+
+1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
+2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
+3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
+
+## 链接参数说明
+
+https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
+
+## 脚本使用说明
+
+https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
diff --git a/config/Stash.stoverride b/config/Stash.stoverride
new file mode 100644
index 0000000000..b46419fb7f
--- /dev/null
+++ b/config/Stash.stoverride
@@ -0,0 +1,37 @@
+name: Sub-Store
+desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
+icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
+
+http:
+ mitm:
+ - sub.store
+ script:
+ - match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
+ name: sub-store-1
+ type: request
+ require-body: true
+ timeout: 120
+ - match: ^https?:\/\/sub\.store
+ name: sub-store-0
+ type: request
+ require-body: true
+ timeout: 120
+
+cron:
+ script:
+ - name: cron-sync-artifacts
+ cron: "55 23 * * *"
+ timeout: 120
+
+script-providers:
+ sub-store-0:
+ url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
+ interval: 86400
+
+ sub-store-1:
+ url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
+ interval: 86400
+
+ cron-sync-artifacts:
+ url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
+ interval: 86400
diff --git a/config/Surge-Beta.sgmodule b/config/Surge-Beta.sgmodule
new file mode 100644
index 0000000000..840025bf2d
--- /dev/null
+++ b/config/Surge-Beta.sgmodule
@@ -0,0 +1,17 @@
+#!name=Sub-Store(β)
+#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
+#!category=订阅管理
+#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,sync_success_notify:true,produce_sub:"sub1,sub2",produce_col:"col1,col2",max_size:-1
+#!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n8️⃣ sync_success_notify\n\n同步配置全部成功时是否发送完成通知\n默认为 true, 填 false/0/no/off 可关闭\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
+
+[MITM]
+hostname = %APPEND% sub.store
+
+[Script]
+Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}},max-size={{{max_size}}}
+
+Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}}
+
+{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sync_success_notify={{{sync_success_notify}}}"
+
+{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"
diff --git a/config/Surge-Noability.sgmodule b/config/Surge-Noability.sgmodule
new file mode 100644
index 0000000000..a6d981a9a6
--- /dev/null
+++ b/config/Surge-Noability.sgmodule
@@ -0,0 +1,13 @@
+#!name=Sub-Store
+#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
+#!category=订阅管理
+
+[MITM]
+hostname = %APPEND% sub.store
+
+[Script]
+# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
+Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
+Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
+
+Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
diff --git a/config/Surge-ability.sgmodule b/config/Surge-ability.sgmodule
new file mode 100644
index 0000000000..fb52536b2b
--- /dev/null
+++ b/config/Surge-ability.sgmodule
@@ -0,0 +1,12 @@
+#!name=Sub-Store
+#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
+#!category=订阅管理
+
+[MITM]
+hostname = %APPEND% sub.store
+
+[Script]
+Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
+Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
+
+Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
diff --git a/config/Surge.sgmodule b/config/Surge.sgmodule
new file mode 100644
index 0000000000..4868cd6b13
--- /dev/null
+++ b/config/Surge.sgmodule
@@ -0,0 +1,17 @@
+#!name=Sub-Store
+#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
+#!category=订阅管理
+#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,sync_success_notify:true,produce_sub:"sub1,sub2",produce_col:"col1,col2",max_size:-1
+#!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n8️⃣ sync_success_notify\n\n同步配置全部成功时是否发送完成通知\n默认为 true, 填 false/0/no/off 可关闭\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
+
+[MITM]
+hostname = %APPEND% sub.store
+
+[Script]
+Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}},max-size={{{max_size}}}
+
+Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}}
+
+{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sync_success_notify={{{sync_success_notify}}}"
+
+{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"
diff --git a/docs/brainstorms/2026-05-15-h2-connect-proxy-headers-requirements.md b/docs/brainstorms/2026-05-15-h2-connect-proxy-headers-requirements.md
new file mode 100644
index 0000000000..45dd4669ad
--- /dev/null
+++ b/docs/brainstorms/2026-05-15-h2-connect-proxy-headers-requirements.md
@@ -0,0 +1,67 @@
+---
+date: 2026-05-15
+topic: h2-connect-proxy-headers
+---
+
+# HTTP/2 CONNECT Proxy Headers
+
+## Summary
+
+Sub-Store will preserve Surge HTTP/2 CONNECT proxies as canonical `h2-connect` nodes and round-trip custom proxy request headers for the Surge proxy types that now support them. Egern HTTP/HTTPS output will also carry root `headers` fields, while unsupported targets must not silently drop headers.
+
+---
+
+## Problem Frame
+
+Surge now supports custom request headers on HTTP, HTTPS, HTTP/2 CONNECT, and TrustTunnel proxy definitions, including dynamic placeholders such as `` and ``. Without parser and producer support, these nodes either fail to round-trip or lose headers during conversion. Egern documentation also lists `headers` for HTTP and HTTPS proxies, but current output does not emit it.
+
+---
+
+## Requirements
+
+**Protocol Coverage**
+- R1. Surge input must parse `h2-connect` proxy lines into type `h2-connect`.
+- R2. Surge output must emit type `h2-connect` for canonical `h2-connect` nodes.
+- R3. Frontend protocol filtering must expose HTTP/2 CONNECT and use value `h2-connect`.
+
+**Header Handling**
+- R4. Surge input and output must preserve root `headers` for HTTP, HTTPS, HTTP/2 CONNECT, and TrustTunnel proxies.
+- R5. Header placeholder text such as `` and `` must round-trip as literal configuration text.
+- R6. Egern HTTP and HTTPS output must include root `headers` when present.
+- R7. Outputs that cannot represent root headers for HTTP/HTTPS, HTTP/2 CONNECT, or TrustTunnel must log and filter or error instead of silently dropping those headers.
+
+**Compatibility**
+- R8. Existing proxy types and transport-layer headers, such as WebSocket or VMess/VLESS h2 transport headers, must keep current behavior.
+- R9. Mihomo support must be based on local source verification, not assumption.
+
+---
+
+## Acceptance Examples
+
+- AE1. **Covers R1, R4, R5.** Given `Proxy = h2-connect, example.com, 443, headers=X-Padding:`, when Surge input is parsed, the node type is `h2-connect` and the header value remains unchanged.
+- AE2. **Covers R2, R4.** Given a Surge HTTP, HTTPS, HTTP/2 CONNECT, or TrustTunnel node with root headers, when Surge output is generated, the line contains `headers=...`.
+- AE3. **Covers R6.** Given an Egern HTTP or HTTPS node with root headers, when Egern output is generated, the YAML object includes `headers`.
+- AE4. **Covers R7.** Given a TrustTunnel node with root headers, when outputting to a target without TrustTunnel header support, Sub-Store logs the unsupported header condition and omits or errors the node.
+
+---
+
+## Success Criteria
+
+- Users can parse and re-export Surge HTTP/2 CONNECT and supported header-bearing Surge proxies without losing header configuration.
+- Downstream implementation has explicit platform boundaries for header support and does not need to infer unsupported-target behavior.
+
+---
+
+## Scope Boundaries
+
+- No frontend node editor for authoring headers is added in this pass.
+- No runtime generation of `` values is implemented by Sub-Store; placeholders remain Surge configuration text.
+- No claim that Mihomo supports HTTP/2 CONNECT as a standalone outbound type unless verified in `adapter/parser.go` and `constant/adapters.go`.
+
+---
+
+## Dependencies / Assumptions
+
+- Egern HTTP/HTTPS `headers` support is based on the current Egern proxy configuration documentation.
+- Local Mihomo source shows HTTP proxy `headers` support for the regular HTTP outbound, but no standalone `h2-connect` adapter type and no TrustTunnel `headers` option.
+- sing-box HTTP outbound supports root `headers`, and JSON output preserves canonical proxy objects without platform-specific header loss.
diff --git a/scripts/demo.js b/scripts/demo.js
new file mode 100644
index 0000000000..67cf9b9663
--- /dev/null
+++ b/scripts/demo.js
@@ -0,0 +1,431 @@
+function operator(proxies = [], targetPlatform, context) {
+ // 支持快捷操作 不一定要写一个 function
+ // 可参考 https://t.me/zhetengsha/970
+ // https://t.me/zhetengsha/1009
+
+ // proxies 为传入的内部节点数组
+ // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
+ // 1. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货. 顺便说一下, 关于 mihomo 不支持的协议, 其实也可以用 JSON/JSON5/YAML 格式来输入, 写法可参考使用 includeUnsupportedProxy 参数或开启 包含官方/商店版不支持的协议 开关时的 mihomo 输出内容, 例如 NaiveProxy 输入写法 (https://t.me/zhetengsha/4308)
+ // 2. `_no-resolve` 为不解析域名
+ // 3. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
+ // 4. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
+ // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
+ // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
+ // 7. `tls-fingerprint` 为 tls 指纹
+ // 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
+ // 例如 $server['underlying-proxy'] = '名称'
+ // 只给 mihomo 输出的话, `dialer-proxy` 也行
+ // 只给 sing-box 输出的话, `detour` 也行
+ // 只给 Egern 输出的话, `prev_hop` 也行
+ // 只给 Shadowrocket 输出的话, `chain` 也行
+ // 输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
+ // 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
+ // 10. `sni` 在某些协议里会自动与 `servername` 转换
+ // 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
+ // 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
+ // Surge 的 `private-key`/`client-cert` 对应内部字段 `keystore-private-key`/`keystore-client-cert`, 值都是 Surge `[Keystore]` 里的密钥库条目名, 不是证书/密钥内容或本地路径
+ // 例如: $server['keystore-private-key'] = 'ssh-key-name' 输出 Surge SSH 时会生成 private-key="ssh-key-name"
+ // 例如: $server['keystore-client-cert'] = 'client-cert-name' 输出 Surge TLS 代理时会生成 client-cert="client-cert-name"
+ // 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
+ // 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
+ // 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`. 同时, `sing-box` 支持使用完整的 `_domain_resolver` 结构设置 `domain_resolver` 字段
+ // 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
+ // 仅对 sing-box 源码里有 `network` 字段的协议生效: `ss`, `ssr`, `socks5`, `vmess`, `vless`, `trojan`, `hysteria`, `hysteria2`, `tuic`.
+ // 注意: mihomo 风格的 `udp: true` 表示节点支持 UDP, 不会转换成 sing-box 的 `network: "udp"`; sing-box 默认就是 TCP+UDP. `udp: false` 会转换成 `network: "tcp"`. `_network` 是显式覆盖, 优先级高于 `udp`.
+ // 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
+ // 18. `sing-box` 支持 `_fragment`, `_fragment_fallback_delay`, `_record_fragment` 设置 `tls` 的 `fragment`, `fragment_fallback_delay`, `record_fragment`
+ // 19. `sing-box` 支持 `_certificate`, `_certificate_path`, `_certificate_public_key_sha256`, `_client_certificate`, `_client_certificate_path`, `_client_key`, `_client_key_path` 设置 `tls` 的 `certificate`, `certificate_path`, `certificate_public_key_sha256`, `client_certificate`, `client_certificate_path`, `client_key`, `client_key_path`
+ // 20. `sing-box` 支持使用完整的 `_ech` 结构设置 `tls` 的 `ech`. 避免冲突, URI 里的原始 `ech` 参数会保存在 `_echConfigList`
+ // 21. 2.21.59 开始, `sing-box` 支持使用 `ech-opts` 结构设置 `tls` 的 `ech`. 参考 https://github.com/sub-store-org/Sub-Store/pull/563/changes 基本沿用 mihomo 风格, mihomo 部分字段自动转换. URI `ech` 与 mihomo `ech-opts` 会互转: base64 ECHConfigList 使用 `ech-opts.config`; Xray 的 DNS server 写法(如 `https://1.1.1.1/dns-query` 或 `example.com+https://1.1.1.1/dns-query`)会把 DNS server 放到 `ech-opts._dns`, 显式查询域名放到 `ech-opts.query-server-name`. mihomo 不支持在 ech-opts 中配置 ECH DNS. 如需跟节点 ECH 配置一致, 请在 mihomo 配置文件里设置, 可参考: `dns["nameserver-policy"]["cloudflare-ech.com"] = ["https://dns.alidns.com/dns-query"]` . 反向输出 URI 时, 可设置 `ech-opts._dns` 来拼回 `ech`; 如果只设置 `query-server-name` 且未设置 `_dns`, 默认使用 `https://dns.alidns.com/dns-query` 并输出 warn 日志, 自定义 root DNS 请设置 `ech-opts._dns`. XHTTP `download-settings` 里嵌套的 TLS ECH 同样支持, 其中 `echForceQuery`/`echSockopt` 分别对应 `ech-opts._force-query`/`ech-opts._sockopt`, 嵌套 DNS 可设置 `xhttp-opts.download-settings.ech-opts._dns`
+ // 22. `sing-box` 支持使用完整的 `_curve_preferences` 结构设置 `tls` 的 `curve_preferences`
+ // 23. `interface-name` 指定流量出站接口 只给 Surge 用的话, `interface` 也可以
+ // 24. Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议, 详见 https://t.me/zhetengsha/1735 . 设置节点字段 `_mihomoExternal` 为 `true` 可强制指定使用 mihomo External Proxy Program 输出该节点. 节点字段 _exec 为 mihomo 路径, 默认 /usr/local/bin/mihomo; 节点字段 _localPort 端口为初始端口号, 逐个递减, 默认为 65535. _merge 为开启仅一个 mihomo 进程+多个 listeners 的模式, 此时仅有一个 mihomo External Proxy Program, 节点会转成 SOCKS5, _mergeName 可设置这个 mihomo 节点的名字(默认为 mihomo merged); _config 对象可覆盖默认配置, _defaultNameserver(默认为 [ '180.76.76.76', '52.80.52.52', '119.28.28.28', '223.6.6.6' ]) 和 _nameserver (默认为 [ 'https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query', 'https://doh-pure.onedns.net/dns-query' ]) 为数组 用于自定义 mihomo 的 default-nameserver 和 nameserver
+ // 25. VLESS xhttp URI 的 extra 默认会拆成两部分处理: mihomo 已支持的字段会解析到节点的结构化字段并在输出 URI 时重新组装; extra 里 mihomo 还不支持的字段只会保存在 `_extra_unsupported` 对象里. 输出 URI 时会用“当前结构化字段 + _extra_unsupported”一起构造 extra, 这样既不会让旧 raw extra 覆盖后来修改过的 mihomo 字段, 也能避免 VLESS URI -> VLESS URI 的流程里把暂不支持的 extra 字段丢掉. 但如果节点上显式设置了 `_extra`, 且它是字符串或普通对象, 那么输出 URI 时 extra 会直接使用 `_extra` (对象会自动转成 JSON 字符串), 不再重组结构化字段. 这是为了方便手动自定义 extra, 不用再一个个同步那些本来会影响 extra 的其它字段
+ // 修改/设置 extra 举例:
+ // 如果是你写的全是 mihomo 已经支持的格式 就直接按 mihomo 的设置
+ // 比如
+ // `$server['xhttp-opts']['download-settings'] = {}`
+ // 如果有 mihomo 不支持的字段可以手动设到 `_extra_unsupported` 字段里, 会自动重组
+ // 如果你是 xray -> xray 用, 可以直接设置 `$server._extra = { }` 或 `$server._extra = ""`
+ // 26. `_qx_obfs_http` 为 QX 的 http obfs 原始值, 例如 `http`, `vmess-http`, `vemss-http`, `shadowsocks-http`, 用于 QX 输入输出时保留原始写法. `vemss-http` 应该是 huaqian 的 typo, 没测过, 反正也支持, 报错就自己改成 `vmess-http` 吧
+ // 27. WireGuard 支持 `ip-cidr`(IPv4 前缀长度) 和 `ipv6-cidr`(IPv6 前缀长度) 字段: 内部会保存前缀长度, 若未设置则默认分别为 `32` 和 `128`. 输出到 `mihomo`/`Shadowrocket`/`sing-box`/`URI` 时会带上该后缀
+ // 28. sing-box 1.14.0 起才有 `control_http_client` , 暂时可使用 `control-http-client` 字段设置 sing-box 的 `control_http_client`
+ // 若 `control-http-client` 非空, 输出 sing-box Tailscale endpoint 时会跳过旧版拨号字段映射, 避免和 `detour`/`dialer-proxy` 等 legacy dialer options 冲突. 需要给控制面设置前置代理时, 请写到 `control-http-client.detour`
+
+ // require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
+ // 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
+ // const fs = eval(`require("fs")`)
+ // // const path = eval(`require("path")`)
+ // fs.writeFileSync('/tmp/1.txt', $content, "utf8");
+
+ // $arguments 为传入的脚本参数
+
+ // $options 为通过链接传入的参数
+ // 例如: { arg1: 'a', arg2: 'b' }
+ // 可这样传:
+ // 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
+ // /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
+ // 或这样传:
+ // 先这样处理 encodeURIComponent('arg1=a&arg2=b')
+ // /api/file/foo?$options=arg1%3Da%26arg2%3Db
+
+ // 注意, 编辑页面左下角那个即可预览只是获取数据 并不是一个真实的请求, 故此时无法使用 $options
+ // 默认会带上 _req 字段, 结构为
+ // {
+ // method,
+ // url,
+ // path,
+ // query,
+ // params,
+ // headers,
+ // body,
+ // scoket: {
+ // remoteAddress
+ // }
+ // }
+ // console.log($options)
+
+ // 若设置 $options._res.headers
+ // 则会在输出时设置响应头, 例如:
+ // if ($options) {
+ // $options._res = {
+ // headers: {
+ // 'X-Custom': '1'
+ // }
+ // }
+ // }
+
+ // 若设置 $options._res.status
+ // 则会在输出时设置响应状态码, 例如:
+ // if ($options) {
+ // $options._res = {
+ // status: 404
+ // }
+ // }
+
+ // 一个示例: 请求来自分享且 ua 不符合时, 返回自定义状态码和响应内容
+
+ // const { headers, url, path } = $options?._req || {}
+ // const ua = headers?.['user-agent'] || headers?.['User-Agent']
+
+ // if ($options && /^\/share\//.test(url) && !/surge/i.test(ua)) {
+ // $options._res = {
+ // status: 418
+ // }
+ // $content = `I'm a teapot`
+ // }
+
+ // targetPlatform 为输出的目标平台
+
+ // lodash
+
+ // $substore 为 OpenAPI
+ // 源码 https://raw.githubusercontent.com/sub-store-org/Sub-Store/refs/heads/master/backend/src/vendor/open-api.js
+ // 一个发请求的例子
+ // const $ = $substore
+ // const { body, statusCode } = await $.http.post({
+ // url: 'https://httpbingo.org/anything',
+ // headers: {
+ // 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0',
+ // 'content-type': 'application/json; charset=utf-8',
+ // },
+ // timeout: 5000,
+ // body: JSON.stringify({
+ // a: 1
+ // }),
+ // })
+ // $.info(statusCode)
+ // $.info(body)
+ // const obj = JSON.parse(body)
+
+ // scriptResourceCache 缓存
+ // 可参考 https://t.me/zhetengsha/1003
+ // const cache = scriptResourceCache
+ // 写入
+ // 第三个参数为自定义过期时间(单位: 毫秒)
+ // cache.set('a:1', 1, 1000)
+ // cache.set('a:2', 2)
+ // 获取
+ // cache.get('a:1')
+ // 获取到期时间
+ // cache.gettime('a:1')
+ // 支持第二个参数: 自定义过期时间(单位: 毫秒)
+ // 支持第三个参数: 是否删除过期项
+ // 下面的例子意思是原来是看 a:2 现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除
+ // cache.get('a:2', 1000, true)
+
+ // 清理
+ // 本来是内部的 反正也能用...先这么用吧...
+ // 清理所有过期的
+ // cache._cleanup()
+ // 支持第一个参数: 匹配前缀的项
+ // 支持第二个参数: 自定义过期时间(单位: 毫秒)
+ // 只清理 a: 开头的过期项
+ // cache._cleanup('a:')
+ // 如果想删除所有的 a: 开头的过期项, 目前先传一个大的过期时间吧...
+ // cache._cleanup(undefined, 48 * 3600 * 1000)
+ // 下面的例子意思是原来是看现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除
+ // cache._cleanup(undefined, 1000)
+
+ // 关于缓存时长
+
+ // 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
+
+ // 通过链接下载资源时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
+
+ // 前端(>= 2.16.0) 后端(>= 2.21.0) 支持自定义各种缓存的 TTL 配置
+
+ // 持久化缓存数据在 JSON 里
+
+ // 当配合脚本使用时, 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活
+
+ // async function operator() {
+ // scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
+ // }
+
+ // DOMAIN_RESOLVERS 为内置域名解析, 详见 backend/src/core/proxy-utils/processors/index.js
+
+ // ProxyUtils 为节点处理工具
+ // 可参考 https://t.me/zhetengsha/1066
+ // const ProxyUtils = {
+ // parse, // 订阅解析
+ // process, // 节点操作/文件操作
+ // produce, // 输出订阅
+ // getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)
+ // ipAddress, // https://github.com/beaugunderson/ip-address
+ // isIPv4,
+ // isIPv6,
+ // isIP,
+ // yaml, // yaml 解析和生成
+ // getFlag, // 获取 emoji 旗帜
+ // removeFlag, // 移除 emoji 旗帜
+ // getISO, // 获取 ISO 3166-1 alpha-2 代码
+ // Gist, // Gist 类
+ // download, // 内部的下载方法, 见 backend/src/utils/download.js
+ // downloadFile, // 下载二进制文件, 见 backend/src/utils/download.js
+ // MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
+ // isValidUUID, // 辅助判断是否为有效的 UUID
+ // doh, // DNS over HTTPS 解析, 源码见 backend/src/utils/dns.js, 使用参考本项目里调用方式 backend/src/core/proxy-utils/processors/index.js
+ // Buffer, // https://github.com/feross/buffer
+ // Base64, // https://github.com/dankogai/js-base64
+ // JSON5, // https://github.com/json5/json5
+ // hex_md5, // backend/src/vendor/md5.js
+ // }
+ // 为兼容 https://github.com/xishang0128/sparkle 的 JavaScript 覆写, 也可以直接使用 `b64d`(Base64 解码), `b64e`(Base64 编码), `Buffer`, `yaml`(简单兼容了下 `yaml.parse` 和 `yaml.stringify`)
+
+ // 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
+ // ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
+ // 示例: 给节点名添加前缀
+ // $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
+ // 示例: 给节点名添加旗帜
+ // $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
+
+ // 示例: 从 sni 文件中读取内容并进行节点操作
+ // const sni = await produceArtifact({
+ // type: 'file',
+ // name: 'sni' // 文件名
+ // });
+ // $server.sni = sni
+
+ // 示例: 从 config 文件中读取配置项并进行节点操作
+ // config 的本地内容为
+ // {
+ // "reuse": false
+ // }
+ // 脚本操作为
+ // const config = (ProxyUtils.JSON5 || JSON).parse(await produceArtifact({
+ // type: 'file',
+ // name: 'config' // 文件名
+ // }))
+ // $server.reuse = config.reuse
+
+ // 1. Surge 输出 WireGuard 完整配置
+
+ // let proxies = await produceArtifact({
+ // type: 'subscription',
+ // name: 'sub',
+ // platform: 'Surge',
+ // produceOpts: {
+ // 'include-unsupported-proxy': true,
+ // }
+ // })
+ // $content = proxies
+
+ // 2. sing-box
+
+ // 但是一般不需要这样用, 可参考
+ // 1. https://t.me/zhetengsha/1111
+ // 2. https://t.me/zhetengsha/1070
+ // 3. https://t.me/zhetengsha/1241
+
+ // let singboxProxies = await produceArtifact({
+ // type: 'subscription', // type: 'subscription' 或 'collection'
+ // name: 'sub', // subscription name
+ // platform: 'sing-box', // target platform
+ // produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )
+ // })
+
+ // // JSON
+ // $content = JSON.stringify({}, null, 2)
+
+ // 3. clash.meta
+
+ // 但是一般不需要这样用, 可参考
+ // 1. https://t.me/zhetengsha/1111
+ // 2. https://t.me/zhetengsha/1070
+ // 3. https://t.me/zhetengsha/1234
+
+ // let clashMetaProxies = await produceArtifact({
+ // type: 'subscription',
+ // name: 'sub',
+ // platform: 'ClashMeta',
+ // produceOpts: {
+ // prettyYaml: true // 输出更易读的块状 YAML, 默认仍是单行 JSON 风格
+ // },
+ // produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
+ // })
+
+ // 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
+ // 见 https://t.me/zhetengsha/1428
+ //
+ // const content = ProxyUtils.produce([...proxies], platform)
+
+ // // YAML
+ // ProxyUtils.yaml.load('YAML String')
+ // ProxyUtils.yaml.safeLoad('YAML String')
+ // $content = ProxyUtils.yaml.safeDump({})
+ // $content = ProxyUtils.yaml.dump({})
+
+ // 一个往文件里插入本地节点的例子:
+ // const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])
+ // let clashMetaProxies = await produceArtifact({
+ // type: 'collection',
+ // name: '机场',
+ // platform: 'ClashMeta',
+ // produceType: 'internal'
+ // })
+ // yaml.proxies.unshift(...clashMetaProxies)
+ // $content = ProxyUtils.yaml.dump(yaml)
+
+ // 一个 Base64 编码的例子:
+ // $content = ProxyUtils.Base64.encode($content ?? $files[0])
+
+ // { $content, $files, $options } will be passed to the next operator
+ // $content is the final content of the file
+
+ // flowUtils 为机场订阅流量信息处理工具
+ // 可参考:
+ // 1. https://t.me/zhetengsha/948
+
+ // context 为传入的上下文, 可在多个脚本中共享使用
+ // 其中 env 为 环境信息, 包含运行版本和其他后端信息
+
+ // 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)
+
+ // 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
+
+ // 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上
+
+ // 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
+
+ // 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
+
+ // let name = ''
+ // for (const [key, value] of Object.entries(context.source)) {
+ // if (!key.startsWith('_')) {
+ // name = value.displayName || value.name
+ // break
+ // }
+ // }
+ // if (!name) {
+ // const collection = context.source._collection
+ // name = collection.displayName || collection.name
+ // }
+
+ // 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
+ // {
+ // "source": {
+ // "sub-1": {
+ // "name": "sub-1",
+ // "displayName": "",
+ // "mergeSources": "",
+ // "ignoreFailedRemoteSub": true,
+ // "process": [],
+ // "icon": "",
+ // "source": "local",
+ // "url": "",
+ // "content": "",
+ // "ua": "",
+ // "display-name": "",
+ // "useCacheForFailedRemoteSub": false
+ // }
+ // },
+ // "backend": "Node",
+ // "version": "2.14.198"
+ // }
+ // 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为:
+ // {
+ // "source": {
+ // "_collection": {
+ // "name": "collection-1",
+ // "displayName": "",
+ // "mergeSources": "",
+ // "ignoreFailedRemoteSub": false,
+ // "icon": "",
+ // "process": [],
+ // "subscriptions": [
+ // "sub-1"
+ // ],
+ // "display-name": ""
+ // }
+ // },
+ // "backend": "Node",
+ // "version": "2.14.198"
+ // }
+ // 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为:
+ // {
+ // "source": {
+ // "sub-1": {
+ // "name": "sub-1",
+ // "displayName": "",
+ // "mergeSources": "",
+ // "ignoreFailedRemoteSub": true,
+ // "icon": "",
+ // "process": [],
+ // "source": "local",
+ // "url": "",
+ // "content": "",
+ // "ua": "",
+ // "display-name": "",
+ // "useCacheForFailedRemoteSub": false
+ // },
+ // "_collection": {
+ // "name": "collection-1",
+ // "displayName": "",
+ // "mergeSources": "",
+ // "ignoreFailedRemoteSub": false,
+ // "icon": "",
+ // "process": [],
+ // "subscriptions": [
+ // "sub-1"
+ // ],
+ // "display-name": ""
+ // }
+ // },
+ // "backend": "Node",
+ // "version": "2.14.198"
+ // }
+
+ // 脚本中使用日志 可以参考这个格式 能在前端日志查看里区分识别
+ // console.log(`[SCOPE] LOG: 信息`)
+ // console.log(`[SCOPE] INFO: 信息`)
+ // console.log(`[SCOPE] WARN: 警告`)
+ // console.log(`[SCOPE] ERROR: 错误`)
+
+ // 参数说明
+ // 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
+
+ console.log(JSON.stringify(context, null, 2));
+
+ return proxies;
+}
diff --git a/scripts/fancy-characters.js b/scripts/fancy-characters.js
new file mode 100644
index 0000000000..8181e5bfbb
--- /dev/null
+++ b/scripts/fancy-characters.js
@@ -0,0 +1,57 @@
+/**
+ * 节点名改为花里胡哨字体,仅支持英文字符和数字
+ *
+ * 【字体】
+ * 可参考:https://www.dute.org/weird-fonts
+ * serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
+ *
+ * 【示例】
+ * 1️⃣ 设置所有格式为 "serif-bold"
+ * #type=serif-bold
+ *
+ * 2️⃣ 设置字母格式为 "serif-bold",数字格式为 "circle-regular"
+ * #type=serif-bold&num=circle-regular
+ */
+
+function operator(proxies) {
+ const { type, num } = $arguments;
+ const TABLE = {
+ "serif-bold": ["𝟎","𝟏","𝟐","𝟑","𝟒","𝟓","𝟔","𝟕","𝟖","𝟗","𝐚","𝐛","𝐜","𝐝","𝐞","𝐟","𝐠","𝐡","𝐢","𝐣","𝐤","𝐥","𝐦","𝐧","𝐨","𝐩","𝐪","𝐫","𝐬","𝐭","𝐮","𝐯","𝐰","𝐱","𝐲","𝐳","𝐀","𝐁","𝐂","𝐃","𝐄","𝐅","𝐆","𝐇","𝐈","𝐉","𝐊","𝐋","𝐌","𝐍","𝐎","𝐏","𝐐","𝐑","𝐒","𝐓","𝐔","𝐕","𝐖","𝐗","𝐘","𝐙"] ,
+ "serif-italic": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "𝑎", "𝑏", "𝑐", "𝑑", "𝑒", "𝑓", "𝑔", "ℎ", "𝑖", "𝑗", "𝑘", "𝑙", "𝑚", "𝑛", "𝑜", "𝑝", "𝑞", "𝑟", "𝑠", "𝑡", "𝑢", "𝑣", "𝑤", "𝑥", "𝑦", "𝑧", "𝐴", "𝐵", "𝐶", "𝐷", "𝐸", "𝐹", "𝐺", "𝐻", "𝐼", "𝐽", "𝐾", "𝐿", "𝑀", "𝑁", "𝑂", "𝑃", "𝑄", "𝑅", "𝑆", "𝑇", "𝑈", "𝑉", "𝑊", "𝑋", "𝑌", "𝑍"],
+ "serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝒂","𝒃","𝒄","𝒅","𝒆","𝒇","𝒈","𝒉","𝒊","𝒋","𝒌","𝒍","𝒎","𝒏","𝒐","𝒑","𝒒","𝒓","𝒔","𝒕","𝒖","𝒗","𝒘","𝒙","𝒚","𝒛","𝑨","𝑩","𝑪","𝑫","𝑬","𝑭","𝑮","𝑯","𝑰","𝑱","𝑲","𝑳","𝑴","𝑵","𝑶","𝑷","𝑸","𝑹","𝑺","𝑻","𝑼","𝑽","𝑾","𝑿","𝒀","𝒁"],
+ "sans-serif-regular": ["𝟢", "𝟣", "𝟤", "𝟥", "𝟦", "𝟧", "𝟨", "𝟩", "𝟪", "𝟫", "𝖺", "𝖻", "𝖼", "𝖽", "𝖾", "𝖿", "𝗀", "𝗁", "𝗂", "𝗃", "𝗄", "𝗅", "𝗆", "𝗇", "𝗈", "𝗉", "𝗊", "𝗋", "𝗌", "𝗍", "𝗎", "𝗏", "𝗐", "𝗑", "𝗒", "𝗓", "𝖠", "𝖡", "𝖢", "𝖣", "𝖤", "𝖥", "𝖦", "𝖧", "𝖨", "𝖩", "𝖪", "𝖫", "𝖬", "𝖭", "𝖮", "𝖯", "𝖰", "𝖱", "𝖲", "𝖳", "𝖴", "𝖵", "𝖶", "𝖷", "𝖸", "𝖹"],
+ "sans-serif-bold": ["𝟬","𝟭","𝟮","𝟯","𝟰","𝟱","𝟲","𝟳","𝟴","𝟵","𝗮","𝗯","𝗰","𝗱","𝗲","𝗳","𝗴","𝗵","𝗶","𝗷","𝗸","𝗹","𝗺","𝗻","𝗼","𝗽","𝗾","𝗿","𝘀","𝘁","𝘂","𝘃","𝘄","𝘅","𝘆","𝘇","𝗔","𝗕","𝗖","𝗗","𝗘","𝗙","𝗚","𝗛","𝗜","𝗝","𝗞","𝗟","𝗠","𝗡","𝗢","𝗣","𝗤","𝗥","𝗦","𝗧","𝗨","𝗩","𝗪","𝗫","𝗬","𝗭"],
+ "sans-serif-italic": ["0","1","2","3","4","5","6","7","8","9","𝘢","𝘣","𝘤","𝘥","𝘦","𝘧","𝘨","𝘩","𝘪","𝘫","𝘬","𝘭","𝘮","𝘯","𝘰","𝘱","𝘲","𝘳","𝘴","𝘵","𝘶","𝘷","𝘸","𝘹","𝘺","𝘻","𝘈","𝘉","𝘊","𝘋","𝘌","𝘍","𝘎","𝘏","𝘐","𝘑","𝘒","𝘓","𝘔","𝘕","𝘖","𝘗","𝘘","𝘙","𝘚","𝘛","𝘜","𝘝","𝘞","𝘟","𝘠","𝘡"],
+ "sans-serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝙖","𝙗","𝙘","𝙙","𝙚","𝙛","𝙜","𝙝","𝙞","𝙟","𝙠","𝙡","𝙢","𝙣","𝙤","𝙥","𝙦","𝙧","𝙨","𝙩","𝙪","𝙫","𝙬","𝙭","𝙮","𝙯","𝘼","𝘽","𝘾","𝘿","𝙀","𝙁","𝙂","𝙃","𝙄","𝙅","𝙆","𝙇","𝙈","𝙉","𝙊","𝙋","𝙌","𝙍","𝙎","𝙏","𝙐","𝙑","𝙒","𝙓","𝙔","𝙕"],
+ "script-regular": ["0","1","2","3","4","5","6","7","8","9","𝒶","𝒷","𝒸","𝒹","ℯ","𝒻","ℊ","𝒽","𝒾","𝒿","𝓀","𝓁","𝓂","𝓃","ℴ","𝓅","𝓆","𝓇","𝓈","𝓉","𝓊","𝓋","𝓌","𝓍","𝓎","𝓏","𝒜","ℬ","𝒞","𝒟","ℰ","ℱ","𝒢","ℋ","ℐ","𝒥","𝒦","ℒ","ℳ","𝒩","𝒪","𝒫","𝒬","ℛ","𝒮","𝒯","𝒰","𝒱","𝒲","𝒳","𝒴","𝒵"],
+ "script-bold": ["0","1","2","3","4","5","6","7","8","9","𝓪","𝓫","𝓬","𝓭","𝓮","𝓯","𝓰","𝓱","𝓲","𝓳","𝓴","𝓵","𝓶","𝓷","𝓸","𝓹","𝓺","𝓻","𝓼","𝓽","𝓾","𝓿","𝔀","𝔁","𝔂","𝔃","𝓐","𝓑","𝓒","𝓓","𝓔","𝓕","𝓖","𝓗","𝓘","𝓙","𝓚","𝓛","𝓜","𝓝","𝓞","𝓟","𝓠","𝓡","𝓢","𝓣","𝓤","𝓥","𝓦","𝓧","𝓨","𝓩"],
+ "fraktur-regular": ["0","1","2","3","4","5","6","7","8","9","𝔞","𝔟","𝔠","𝔡","𝔢","𝔣","𝔤","𝔥","𝔦","𝔧","𝔨","𝔩","𝔪","𝔫","𝔬","𝔭","𝔮","𝔯","𝔰","𝔱","𝔲","𝔳","𝔴","𝔵","𝔶","𝔷","𝔄","𝔅","ℭ","𝔇","𝔈","𝔉","𝔊","ℌ","ℑ","𝔍","𝔎","𝔏","𝔐","𝔑","𝔒","𝔓","𝔔","ℜ","𝔖","𝔗","𝔘","𝔙","𝔚","𝔛","𝔜","ℨ"],
+ "fraktur-bold": ["0","1","2","3","4","5","6","7","8","9","𝖆","𝖇","𝖈","𝖉","𝖊","𝖋","𝖌","𝖍","𝖎","𝖏","𝖐","𝖑","𝖒","𝖓","𝖔","𝖕","𝖖","𝖗","𝖘","𝖙","𝖚","𝖛","𝖜","𝖝","𝖞","𝖟","𝕬","𝕭","𝕮","𝕯","𝕰","𝕱","𝕲","𝕳","𝕴","𝕵","𝕶","𝕷","𝕸","𝕹","𝕺","𝕻","𝕼","𝕽","𝕾","𝕿","𝖀","𝖁","𝖂","𝖃","𝖄","𝖅"],
+ "monospace-regular": ["𝟶","𝟷","𝟸","𝟹","𝟺","𝟻","𝟼","𝟽","𝟾","𝟿","𝚊","𝚋","𝚌","𝚍","𝚎","𝚏","𝚐","𝚑","𝚒","𝚓","𝚔","𝚕","𝚖","𝚗","𝚘","𝚙","𝚚","𝚛","𝚜","𝚝","𝚞","𝚟","𝚠","𝚡","𝚢","𝚣","𝙰","𝙱","𝙲","𝙳","𝙴","𝙵","𝙶","𝙷","𝙸","𝙹","𝙺","𝙻","𝙼","𝙽","𝙾","𝙿","𝚀","𝚁","𝚂","𝚃","𝚄","𝚅","𝚆","𝚇","𝚈","𝚉"],
+ "double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"],
+ "circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
+ "square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
+ "modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
+ };
+
+ // charCode => index in `TABLE`
+ const INDEX = { "48": 0, "49": 1, "50": 2, "51": 3, "52": 4, "53": 5, "54": 6, "55": 7, "56": 8, "57": 9, "65": 36, "66": 37, "67": 38, "68": 39, "69": 40, "70": 41, "71": 42, "72": 43, "73": 44, "74": 45, "75": 46, "76": 47, "77": 48, "78": 49, "79": 50, "80": 51, "81": 52, "82": 53, "83": 54, "84": 55, "85": 56, "86": 57, "87": 58, "88": 59, "89": 60, "90": 61, "97": 10, "98": 11, "99": 12, "100": 13, "101": 14, "102": 15, "103": 16, "104": 17, "105": 18, "106": 19, "107": 20, "108": 21, "109": 22, "110": 23, "111": 24, "112": 25, "113": 26, "114": 27, "115": 28, "116": 29, "117": 30, "118": 31, "119": 32, "120": 33, "121": 34, "122": 35 };
+
+ return proxies.map(p => {
+ p.name = [...p.name].map(c => {
+ if (/[a-zA-Z0-9]/.test(c)) {
+ const code = c.charCodeAt(0);
+ const index = INDEX[code];
+ if (isNumber(code) && num) {
+ return TABLE[num][index];
+ } else {
+ return TABLE[type][index];
+ }
+ }
+ return c;
+ }).join("");
+ return p;
+ })
+}
+
+function isNumber(code) { return code >= 48 && code <= 57; }
\ No newline at end of file
diff --git a/scripts/ip-flag-node.js b/scripts/ip-flag-node.js
new file mode 100644
index 0000000000..650ded1c0c
--- /dev/null
+++ b/scripts/ip-flag-node.js
@@ -0,0 +1,79 @@
+const $ = $substore;
+
+const {onlyFlagIP = true} = $arguments
+
+async function operator(proxies) {
+ const BATCH_SIZE = 10;
+
+ let i = 0;
+ while (i < proxies.length) {
+ const batch = proxies.slice(i, i + BATCH_SIZE);
+ await Promise.all(batch.map(async proxy => {
+ if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return;
+ try {
+ // remove the original flag
+ let proxyName = removeFlag(proxy.name);
+
+ // query ip-api
+ const countryCode = await queryIpApi(proxy);
+
+ proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;
+ proxy.name = proxyName;
+ } catch (err) {
+ // TODO:
+ }
+ }));
+
+ await sleep(1000);
+ i += BATCH_SIZE;
+ }
+ return proxies;
+}
+
+
+async function queryIpApi(proxy) {
+ const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0";
+ const headers = {
+ "User-Agent": ua
+ };
+ const result = new Promise((resolve, reject) => {
+ const url =
+ `http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`;
+ $.http.get({
+ url,
+ headers,
+ }).then(resp => {
+ const data = JSON.parse(resp.body);
+ if (data.status === "success") {
+ resolve(data.countryCode);
+ } else {
+ reject(new Error(data.message));
+ }
+ }).catch(err => {
+ console.log(err);
+ reject(err);
+ });
+ });
+ return result;
+}
+
+function getFlagEmoji(countryCode) {
+ const codePoints = countryCode
+ .toUpperCase()
+ .split('')
+ .map(char => 127397 + char.charCodeAt());
+ return String
+ .fromCodePoint(...codePoints)
+ .replace(/🇹🇼/g, '🇨🇳');
+}
+
+function removeFlag(str) {
+ return str
+ .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
+ .trim();
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
diff --git a/scripts/ip-flag.js b/scripts/ip-flag.js
new file mode 100644
index 0000000000..bda8c792f6
--- /dev/null
+++ b/scripts/ip-flag.js
@@ -0,0 +1,175 @@
+const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
+const CACHE_EXPIRATION_TIME_MS = 10 * 60 * 1000;
+const $ = $substore;
+
+class ResourceCache {
+ constructor(expires) {
+ this.expires = expires;
+ if (!$.read(RESOURCE_CACHE_KEY)) {
+ $.write('{}', RESOURCE_CACHE_KEY);
+ }
+ this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
+ this._cleanup();
+ }
+
+ _cleanup() {
+ // clear obsolete cached resource
+ let clear = false;
+ Object.entries(this.resourceCache).forEach((entry) => {
+ const [id, updated] = entry;
+ if (!updated.time) {
+ // clear old version cache
+ delete this.resourceCache[id];
+ $.delete(`#${id}`);
+ clear = true;
+ }
+ if (new Date().getTime() - updated.time > this.expires) {
+ delete this.resourceCache[id];
+ clear = true;
+ }
+ });
+ if (clear) this._persist();
+ }
+
+ revokeAll() {
+ this.resourceCache = {};
+ this._persist();
+ }
+
+ _persist() {
+ $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
+ }
+
+ get(id) {
+ const updated = this.resourceCache[id] && this.resourceCache[id].time;
+ if (updated && new Date().getTime() - updated <= this.expires) {
+ return this.resourceCache[id].data;
+ }
+ return null;
+ }
+
+ set(id, value) {
+ this.resourceCache[id] = { time: new Date().getTime(), data: value }
+ this._persist();
+ }
+}
+
+const resourceCache = new ResourceCache(CACHE_EXPIRATION_TIME_MS);
+
+async function operator(proxies) {
+ const { isLoon, isSurge } = $substore.env;
+ let support = false;
+ if (isLoon) {
+ support = true;
+ } else if (isSurge) {
+ const build = $environment['surge-build'];
+ if (build && parseInt(build) >= 2407) {
+ support = true;
+ }
+ }
+
+ if (support) {
+ const batches = [];
+ const BATCH_SIZE = 10;
+
+ let i = 0;
+ while (i < proxies.length) {
+ const batch = proxies.slice(i, i + BATCH_SIZE);
+ await Promise.all(batch.map(async proxy => {
+ try {
+ // remove the original flag
+ let proxyName = removeFlag(proxy.name);
+
+ // query ip-api
+ const countryCode = await queryIpApi(proxy);
+
+ proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;
+ proxy.name = proxyName;
+ } catch (err) {
+ // TODO:
+ }
+ }));
+
+ await sleep(1000);
+ i += BATCH_SIZE;
+ }
+ } else {
+ $.error(`IP Flag only supports Loon and Surge!`);
+ }
+ return proxies;
+}
+
+const tasks = new Map();
+async function queryIpApi(proxy) {
+ const id = getId(proxy);
+ if (tasks.has(id)) {
+ return tasks.get(id);
+ }
+
+ const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0";
+ const headers = {
+ "User-Agent": ua
+ };
+ const { isLoon } = $substore.env;
+ const target = isLoon ? "Loon" : "Surge";
+ const result = new Promise((resolve, reject) => {
+ const cached = resourceCache.get(id);
+ if (cached) {
+ resolve(cached);
+ }
+ const url = `http://ip-api.com/json`;
+ let node = ProxyUtils.produce([proxy], target);
+
+ // Loon 需要去掉节点名字
+ if (isLoon) {
+ const s = node.indexOf("=");
+ node = node.substring(s + 1);
+ }
+
+ $.http.get({
+ url,
+ headers,
+ node
+ }).then(resp => {
+ const body = resp.body;
+ const data = JSON.parse(body);
+ if (data.status === "success") {
+ resourceCache.set(id, data.countryCode);
+ resolve(data.countryCode);
+ } else {
+ reject(new Error(data.message));
+ }
+ }).catch(err => {
+ console.log(err);
+ reject(err);
+ });
+ });
+ tasks.set(id, result);
+ return result;
+}
+
+function getId(proxy) {
+ return MD5(`IP-FLAG-${proxy.server}-${proxy.port}`);
+}
+
+function getFlagEmoji(countryCode) {
+ const codePoints = countryCode
+ .toUpperCase()
+ .split('')
+ .map(char => 127397 + char.charCodeAt());
+ return String
+ .fromCodePoint(...codePoints)
+ .replace(/🇹🇼/g, '🇨🇳');
+}
+
+function removeFlag(str) {
+ return str
+ .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
+ .trim();
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+var MD5 = function (d) { var r = M(V(Y(X(d), 8 * d.length))); return r.toLowerCase() }; function M(d) { for (var _, m = "0123456789ABCDEF", f = "", r = 0; r < d.length; r++)_ = d.charCodeAt(r), f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _); return f } function X(d) { for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++)_[m] = 0; for (m = 0; m < 8 * d.length; m += 8)_[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; return _ } function V(d) { for (var _ = "", m = 0; m < 32 * d.length; m += 8)_ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); return _ } function Y(d, _) { d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _; for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) { var h = m, t = f, g = r, e = i; f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e) } return Array(m, f, r, i) } function md5_cmn(d, _, m, f, r, i) { return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m) } function md5_ff(d, _, m, f, r, i, n) { return md5_cmn(_ & m | ~_ & f, d, _, r, i, n) } function md5_gg(d, _, m, f, r, i, n) { return md5_cmn(_ & f | m & ~f, d, _, r, i, n) } function md5_hh(d, _, m, f, r, i, n) { return md5_cmn(_ ^ m ^ f, d, _, r, i, n) } function md5_ii(d, _, m, f, r, i, n) { return md5_cmn(m ^ (_ | ~f), d, _, r, i, n) } function safe_add(d, _) { var m = (65535 & d) + (65535 & _); return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m } function bit_rol(d, _) { return d << _ | d >>> 32 - _ }
\ No newline at end of file
diff --git a/scripts/media-filter.js b/scripts/media-filter.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/scripts/revert.js b/scripts/revert.js
new file mode 100644
index 0000000000..c64cc1b712
--- /dev/null
+++ b/scripts/revert.js
@@ -0,0 +1,5 @@
+const $ = API()
+$.write("{}", "#sub-store")
+$.done()
+
+function ENV(){const e="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:"undefined"!=typeof $task,isLoon:"undefined"!=typeof $loon,isSurge:"undefined"!=typeof $httpClient&&"undefined"!=typeof $utils,isBrowser:"undefined"!=typeof document,isNode:"function"==typeof require&&!e,isJSBox:e,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:o,isScriptable:n,isNode:i,isBrowser:r}=ENV(),u=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;const a={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(h=>a[h.toLowerCase()]=(a=>(function(a,h){h="string"==typeof h?{url:h}:h;const d=e.baseURL;d&&!u.test(h.url||"")&&(h.url=d?d+h.url:h.url),h.body&&h.headers&&!h.headers["Content-Type"]&&(h.headers["Content-Type"]="application/x-www-form-urlencoded");const l=(h={...e,...h}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...h.events};let f,p;if(c.onRequest(a,h),t)f=$task.fetch({method:a,...h});else if(s||o||i)f=new Promise((e,t)=>{(i?require("request"):$httpClient)[a.toLowerCase()](h,(s,o,n)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:n})})});else if(n){const e=new Request(h.url);e.method=a,e.headers=h.headers,e.body=h.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}else r&&(f=new Promise((e,t)=>{fetch(h.url,{method:a,headers:h.headers,body:h.body}).then(e=>e.json()).then(t=>e({statusCode:t.status,headers:t.headers,body:t.data})).catch(t)}));const y=l?new Promise((e,t)=>{p=setTimeout(()=>(c.onTimeout(),t(`${a} URL: ${h.url} exceeds the timeout ${l} ms`)),l)}):null;return(y?Promise.race([y,f]).then(e=>(clearTimeout(p),e)):f).then(e=>c.onResponse(e))})(h,a))),a}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:n,isNode:i,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(i){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),i){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(o||n)&&$persistentStore.write(e,this.name),i&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||o)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);i&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||o?$persistentStore.read(e):s?$prefs.valueForKey(e):i?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||o)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);i&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",a="",h={}){const d=h["open-url"],l=h["media-url"];if(s&&$notify(e,t,a,h),n&&$notification.post(e,t,a+`${l?"\n多媒体:"+l:""}`,{url:d}),o){let s={};d&&(s.openUrl=d),l&&(s.mediaUrl=l),"{}"===JSON.stringify(s)?$notification.post(e,t,a):$notification.post(e,t,a,s)}if(i||u){const s=a+(d?`\n点击跳转: ${d}`:"")+(l?`\n多媒体: ${l}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||n?$done(e):i&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}
diff --git a/scripts/tls-fingerprint.js b/scripts/tls-fingerprint.js
new file mode 100644
index 0000000000..20838a9bcf
--- /dev/null
+++ b/scripts/tls-fingerprint.js
@@ -0,0 +1,12 @@
+/**
+ * 为节点添加 tls 证书指纹
+ * 示例
+ * #fingerprint=...
+ */
+function operator(proxies) {
+ const { fingerprint } = $arguments;
+ proxies.forEach(proxy => {
+ proxy['tls-fingerprint'] = fingerprint;
+ });
+ return proxies;
+}
\ No newline at end of file
diff --git a/scripts/udp-filter.js b/scripts/udp-filter.js
new file mode 100644
index 0000000000..7bd76af07a
--- /dev/null
+++ b/scripts/udp-filter.js
@@ -0,0 +1,6 @@
+/**
+ * 过滤 UDP 节点
+ */
+function filter(proxies) {
+ return proxies.map(p => p.udp);
+}
diff --git a/scripts/vmess-ws-obfs-host.js b/scripts/vmess-ws-obfs-host.js
new file mode 100644
index 0000000000..58c45c5596
--- /dev/null
+++ b/scripts/vmess-ws-obfs-host.js
@@ -0,0 +1,16 @@
+/**
+ * 为 VMess WebSocket 节点修改混淆 host
+ * 示例
+ * #host=google.com
+ */
+function operator(proxies) {
+ const { host } = $arguments;
+ proxies.forEach(p => {
+ if (p.type === 'vmess' && p.network === 'ws') {
+ p["ws-opts"] = p["ws-opts"] || {};
+ p["ws-opts"]["headers"] = p["ws-opts"]["headers"] || {};
+ p["ws-opts"]["headers"]["Host"] = host;
+ }
+ });
+ return proxies;
+}
\ No newline at end of file
diff --git a/sub-store-web/.gitignore b/sub-store-web/.gitignore
deleted file mode 100644
index 403adbc1e5..0000000000
--- a/sub-store-web/.gitignore
+++ /dev/null
@@ -1,23 +0,0 @@
-.DS_Store
-node_modules
-/dist
-
-
-# local env files
-.env.local
-.env.*.local
-
-# Log files
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-
-# Editor directories and files
-.idea
-.vscode
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/sub-store-web/README.md b/sub-store-web/README.md
deleted file mode 100644
index 4f753635d5..0000000000
--- a/sub-store-web/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# sub-store-web
-
-## Project setup
-```
-npm install
-```
-
-### Compiles and hot-reloads for development
-```
-npm run serve
-```
-
-### Compiles and minifies for production
-```
-npm run build
-```
-
-### Lints and fixes files
-```
-npm run lint
-```
-
-### Customize configuration
-See [Configuration Reference](https://cli.vuejs.org/config/).
diff --git a/sub-store-web/babel.config.js b/sub-store-web/babel.config.js
deleted file mode 100644
index e9558405fd..0000000000
--- a/sub-store-web/babel.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- presets: [
- '@vue/cli-plugin-babel/preset'
- ]
-}
diff --git a/sub-store-web/package-lock.json b/sub-store-web/package-lock.json
deleted file mode 100644
index 67a0bd8a5c..0000000000
--- a/sub-store-web/package-lock.json
+++ /dev/null
@@ -1,12117 +0,0 @@
-{
- "name": "sub-store-web",
- "version": "0.1.0",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "@ant-design-vue/babel-helper-vue-transform-on": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/@ant-design-vue/babel-helper-vue-transform-on/download/@ant-design-vue/babel-helper-vue-transform-on-1.0.1.tgz",
- "integrity": "sha1-0hnZL04fxeet0hHDR8f6AAUYtiM=",
- "dev": true
- },
- "@ant-design-vue/babel-plugin-jsx": {
- "version": "1.0.0-rc.1",
- "resolved": "https://registry.npm.taobao.org/@ant-design-vue/babel-plugin-jsx/download/@ant-design-vue/babel-plugin-jsx-1.0.0-rc.1.tgz",
- "integrity": "sha1-rlbOy9qfCGkbz5Lf6Y4kFud9dYs=",
- "dev": true,
- "requires": {
- "@ant-design-vue/babel-helper-vue-transform-on": "^1.0.0",
- "@babel/helper-module-imports": "^7.0.0",
- "@babel/plugin-syntax-jsx": "^7.0.0",
- "@babel/traverse": "^7.0.0",
- "@babel/types": "^7.0.0",
- "camelcase": "^6.0.0",
- "html-tags": "^3.1.0",
- "svg-tags": "^1.0.0"
- }
- },
- "@babel/code-frame": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.10.4.tgz?cache=0&sync_timestamp=1593522948158&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fcode-frame%2Fdownload%2F%40babel%2Fcode-frame-7.10.4.tgz",
- "integrity": "sha1-Fo2ho26Q2miujUnA8bSMfGJJITo=",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.10.4"
- }
- },
- "@babel/compat-data": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/compat-data/download/@babel/compat-data-7.11.0.tgz",
- "integrity": "sha1-6fc+/gmvE1W3I6fzmxG61jfXyZw=",
- "dev": true,
- "requires": {
- "browserslist": "^4.12.0",
- "invariant": "^2.2.4",
- "semver": "^5.5.0"
- }
- },
- "@babel/core": {
- "version": "7.11.1",
- "resolved": "https://registry.npm.taobao.org/@babel/core/download/@babel/core-7.11.1.tgz?cache=0&sync_timestamp=1596578849394&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fcore%2Fdownload%2F%40babel%2Fcore-7.11.1.tgz",
- "integrity": "sha1-LFW2BOc6QNwhsOUmULEcZc8nZkM=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.11.0",
- "@babel/helper-module-transforms": "^7.11.0",
- "@babel/helpers": "^7.10.4",
- "@babel/parser": "^7.11.1",
- "@babel/template": "^7.10.4",
- "@babel/traverse": "^7.11.0",
- "@babel/types": "^7.11.0",
- "convert-source-map": "^1.7.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.1",
- "json5": "^2.1.2",
- "lodash": "^4.17.19",
- "resolve": "^1.3.2",
- "semver": "^5.4.1",
- "source-map": "^0.5.0"
- }
- },
- "@babel/generator": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/generator/download/@babel/generator-7.11.0.tgz?cache=0&sync_timestamp=1596144430330&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fgenerator%2Fdownload%2F%40babel%2Fgenerator-7.11.0.tgz",
- "integrity": "sha1-S5DHjYwSglAkVoy+g+5smvGTWFw=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.11.0",
- "jsesc": "^2.5.1",
- "source-map": "^0.5.0"
- }
- },
- "@babel/helper-annotate-as-pure": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-annotate-as-pure/download/@babel/helper-annotate-as-pure-7.10.4.tgz?cache=0&sync_timestamp=1593521294951&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-annotate-as-pure%2Fdownload%2F%40babel%2Fhelper-annotate-as-pure-7.10.4.tgz",
- "integrity": "sha1-W/DUlaP3V6w72ki1vzs7ownHK6M=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-builder-binary-assignment-operator-visitor": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-builder-binary-assignment-operator-visitor/download/@babel/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz?cache=0&sync_timestamp=1593522960718&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-builder-binary-assignment-operator-visitor%2Fdownload%2F%40babel%2Fhelper-builder-binary-assignment-operator-visitor-7.10.4.tgz",
- "integrity": "sha1-uwt18xv5jL+f8UPBrleLhydK4aM=",
- "dev": true,
- "requires": {
- "@babel/helper-explode-assignable-expression": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-compilation-targets": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-compilation-targets/download/@babel/helper-compilation-targets-7.10.4.tgz?cache=0&sync_timestamp=1593522816195&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-compilation-targets%2Fdownload%2F%40babel%2Fhelper-compilation-targets-7.10.4.tgz",
- "integrity": "sha1-gEro4/BDdmB8x5G51H1UAnYzK9I=",
- "dev": true,
- "requires": {
- "@babel/compat-data": "^7.10.4",
- "browserslist": "^4.12.0",
- "invariant": "^2.2.4",
- "levenary": "^1.1.1",
- "semver": "^5.5.0"
- }
- },
- "@babel/helper-create-class-features-plugin": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-create-class-features-plugin/download/@babel/helper-create-class-features-plugin-7.10.5.tgz",
- "integrity": "sha1-n2FEa6gOgkCwpchcb9rIRZ1vJZ0=",
- "dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.10.4",
- "@babel/helper-member-expression-to-functions": "^7.10.5",
- "@babel/helper-optimise-call-expression": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-replace-supers": "^7.10.4",
- "@babel/helper-split-export-declaration": "^7.10.4"
- }
- },
- "@babel/helper-create-regexp-features-plugin": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-create-regexp-features-plugin/download/@babel/helper-create-regexp-features-plugin-7.10.4.tgz?cache=0&sync_timestamp=1593522973297&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-create-regexp-features-plugin%2Fdownload%2F%40babel%2Fhelper-create-regexp-features-plugin-7.10.4.tgz",
- "integrity": "sha1-/dYNiFJGWaC2lZwFeZJeQlcU87g=",
- "dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.10.4",
- "@babel/helper-regex": "^7.10.4",
- "regexpu-core": "^4.7.0"
- }
- },
- "@babel/helper-define-map": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-define-map/download/@babel/helper-define-map-7.10.5.tgz",
- "integrity": "sha1-tTwQ23imQIABUmkrEzkxR6y5uzA=",
- "dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.10.4",
- "@babel/types": "^7.10.5",
- "lodash": "^4.17.19"
- }
- },
- "@babel/helper-explode-assignable-expression": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-explode-assignable-expression/download/@babel/helper-explode-assignable-expression-7.10.4.tgz?cache=0&sync_timestamp=1593522981063&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-explode-assignable-expression%2Fdownload%2F%40babel%2Fhelper-explode-assignable-expression-7.10.4.tgz",
- "integrity": "sha1-QKHNkXv/Eoj2malKdbN6Gi29jHw=",
- "dev": true,
- "requires": {
- "@babel/traverse": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-function-name": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-function-name/download/@babel/helper-function-name-7.10.4.tgz?cache=0&sync_timestamp=1593522977138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-function-name%2Fdownload%2F%40babel%2Fhelper-function-name-7.10.4.tgz",
- "integrity": "sha1-0tOyDFmtjEcRL6fSqUvAnV74Lxo=",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.10.4",
- "@babel/template": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-get-function-arity": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-get-function-arity/download/@babel/helper-get-function-arity-7.10.4.tgz?cache=0&sync_timestamp=1593521294451&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-get-function-arity%2Fdownload%2F%40babel%2Fhelper-get-function-arity-7.10.4.tgz",
- "integrity": "sha1-mMHL6g4jMvM/mkZhuM4VBbLBm6I=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-hoist-variables": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-hoist-variables/download/@babel/helper-hoist-variables-7.10.4.tgz?cache=0&sync_timestamp=1593521294715&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-hoist-variables%2Fdownload%2F%40babel%2Fhelper-hoist-variables-7.10.4.tgz",
- "integrity": "sha1-1JsAHR1aaMpeZgTdoBpil/fJOB4=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-member-expression-to-functions": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-member-expression-to-functions/download/@babel/helper-member-expression-to-functions-7.11.0.tgz?cache=0&sync_timestamp=1596144430473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-member-expression-to-functions%2Fdownload%2F%40babel%2Fhelper-member-expression-to-functions-7.11.0.tgz",
- "integrity": "sha1-rmnIPYTugvS0L5bioJQQk1qPJt8=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.11.0"
- }
- },
- "@babel/helper-module-imports": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-module-imports/download/@babel/helper-module-imports-7.10.4.tgz?cache=0&sync_timestamp=1593522965782&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-module-imports%2Fdownload%2F%40babel%2Fhelper-module-imports-7.10.4.tgz",
- "integrity": "sha1-TFxUvgS9MWcKc4J5fXW5+i5bViA=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-module-transforms": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-module-transforms/download/@babel/helper-module-transforms-7.11.0.tgz",
- "integrity": "sha1-sW8lAinkchGr3YSzS2RzfCqy01k=",
- "dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.10.4",
- "@babel/helper-replace-supers": "^7.10.4",
- "@babel/helper-simple-access": "^7.10.4",
- "@babel/helper-split-export-declaration": "^7.11.0",
- "@babel/template": "^7.10.4",
- "@babel/types": "^7.11.0",
- "lodash": "^4.17.19"
- }
- },
- "@babel/helper-optimise-call-expression": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-optimise-call-expression/download/@babel/helper-optimise-call-expression-7.10.4.tgz?cache=0&sync_timestamp=1593521296446&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-optimise-call-expression%2Fdownload%2F%40babel%2Fhelper-optimise-call-expression-7.10.4.tgz",
- "integrity": "sha1-UNyWQT1ZT5lad5BZBbBYk813lnM=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-plugin-utils": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-plugin-utils/download/@babel/helper-plugin-utils-7.10.4.tgz?cache=0&sync_timestamp=1593521082372&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-plugin-utils%2Fdownload%2F%40babel%2Fhelper-plugin-utils-7.10.4.tgz",
- "integrity": "sha1-L3WoMSadT2d95JmG3/WZJ1M883U=",
- "dev": true
- },
- "@babel/helper-regex": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-regex/download/@babel/helper-regex-7.10.5.tgz?cache=0&sync_timestamp=1594750677873&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-regex%2Fdownload%2F%40babel%2Fhelper-regex-7.10.5.tgz",
- "integrity": "sha1-Mt+7eYmQc8QVVXBToZvQVarlCuA=",
- "dev": true,
- "requires": {
- "lodash": "^4.17.19"
- }
- },
- "@babel/helper-remap-async-to-generator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-remap-async-to-generator/download/@babel/helper-remap-async-to-generator-7.10.4.tgz?cache=0&sync_timestamp=1593522966172&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-remap-async-to-generator%2Fdownload%2F%40babel%2Fhelper-remap-async-to-generator-7.10.4.tgz",
- "integrity": "sha1-/Oi+pOlpC76SMFbe0h5UtOi2jtU=",
- "dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.10.4",
- "@babel/helper-wrap-function": "^7.10.4",
- "@babel/template": "^7.10.4",
- "@babel/traverse": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-replace-supers": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-replace-supers/download/@babel/helper-replace-supers-7.10.4.tgz?cache=0&sync_timestamp=1593522959591&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-replace-supers%2Fdownload%2F%40babel%2Fhelper-replace-supers-7.10.4.tgz",
- "integrity": "sha1-1YXNk4jqBuYDHkzUS2cTy+rZ5s8=",
- "dev": true,
- "requires": {
- "@babel/helper-member-expression-to-functions": "^7.10.4",
- "@babel/helper-optimise-call-expression": "^7.10.4",
- "@babel/traverse": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-simple-access": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-simple-access/download/@babel/helper-simple-access-7.10.4.tgz?cache=0&sync_timestamp=1593522975802&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-simple-access%2Fdownload%2F%40babel%2Fhelper-simple-access-7.10.4.tgz",
- "integrity": "sha1-D1zNopRSd6KnotOoIeFTle3PNGE=",
- "dev": true,
- "requires": {
- "@babel/template": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-skip-transparent-expression-wrappers/download/@babel/helper-skip-transparent-expression-wrappers-7.11.0.tgz",
- "integrity": "sha1-7sFi8RLC9Y068K8SXju1dmUUZyk=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.11.0"
- }
- },
- "@babel/helper-split-export-declaration": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-split-export-declaration/download/@babel/helper-split-export-declaration-7.11.0.tgz",
- "integrity": "sha1-+KSRJErPamdhWKxCBykRuoOtCZ8=",
- "dev": true,
- "requires": {
- "@babel/types": "^7.11.0"
- }
- },
- "@babel/helper-validator-identifier": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-validator-identifier/download/@babel/helper-validator-identifier-7.10.4.tgz?cache=0&sync_timestamp=1593521083613&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-validator-identifier%2Fdownload%2F%40babel%2Fhelper-validator-identifier-7.10.4.tgz",
- "integrity": "sha1-p4x6clHgH2FlEtMbEK3PUq2l4NI="
- },
- "@babel/helper-wrap-function": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helper-wrap-function/download/@babel/helper-wrap-function-7.10.4.tgz?cache=0&sync_timestamp=1593522965325&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-wrap-function%2Fdownload%2F%40babel%2Fhelper-wrap-function-7.10.4.tgz",
- "integrity": "sha1-im9wHqsP8592W1oc/vQJmQ5iS4c=",
- "dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.10.4",
- "@babel/template": "^7.10.4",
- "@babel/traverse": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/helpers": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/helpers/download/@babel/helpers-7.10.4.tgz?cache=0&sync_timestamp=1593522959913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelpers%2Fdownload%2F%40babel%2Fhelpers-7.10.4.tgz",
- "integrity": "sha1-Kr6w1yGv98Cpc3a54fb2XXpHUEQ=",
- "dev": true,
- "requires": {
- "@babel/template": "^7.10.4",
- "@babel/traverse": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/highlight": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/highlight/download/@babel/highlight-7.10.4.tgz?cache=0&sync_timestamp=1593521095576&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhighlight%2Fdownload%2F%40babel%2Fhighlight-7.10.4.tgz",
- "integrity": "sha1-fRvf1ldTU4+r5sOFls23bZrGAUM=",
- "dev": true,
- "requires": {
- "@babel/helper-validator-identifier": "^7.10.4",
- "chalk": "^2.0.0",
- "js-tokens": "^4.0.0"
- }
- },
- "@babel/parser": {
- "version": "7.11.3",
- "resolved": "https://registry.npm.taobao.org/@babel/parser/download/@babel/parser-7.11.3.tgz?cache=0&sync_timestamp=1596915985899&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fparser%2Fdownload%2F%40babel%2Fparser-7.11.3.tgz",
- "integrity": "sha1-nh6uRnOLzQjiPoZ7q0PnuVKZqPk="
- },
- "@babel/plugin-proposal-async-generator-functions": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-async-generator-functions/download/@babel/plugin-proposal-async-generator-functions-7.10.5.tgz?cache=0&sync_timestamp=1594750682516&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-async-generator-functions%2Fdownload%2F%40babel%2Fplugin-proposal-async-generator-functions-7.10.5.tgz",
- "integrity": "sha1-NJHKvy98F5q4IGBs7Cf+0V4OhVg=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-remap-async-to-generator": "^7.10.4",
- "@babel/plugin-syntax-async-generators": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-class-properties": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-class-properties/download/@babel/plugin-proposal-class-properties-7.10.4.tgz?cache=0&sync_timestamp=1593522963242&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-class-properties%2Fdownload%2F%40babel%2Fplugin-proposal-class-properties-7.10.4.tgz",
- "integrity": "sha1-ozv2Mto5ClnHqMVwBF0RFc13iAc=",
- "dev": true,
- "requires": {
- "@babel/helper-create-class-features-plugin": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-decorators": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-decorators/download/@babel/plugin-proposal-decorators-7.10.5.tgz?cache=0&sync_timestamp=1594750722573&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-decorators%2Fdownload%2F%40babel%2Fplugin-proposal-decorators-7.10.5.tgz",
- "integrity": "sha1-QomLukeLxLGuJCpwOpU6etNQ/7Q=",
- "dev": true,
- "requires": {
- "@babel/helper-create-class-features-plugin": "^7.10.5",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-decorators": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-dynamic-import": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-dynamic-import/download/@babel/plugin-proposal-dynamic-import-7.10.4.tgz?cache=0&sync_timestamp=1593521093903&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-dynamic-import%2Fdownload%2F%40babel%2Fplugin-proposal-dynamic-import-7.10.4.tgz",
- "integrity": "sha1-uleibLmLN3QenVvKG4sN34KR8X4=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-dynamic-import": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-export-namespace-from": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-export-namespace-from/download/@babel/plugin-proposal-export-namespace-from-7.10.4.tgz",
- "integrity": "sha1-Vw2IO5EDFjez4pWO6jxDjmLAX1Q=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
- }
- },
- "@babel/plugin-proposal-json-strings": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-json-strings/download/@babel/plugin-proposal-json-strings-7.10.4.tgz?cache=0&sync_timestamp=1593521092651&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-json-strings%2Fdownload%2F%40babel%2Fplugin-proposal-json-strings-7.10.4.tgz",
- "integrity": "sha1-WT5ZxjUoFgIzvTIbGuvgggwjQds=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-json-strings": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-logical-assignment-operators": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-logical-assignment-operators/download/@babel/plugin-proposal-logical-assignment-operators-7.11.0.tgz",
- "integrity": "sha1-n4DkgsAwg8hxJd7hACa1hSfqIMg=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-nullish-coalescing-operator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-nullish-coalescing-operator/download/@babel/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz?cache=0&sync_timestamp=1593521087263&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-nullish-coalescing-operator%2Fdownload%2F%40babel%2Fplugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
- "integrity": "sha1-AqfpYfwy5tWy2wZJ4Bv4Dd7n4Eo=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-numeric-separator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-numeric-separator/download/@babel/plugin-proposal-numeric-separator-7.10.4.tgz?cache=0&sync_timestamp=1593522970329&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-numeric-separator%2Fdownload%2F%40babel%2Fplugin-proposal-numeric-separator-7.10.4.tgz",
- "integrity": "sha1-zhWQ/wplrRKXCmCdeIVemkwa7wY=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-object-rest-spread": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-object-rest-spread/download/@babel/plugin-proposal-object-rest-spread-7.11.0.tgz",
- "integrity": "sha1-vYH5Wh90Z2DqQ7bC09YrEXkK0K8=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
- "@babel/plugin-transform-parameters": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-optional-catch-binding": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-optional-catch-binding/download/@babel/plugin-proposal-optional-catch-binding-7.10.4.tgz?cache=0&sync_timestamp=1593521089386&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-optional-catch-binding%2Fdownload%2F%40babel%2Fplugin-proposal-optional-catch-binding-7.10.4.tgz",
- "integrity": "sha1-Mck4MJ0kp4pJ1o/av/qoY3WFVN0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-optional-catch-binding": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-optional-chaining": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-optional-chaining/download/@babel/plugin-proposal-optional-chaining-7.11.0.tgz",
- "integrity": "sha1-3lhm0GRvav2quKVmOC/joiF1UHY=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0",
- "@babel/plugin-syntax-optional-chaining": "^7.8.0"
- }
- },
- "@babel/plugin-proposal-private-methods": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-private-methods/download/@babel/plugin-proposal-private-methods-7.10.4.tgz?cache=0&sync_timestamp=1593522970831&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-private-methods%2Fdownload%2F%40babel%2Fplugin-proposal-private-methods-7.10.4.tgz",
- "integrity": "sha1-sWDZcrj9ulx9ERoUX8jEIfwqaQk=",
- "dev": true,
- "requires": {
- "@babel/helper-create-class-features-plugin": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-proposal-unicode-property-regex": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-proposal-unicode-property-regex/download/@babel/plugin-proposal-unicode-property-regex-7.10.4.tgz?cache=0&sync_timestamp=1593522975170&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-proposal-unicode-property-regex%2Fdownload%2F%40babel%2Fplugin-proposal-unicode-property-regex-7.10.4.tgz",
- "integrity": "sha1-RIPNpTBBzjQTt/4vAAImZd36p10=",
- "dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-async-generators": {
- "version": "7.8.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-async-generators/download/@babel/plugin-syntax-async-generators-7.8.4.tgz",
- "integrity": "sha1-qYP7Gusuw/btBCohD2QOkOeG/g0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-class-properties": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-class-properties/download/@babel/plugin-syntax-class-properties-7.10.4.tgz?cache=0&sync_timestamp=1593521086484&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-syntax-class-properties%2Fdownload%2F%40babel%2Fplugin-syntax-class-properties-7.10.4.tgz",
- "integrity": "sha1-ZkTmoLqlWmH54yMfbJ7rbuRsEkw=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-decorators": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-decorators/download/@babel/plugin-syntax-decorators-7.10.4.tgz?cache=0&sync_timestamp=1593521097781&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-syntax-decorators%2Fdownload%2F%40babel%2Fplugin-syntax-decorators-7.10.4.tgz",
- "integrity": "sha1-aFMIWyxCn50yLQL1pjUBjN6yNgw=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-dynamic-import": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-dynamic-import/download/@babel/plugin-syntax-dynamic-import-7.8.3.tgz",
- "integrity": "sha1-Yr+Ysto80h1iYVT8lu5bPLaOrLM=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-export-namespace-from": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-export-namespace-from/download/@babel/plugin-syntax-export-namespace-from-7.8.3.tgz",
- "integrity": "sha1-AolkqbqA28CUyRXEh618TnpmRlo=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.3"
- }
- },
- "@babel/plugin-syntax-json-strings": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-json-strings/download/@babel/plugin-syntax-json-strings-7.8.3.tgz",
- "integrity": "sha1-AcohtmjNghjJ5kDLbdiMVBKyyWo=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-jsx": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-jsx/download/@babel/plugin-syntax-jsx-7.10.4.tgz",
- "integrity": "sha1-Oauq48v3EMQ3PYQpSE5rohNAFmw=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-logical-assignment-operators": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-logical-assignment-operators/download/@babel/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
- "integrity": "sha1-ypHvRjA1MESLkGZSusLp/plB9pk=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-nullish-coalescing-operator": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-nullish-coalescing-operator/download/@babel/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
- "integrity": "sha1-Fn7XA2iIYIH3S1w2xlqIwDtm0ak=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-numeric-separator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-numeric-separator/download/@babel/plugin-syntax-numeric-separator-7.10.4.tgz?cache=0&sync_timestamp=1593521788128&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-syntax-numeric-separator%2Fdownload%2F%40babel%2Fplugin-syntax-numeric-separator-7.10.4.tgz",
- "integrity": "sha1-ubBws+M1cM2f0Hun+pHA3Te5r5c=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-syntax-object-rest-spread": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-object-rest-spread/download/@babel/plugin-syntax-object-rest-spread-7.8.3.tgz",
- "integrity": "sha1-YOIl7cvZimQDMqLnLdPmbxr1WHE=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-optional-catch-binding": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-optional-catch-binding/download/@babel/plugin-syntax-optional-catch-binding-7.8.3.tgz",
- "integrity": "sha1-YRGiZbz7Ag6579D9/X0mQCue1sE=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-optional-chaining": {
- "version": "7.8.3",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-optional-chaining/download/@babel/plugin-syntax-optional-chaining-7.8.3.tgz",
- "integrity": "sha1-T2nCq5UWfgGAzVM2YT+MV4j31Io=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.8.0"
- }
- },
- "@babel/plugin-syntax-top-level-await": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-syntax-top-level-await/download/@babel/plugin-syntax-top-level-await-7.10.4.tgz",
- "integrity": "sha1-S764kXtU/PdoNk4KgfVg4zo+9X0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-arrow-functions": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-arrow-functions/download/@babel/plugin-transform-arrow-functions-7.10.4.tgz?cache=0&sync_timestamp=1593521290488&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-arrow-functions%2Fdownload%2F%40babel%2Fplugin-transform-arrow-functions-7.10.4.tgz",
- "integrity": "sha1-4ilg135pfHT0HFAdRNc9v4pqZM0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-async-to-generator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-async-to-generator/download/@babel/plugin-transform-async-to-generator-7.10.4.tgz?cache=0&sync_timestamp=1593522968362&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-async-to-generator%2Fdownload%2F%40babel%2Fplugin-transform-async-to-generator-7.10.4.tgz",
- "integrity": "sha1-QaUBfknrbzzak5KlHu8pQFskWjc=",
- "dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-remap-async-to-generator": "^7.10.4"
- }
- },
- "@babel/plugin-transform-block-scoped-functions": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-block-scoped-functions/download/@babel/plugin-transform-block-scoped-functions-7.10.4.tgz?cache=0&sync_timestamp=1593522071341&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-block-scoped-functions%2Fdownload%2F%40babel%2Fplugin-transform-block-scoped-functions-7.10.4.tgz",
- "integrity": "sha1-GvpZV0T3XkOpGvc7DZmOz+Trwug=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-block-scoping": {
- "version": "7.11.1",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-block-scoping/download/@babel/plugin-transform-block-scoping-7.11.1.tgz",
- "integrity": "sha1-W37+mIUr741lLAsoFEzZOp5LUhU=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-classes": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-classes/download/@babel/plugin-transform-classes-7.10.4.tgz?cache=0&sync_timestamp=1593522971188&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-classes%2Fdownload%2F%40babel%2Fplugin-transform-classes-7.10.4.tgz",
- "integrity": "sha1-QFE2rys+IYvEoZJiKLyRerGgrcc=",
- "dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.10.4",
- "@babel/helper-define-map": "^7.10.4",
- "@babel/helper-function-name": "^7.10.4",
- "@babel/helper-optimise-call-expression": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-replace-supers": "^7.10.4",
- "@babel/helper-split-export-declaration": "^7.10.4",
- "globals": "^11.1.0"
- }
- },
- "@babel/plugin-transform-computed-properties": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-computed-properties/download/@babel/plugin-transform-computed-properties-7.10.4.tgz?cache=0&sync_timestamp=1593521290771&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-computed-properties%2Fdownload%2F%40babel%2Fplugin-transform-computed-properties-7.10.4.tgz",
- "integrity": "sha1-ne2DqBboLe0o1S1LTsvdgQzfwOs=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-destructuring": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-destructuring/download/@babel/plugin-transform-destructuring-7.10.4.tgz?cache=0&sync_timestamp=1593521291443&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-destructuring%2Fdownload%2F%40babel%2Fplugin-transform-destructuring-7.10.4.tgz",
- "integrity": "sha1-cN3Ss9G+qD0BUJ6bsl3bOnT8heU=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-dotall-regex": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-dotall-regex/download/@babel/plugin-transform-dotall-regex-7.10.4.tgz?cache=0&sync_timestamp=1593522977820&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-dotall-regex%2Fdownload%2F%40babel%2Fplugin-transform-dotall-regex-7.10.4.tgz",
- "integrity": "sha1-RpwgYhBcHragQOr0+sS0iAeDle4=",
- "dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-duplicate-keys": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-duplicate-keys/download/@babel/plugin-transform-duplicate-keys-7.10.4.tgz",
- "integrity": "sha1-aX5Qyf7hQ4D+hD0fMGspVhdDHkc=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-exponentiation-operator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-exponentiation-operator/download/@babel/plugin-transform-exponentiation-operator-7.10.4.tgz?cache=0&sync_timestamp=1593522967206&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-exponentiation-operator%2Fdownload%2F%40babel%2Fplugin-transform-exponentiation-operator-7.10.4.tgz",
- "integrity": "sha1-WuM4xX+M9AAb2zVgeuZrktZlry4=",
- "dev": true,
- "requires": {
- "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-for-of": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-for-of/download/@babel/plugin-transform-for-of-7.10.4.tgz?cache=0&sync_timestamp=1593521291715&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-for-of%2Fdownload%2F%40babel%2Fplugin-transform-for-of-7.10.4.tgz",
- "integrity": "sha1-wIiS6IGdOl2ykDGxFa9RHbv+uuk=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-function-name": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-function-name/download/@babel/plugin-transform-function-name-7.10.4.tgz?cache=0&sync_timestamp=1593522961117&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-function-name%2Fdownload%2F%40babel%2Fplugin-transform-function-name-7.10.4.tgz",
- "integrity": "sha1-akZ4gOD8ljhRS6NpERgR3b4mRLc=",
- "dev": true,
- "requires": {
- "@babel/helper-function-name": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-literals": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-literals/download/@babel/plugin-transform-literals-7.10.4.tgz?cache=0&sync_timestamp=1593521291903&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-literals%2Fdownload%2F%40babel%2Fplugin-transform-literals-7.10.4.tgz",
- "integrity": "sha1-n0K6CEEQChNfInEtDjkcRi9XHzw=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-member-expression-literals": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-member-expression-literals/download/@babel/plugin-transform-member-expression-literals-7.10.4.tgz?cache=0&sync_timestamp=1593521293070&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-member-expression-literals%2Fdownload%2F%40babel%2Fplugin-transform-member-expression-literals-7.10.4.tgz",
- "integrity": "sha1-sexE/PGVr8uNssYs2OVRyIG6+Lc=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-modules-amd": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-modules-amd/download/@babel/plugin-transform-modules-amd-7.10.5.tgz?cache=0&sync_timestamp=1594750712546&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-modules-amd%2Fdownload%2F%40babel%2Fplugin-transform-modules-amd-7.10.5.tgz",
- "integrity": "sha1-G5zdrwXZ6Is6rTOcs+RFxPAgqbE=",
- "dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.10.5",
- "@babel/helper-plugin-utils": "^7.10.4",
- "babel-plugin-dynamic-import-node": "^2.3.3"
- }
- },
- "@babel/plugin-transform-modules-commonjs": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-modules-commonjs/download/@babel/plugin-transform-modules-commonjs-7.10.4.tgz?cache=0&sync_timestamp=1593522963909&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-modules-commonjs%2Fdownload%2F%40babel%2Fplugin-transform-modules-commonjs-7.10.4.tgz",
- "integrity": "sha1-ZmZ8Pu2h6/eJbUHx8WsXEFovvKA=",
- "dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-simple-access": "^7.10.4",
- "babel-plugin-dynamic-import-node": "^2.3.3"
- }
- },
- "@babel/plugin-transform-modules-systemjs": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-modules-systemjs/download/@babel/plugin-transform-modules-systemjs-7.10.5.tgz?cache=0&sync_timestamp=1594750707592&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-modules-systemjs%2Fdownload%2F%40babel%2Fplugin-transform-modules-systemjs-7.10.5.tgz",
- "integrity": "sha1-YnAJnIVAZmgbrp4F+H4bnK2+jIU=",
- "dev": true,
- "requires": {
- "@babel/helper-hoist-variables": "^7.10.4",
- "@babel/helper-module-transforms": "^7.10.5",
- "@babel/helper-plugin-utils": "^7.10.4",
- "babel-plugin-dynamic-import-node": "^2.3.3"
- }
- },
- "@babel/plugin-transform-modules-umd": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-modules-umd/download/@babel/plugin-transform-modules-umd-7.10.4.tgz?cache=0&sync_timestamp=1593522964232&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-modules-umd%2Fdownload%2F%40babel%2Fplugin-transform-modules-umd-7.10.4.tgz",
- "integrity": "sha1-moSB/oG4JGVLOgtl2j34nz0hg54=",
- "dev": true,
- "requires": {
- "@babel/helper-module-transforms": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-named-capturing-groups-regex/download/@babel/plugin-transform-named-capturing-groups-regex-7.10.4.tgz?cache=0&sync_timestamp=1593522978582&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-named-capturing-groups-regex%2Fdownload%2F%40babel%2Fplugin-transform-named-capturing-groups-regex-7.10.4.tgz",
- "integrity": "sha1-eLTZeIELbzvPA/njGPL8DtQa7LY=",
- "dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.10.4"
- }
- },
- "@babel/plugin-transform-new-target": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-new-target/download/@babel/plugin-transform-new-target-7.10.4.tgz?cache=0&sync_timestamp=1593521292141&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-new-target%2Fdownload%2F%40babel%2Fplugin-transform-new-target-7.10.4.tgz",
- "integrity": "sha1-kJfXU8t7Aky3OBo7LlLpUTqcaIg=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-object-super": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-object-super/download/@babel/plugin-transform-object-super-7.10.4.tgz?cache=0&sync_timestamp=1593522965761&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-object-super%2Fdownload%2F%40babel%2Fplugin-transform-object-super-7.10.4.tgz",
- "integrity": "sha1-1xRsTROUM+emUm+IjGZ+MUoJOJQ=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-replace-supers": "^7.10.4"
- }
- },
- "@babel/plugin-transform-parameters": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-parameters/download/@babel/plugin-transform-parameters-7.10.5.tgz?cache=0&sync_timestamp=1594750687789&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-parameters%2Fdownload%2F%40babel%2Fplugin-transform-parameters-7.10.5.tgz",
- "integrity": "sha1-WdM51Y0LGVBDX0BD504lEABeLEo=",
- "dev": true,
- "requires": {
- "@babel/helper-get-function-arity": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-property-literals": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-property-literals/download/@babel/plugin-transform-property-literals-7.10.4.tgz?cache=0&sync_timestamp=1593521293406&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-property-literals%2Fdownload%2F%40babel%2Fplugin-transform-property-literals-7.10.4.tgz",
- "integrity": "sha1-9v5UtlkDUimHhbg+3YFdIUxC48A=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-regenerator": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-regenerator/download/@babel/plugin-transform-regenerator-7.10.4.tgz?cache=0&sync_timestamp=1593521089707&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-regenerator%2Fdownload%2F%40babel%2Fplugin-transform-regenerator-7.10.4.tgz",
- "integrity": "sha1-IBXlnYOQdOdoON4hWdtCGWb9i2M=",
- "dev": true,
- "requires": {
- "regenerator-transform": "^0.14.2"
- }
- },
- "@babel/plugin-transform-reserved-words": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-reserved-words/download/@babel/plugin-transform-reserved-words-7.10.4.tgz?cache=0&sync_timestamp=1593522978219&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-reserved-words%2Fdownload%2F%40babel%2Fplugin-transform-reserved-words-7.10.4.tgz",
- "integrity": "sha1-jyaCvNzvntMn4bCGFYXXAT+KVN0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-runtime": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-runtime/download/@babel/plugin-transform-runtime-7.11.0.tgz",
- "integrity": "sha1-4n946zbxlEhjbgXDPJD9mtm4vM8=",
- "dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "resolve": "^1.8.1",
- "semver": "^5.5.1"
- }
- },
- "@babel/plugin-transform-shorthand-properties": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-shorthand-properties/download/@babel/plugin-transform-shorthand-properties-7.10.4.tgz?cache=0&sync_timestamp=1593521293679&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-shorthand-properties%2Fdownload%2F%40babel%2Fplugin-transform-shorthand-properties-7.10.4.tgz",
- "integrity": "sha1-n9Jexc3VVbt/Rz5ebuHJce7eTdY=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-spread": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-spread/download/@babel/plugin-transform-spread-7.11.0.tgz",
- "integrity": "sha1-+oTTAPXk9XdS/kGm0bPFVPE/F8w=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0"
- }
- },
- "@babel/plugin-transform-sticky-regex": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-sticky-regex/download/@babel/plugin-transform-sticky-regex-7.10.4.tgz?cache=0&sync_timestamp=1593521295131&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-sticky-regex%2Fdownload%2F%40babel%2Fplugin-transform-sticky-regex-7.10.4.tgz",
- "integrity": "sha1-jziJ7oZXWBEwop2cyR18c7fEoo0=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/helper-regex": "^7.10.4"
- }
- },
- "@babel/plugin-transform-template-literals": {
- "version": "7.10.5",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-template-literals/download/@babel/plugin-transform-template-literals-7.10.5.tgz?cache=0&sync_timestamp=1594750692589&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-template-literals%2Fdownload%2F%40babel%2Fplugin-transform-template-literals-7.10.5.tgz",
- "integrity": "sha1-eLxdYmpmQtszEtnQ8AH152Of3ow=",
- "dev": true,
- "requires": {
- "@babel/helper-annotate-as-pure": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-typeof-symbol": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-typeof-symbol/download/@babel/plugin-transform-typeof-symbol-7.10.4.tgz?cache=0&sync_timestamp=1593522969066&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-typeof-symbol%2Fdownload%2F%40babel%2Fplugin-transform-typeof-symbol-7.10.4.tgz",
- "integrity": "sha1-lQnxp+7DHE7b/+E3wWzDP/C8W/w=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-unicode-escapes": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-unicode-escapes/download/@babel/plugin-transform-unicode-escapes-7.10.4.tgz?cache=0&sync_timestamp=1593522967875&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-unicode-escapes%2Fdownload%2F%40babel%2Fplugin-transform-unicode-escapes-7.10.4.tgz",
- "integrity": "sha1-/q5SM5HHZR3awRXa4KnQaFeJIAc=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/plugin-transform-unicode-regex": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/plugin-transform-unicode-regex/download/@babel/plugin-transform-unicode-regex-7.10.4.tgz?cache=0&sync_timestamp=1593522975515&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fplugin-transform-unicode-regex%2Fdownload%2F%40babel%2Fplugin-transform-unicode-regex-7.10.4.tgz",
- "integrity": "sha1-5W1x+SgvrG2wnIJ0IFVXbV5tgKg=",
- "dev": true,
- "requires": {
- "@babel/helper-create-regexp-features-plugin": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4"
- }
- },
- "@babel/preset-env": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/preset-env/download/@babel/preset-env-7.11.0.tgz",
- "integrity": "sha1-hg7jjyzhetYEgMICG6lok5Pvt5Y=",
- "dev": true,
- "requires": {
- "@babel/compat-data": "^7.11.0",
- "@babel/helper-compilation-targets": "^7.10.4",
- "@babel/helper-module-imports": "^7.10.4",
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-proposal-async-generator-functions": "^7.10.4",
- "@babel/plugin-proposal-class-properties": "^7.10.4",
- "@babel/plugin-proposal-dynamic-import": "^7.10.4",
- "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
- "@babel/plugin-proposal-json-strings": "^7.10.4",
- "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0",
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
- "@babel/plugin-proposal-numeric-separator": "^7.10.4",
- "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
- "@babel/plugin-proposal-optional-catch-binding": "^7.10.4",
- "@babel/plugin-proposal-optional-chaining": "^7.11.0",
- "@babel/plugin-proposal-private-methods": "^7.10.4",
- "@babel/plugin-proposal-unicode-property-regex": "^7.10.4",
- "@babel/plugin-syntax-async-generators": "^7.8.0",
- "@babel/plugin-syntax-class-properties": "^7.10.4",
- "@babel/plugin-syntax-dynamic-import": "^7.8.0",
- "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
- "@babel/plugin-syntax-json-strings": "^7.8.0",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
- "@babel/plugin-syntax-optional-catch-binding": "^7.8.0",
- "@babel/plugin-syntax-optional-chaining": "^7.8.0",
- "@babel/plugin-syntax-top-level-await": "^7.10.4",
- "@babel/plugin-transform-arrow-functions": "^7.10.4",
- "@babel/plugin-transform-async-to-generator": "^7.10.4",
- "@babel/plugin-transform-block-scoped-functions": "^7.10.4",
- "@babel/plugin-transform-block-scoping": "^7.10.4",
- "@babel/plugin-transform-classes": "^7.10.4",
- "@babel/plugin-transform-computed-properties": "^7.10.4",
- "@babel/plugin-transform-destructuring": "^7.10.4",
- "@babel/plugin-transform-dotall-regex": "^7.10.4",
- "@babel/plugin-transform-duplicate-keys": "^7.10.4",
- "@babel/plugin-transform-exponentiation-operator": "^7.10.4",
- "@babel/plugin-transform-for-of": "^7.10.4",
- "@babel/plugin-transform-function-name": "^7.10.4",
- "@babel/plugin-transform-literals": "^7.10.4",
- "@babel/plugin-transform-member-expression-literals": "^7.10.4",
- "@babel/plugin-transform-modules-amd": "^7.10.4",
- "@babel/plugin-transform-modules-commonjs": "^7.10.4",
- "@babel/plugin-transform-modules-systemjs": "^7.10.4",
- "@babel/plugin-transform-modules-umd": "^7.10.4",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4",
- "@babel/plugin-transform-new-target": "^7.10.4",
- "@babel/plugin-transform-object-super": "^7.10.4",
- "@babel/plugin-transform-parameters": "^7.10.4",
- "@babel/plugin-transform-property-literals": "^7.10.4",
- "@babel/plugin-transform-regenerator": "^7.10.4",
- "@babel/plugin-transform-reserved-words": "^7.10.4",
- "@babel/plugin-transform-shorthand-properties": "^7.10.4",
- "@babel/plugin-transform-spread": "^7.11.0",
- "@babel/plugin-transform-sticky-regex": "^7.10.4",
- "@babel/plugin-transform-template-literals": "^7.10.4",
- "@babel/plugin-transform-typeof-symbol": "^7.10.4",
- "@babel/plugin-transform-unicode-escapes": "^7.10.4",
- "@babel/plugin-transform-unicode-regex": "^7.10.4",
- "@babel/preset-modules": "^0.1.3",
- "@babel/types": "^7.11.0",
- "browserslist": "^4.12.0",
- "core-js-compat": "^3.6.2",
- "invariant": "^2.2.2",
- "levenary": "^1.1.1",
- "semver": "^5.5.0"
- }
- },
- "@babel/preset-modules": {
- "version": "0.1.3",
- "resolved": "https://registry.npm.taobao.org/@babel/preset-modules/download/@babel/preset-modules-0.1.3.tgz",
- "integrity": "sha1-EyQrU7XvjIg8PPfd3VWzbOgPvHI=",
- "dev": true,
- "requires": {
- "@babel/helper-plugin-utils": "^7.0.0",
- "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
- "@babel/plugin-transform-dotall-regex": "^7.4.4",
- "@babel/types": "^7.4.4",
- "esutils": "^2.0.2"
- }
- },
- "@babel/runtime": {
- "version": "7.11.2",
- "resolved": "https://registry.npm.taobao.org/@babel/runtime/download/@babel/runtime-7.11.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.11.2.tgz",
- "integrity": "sha1-9UnBPHVMxAuHZEufqfCaapX+BzY=",
- "dev": true,
- "requires": {
- "regenerator-runtime": "^0.13.4"
- }
- },
- "@babel/template": {
- "version": "7.10.4",
- "resolved": "https://registry.npm.taobao.org/@babel/template/download/@babel/template-7.10.4.tgz",
- "integrity": "sha1-MlGZbEIA68cdGo/EBfupQPNrong=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.10.4",
- "@babel/parser": "^7.10.4",
- "@babel/types": "^7.10.4"
- }
- },
- "@babel/traverse": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/traverse/download/@babel/traverse-7.11.0.tgz",
- "integrity": "sha1-m5ls4bmPU/fD5BdRFWBdVu0H3SQ=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.11.0",
- "@babel/helper-function-name": "^7.10.4",
- "@babel/helper-split-export-declaration": "^7.11.0",
- "@babel/parser": "^7.11.0",
- "@babel/types": "^7.11.0",
- "debug": "^4.1.0",
- "globals": "^11.1.0",
- "lodash": "^4.17.19"
- }
- },
- "@babel/types": {
- "version": "7.11.0",
- "resolved": "https://registry.npm.taobao.org/@babel/types/download/@babel/types-7.11.0.tgz",
- "integrity": "sha1-Kua/G6mujDxDgk5YYSaYcbIG6Q0=",
- "requires": {
- "@babel/helper-validator-identifier": "^7.10.4",
- "lodash": "^4.17.19",
- "to-fast-properties": "^2.0.0"
- }
- },
- "@hapi/address": {
- "version": "2.1.4",
- "resolved": "https://registry.npm.taobao.org/@hapi/address/download/@hapi/address-2.1.4.tgz?cache=0&sync_timestamp=1593993895205&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40hapi%2Faddress%2Fdownload%2F%40hapi%2Faddress-2.1.4.tgz",
- "integrity": "sha1-XWftQ/P9QaadS5/3tW58DR0KgeU=",
- "dev": true
- },
- "@hapi/bourne": {
- "version": "1.3.2",
- "resolved": "https://registry.npm.taobao.org/@hapi/bourne/download/@hapi/bourne-1.3.2.tgz?cache=0&sync_timestamp=1593915150444&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40hapi%2Fbourne%2Fdownload%2F%40hapi%2Fbourne-1.3.2.tgz",
- "integrity": "sha1-CnCVreoGckPOMoPhtWuKj0U7JCo=",
- "dev": true
- },
- "@hapi/hoek": {
- "version": "8.5.1",
- "resolved": "https://registry.npm.taobao.org/@hapi/hoek/download/@hapi/hoek-8.5.1.tgz?cache=0&sync_timestamp=1596229985980&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40hapi%2Fhoek%2Fdownload%2F%40hapi%2Fhoek-8.5.1.tgz",
- "integrity": "sha1-/elgZMpEbeyMVajC8TCVewcMbgY=",
- "dev": true
- },
- "@hapi/joi": {
- "version": "15.1.1",
- "resolved": "https://registry.npm.taobao.org/@hapi/joi/download/@hapi/joi-15.1.1.tgz",
- "integrity": "sha1-xnW4pxKW8Cgz+NbSQ7NMV7jOGdc=",
- "dev": true,
- "requires": {
- "@hapi/address": "2.x.x",
- "@hapi/bourne": "1.x.x",
- "@hapi/hoek": "8.x.x",
- "@hapi/topo": "3.x.x"
- }
- },
- "@hapi/topo": {
- "version": "3.1.6",
- "resolved": "https://registry.npm.taobao.org/@hapi/topo/download/@hapi/topo-3.1.6.tgz?cache=0&sync_timestamp=1593916080558&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40hapi%2Ftopo%2Fdownload%2F%40hapi%2Ftopo-3.1.6.tgz",
- "integrity": "sha1-aNk1+j6uf91asNf5U/MgXYsr/Ck=",
- "dev": true,
- "requires": {
- "@hapi/hoek": "^8.3.0"
- }
- },
- "@intervolga/optimize-cssnano-plugin": {
- "version": "1.0.6",
- "resolved": "https://registry.npm.taobao.org/@intervolga/optimize-cssnano-plugin/download/@intervolga/optimize-cssnano-plugin-1.0.6.tgz",
- "integrity": "sha1-vnx4RhKLiPapsdEmGgrQbrXA/fg=",
- "dev": true,
- "requires": {
- "cssnano": "^4.0.0",
- "cssnano-preset-default": "^4.0.0",
- "postcss": "^7.0.0"
- }
- },
- "@mrmlnc/readdir-enhanced": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/@mrmlnc/readdir-enhanced/download/@mrmlnc/readdir-enhanced-2.2.1.tgz",
- "integrity": "sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=",
- "dev": true,
- "requires": {
- "call-me-maybe": "^1.0.1",
- "glob-to-regexp": "^0.3.0"
- }
- },
- "@nodelib/fs.stat": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/@nodelib/fs.stat/download/@nodelib/fs.stat-1.1.3.tgz",
- "integrity": "sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs=",
- "dev": true
- },
- "@soda/friendly-errors-webpack-plugin": {
- "version": "1.7.1",
- "resolved": "https://registry.npm.taobao.org/@soda/friendly-errors-webpack-plugin/download/@soda/friendly-errors-webpack-plugin-1.7.1.tgz",
- "integrity": "sha1-cG9kvLSouWQrSK46zkRMcDNNYV0=",
- "dev": true,
- "requires": {
- "chalk": "^1.1.3",
- "error-stack-parser": "^2.0.0",
- "string-width": "^2.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
- "requires": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
- }
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-2.1.1.tgz",
- "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-4.0.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
- }
- }
- },
- "@soda/get-current-script": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/@soda/get-current-script/download/@soda/get-current-script-1.0.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40soda%2Fget-current-script%2Fdownload%2F%40soda%2Fget-current-script-1.0.2.tgz",
- "integrity": "sha1-pTUV2yXYA4N0OBtzryC7Ty5QjYc=",
- "dev": true
- },
- "@types/anymatch": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/@types/anymatch/download/@types/anymatch-1.3.1.tgz",
- "integrity": "sha1-M2utwb7sudrMOL6izzKt9ieoQho=",
- "dev": true
- },
- "@types/body-parser": {
- "version": "1.19.0",
- "resolved": "https://registry.npm.taobao.org/@types/body-parser/download/@types/body-parser-1.19.0.tgz",
- "integrity": "sha1-BoWzxH6zAG/+0RfN1VFkth+AU48=",
- "dev": true,
- "requires": {
- "@types/connect": "*",
- "@types/node": "*"
- }
- },
- "@types/color-name": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/@types/color-name/download/@types/color-name-1.1.1.tgz",
- "integrity": "sha1-HBJhu+qhCoBVu8XYq4S3sq/IRqA=",
- "dev": true
- },
- "@types/connect": {
- "version": "3.4.33",
- "resolved": "https://registry.npm.taobao.org/@types/connect/download/@types/connect-3.4.33.tgz?cache=0&sync_timestamp=1596837850490&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fconnect%2Fdownload%2F%40types%2Fconnect-3.4.33.tgz",
- "integrity": "sha1-MWEMkB7KVzuHE8MzCrxua59YhUY=",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/connect-history-api-fallback": {
- "version": "1.3.3",
- "resolved": "https://registry.npm.taobao.org/@types/connect-history-api-fallback/download/@types/connect-history-api-fallback-1.3.3.tgz",
- "integrity": "sha1-R3K3m4tTGF8PTJ3qsJI2uvdu47Q=",
- "dev": true,
- "requires": {
- "@types/express-serve-static-core": "*",
- "@types/node": "*"
- }
- },
- "@types/express": {
- "version": "4.17.7",
- "resolved": "https://registry.npm.taobao.org/@types/express/download/@types/express-4.17.7.tgz",
- "integrity": "sha1-QgRb5kdWNtmAE2nNRBjvZc2w3Vk=",
- "dev": true,
- "requires": {
- "@types/body-parser": "*",
- "@types/express-serve-static-core": "*",
- "@types/qs": "*",
- "@types/serve-static": "*"
- }
- },
- "@types/express-serve-static-core": {
- "version": "4.17.9",
- "resolved": "https://registry.npm.taobao.org/@types/express-serve-static-core/download/@types/express-serve-static-core-4.17.9.tgz",
- "integrity": "sha1-LXs03P0l7GY8JchddmCPiySWZ/E=",
- "dev": true,
- "requires": {
- "@types/node": "*",
- "@types/qs": "*",
- "@types/range-parser": "*"
- }
- },
- "@types/glob": {
- "version": "7.1.3",
- "resolved": "https://registry.npm.taobao.org/@types/glob/download/@types/glob-7.1.3.tgz?cache=0&sync_timestamp=1596838206290&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fglob%2Fdownload%2F%40types%2Fglob-7.1.3.tgz",
- "integrity": "sha1-5rqA82t9qtLGhazZJmOC5omFwYM=",
- "dev": true,
- "requires": {
- "@types/minimatch": "*",
- "@types/node": "*"
- }
- },
- "@types/http-proxy": {
- "version": "1.17.4",
- "resolved": "https://registry.npm.taobao.org/@types/http-proxy/download/@types/http-proxy-1.17.4.tgz?cache=0&sync_timestamp=1596839386031&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fhttp-proxy%2Fdownload%2F%40types%2Fhttp-proxy-1.17.4.tgz",
- "integrity": "sha1-58kuPb4+E6p5lED/QubToXqdBFs=",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/http-proxy-middleware": {
- "version": "0.19.3",
- "resolved": "https://registry.npm.taobao.org/@types/http-proxy-middleware/download/@types/http-proxy-middleware-0.19.3.tgz?cache=0&sync_timestamp=1596839386511&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fhttp-proxy-middleware%2Fdownload%2F%40types%2Fhttp-proxy-middleware-0.19.3.tgz",
- "integrity": "sha1-suuW+8D5rHJQtdnExTqt4ElJfQM=",
- "dev": true,
- "requires": {
- "@types/connect": "*",
- "@types/http-proxy": "*",
- "@types/node": "*"
- }
- },
- "@types/json-schema": {
- "version": "7.0.5",
- "resolved": "https://registry.npm.taobao.org/@types/json-schema/download/@types/json-schema-7.0.5.tgz?cache=0&sync_timestamp=1596838729190&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fjson-schema%2Fdownload%2F%40types%2Fjson-schema-7.0.5.tgz",
- "integrity": "sha1-3M5EMOZLRDuolF8CkPtWStW6xt0=",
- "dev": true
- },
- "@types/mime": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/@types/mime/download/@types/mime-2.0.3.tgz",
- "integrity": "sha1-yJO3NyHbc2mZQ7/DZTsd63+qSjo=",
- "dev": true
- },
- "@types/mini-css-extract-plugin": {
- "version": "0.9.1",
- "resolved": "https://registry.npm.taobao.org/@types/mini-css-extract-plugin/download/@types/mini-css-extract-plugin-0.9.1.tgz?cache=0&sync_timestamp=1596839014743&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fmini-css-extract-plugin%2Fdownload%2F%40types%2Fmini-css-extract-plugin-0.9.1.tgz",
- "integrity": "sha1-1L3eUZcyb8oDnUGPS92gPcdNxFE=",
- "dev": true,
- "optional": true,
- "requires": {
- "@types/webpack": "*"
- }
- },
- "@types/minimatch": {
- "version": "3.0.3",
- "resolved": "https://registry.npm.taobao.org/@types/minimatch/download/@types/minimatch-3.0.3.tgz?cache=0&sync_timestamp=1596839141589&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fminimatch%2Fdownload%2F%40types%2Fminimatch-3.0.3.tgz",
- "integrity": "sha1-PcoOPzOyAPx9ETnAzZbBJoyt/Z0=",
- "dev": true
- },
- "@types/minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/@types/minimist/download/@types/minimist-1.2.0.tgz",
- "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
- "dev": true
- },
- "@types/node": {
- "version": "14.6.0",
- "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.6.0.tgz?cache=0&sync_timestamp=1597674929436&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.6.0.tgz",
- "integrity": "sha1-fUQRv1FXM5M318/4ZNn/RfF3tJk=",
- "dev": true
- },
- "@types/normalize-package-data": {
- "version": "2.4.0",
- "resolved": "https://registry.npm.taobao.org/@types/normalize-package-data/download/@types/normalize-package-data-2.4.0.tgz?cache=0&sync_timestamp=1596839391651&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnormalize-package-data%2Fdownload%2F%40types%2Fnormalize-package-data-2.4.0.tgz",
- "integrity": "sha1-5IbQ2XOW15vu3QpuM/RTT/a0lz4=",
- "dev": true
- },
- "@types/q": {
- "version": "1.5.4",
- "resolved": "https://registry.npm.taobao.org/@types/q/download/@types/q-1.5.4.tgz",
- "integrity": "sha1-FZJUFOCtLNdlv+9YhC9+JqesyyQ=",
- "dev": true
- },
- "@types/qs": {
- "version": "6.9.4",
- "resolved": "https://registry.npm.taobao.org/@types/qs/download/@types/qs-6.9.4.tgz",
- "integrity": "sha1-pZ6FHBuhbAUT6hI4MN1jmgoVy2o=",
- "dev": true
- },
- "@types/range-parser": {
- "version": "1.2.3",
- "resolved": "https://registry.npm.taobao.org/@types/range-parser/download/@types/range-parser-1.2.3.tgz",
- "integrity": "sha1-fuMwunyq+5gJC+zoal7kQRWQTCw=",
- "dev": true
- },
- "@types/serve-static": {
- "version": "1.13.5",
- "resolved": "https://registry.npm.taobao.org/@types/serve-static/download/@types/serve-static-1.13.5.tgz?cache=0&sync_timestamp=1596840339942&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fserve-static%2Fdownload%2F%40types%2Fserve-static-1.13.5.tgz",
- "integrity": "sha1-PSXZQaGEFdOrCS3vhG4TWgi7z1M=",
- "dev": true,
- "requires": {
- "@types/express-serve-static-core": "*",
- "@types/mime": "*"
- }
- },
- "@types/source-list-map": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fsource-list-map%2Fdownload%2F%40types%2Fsource-list-map-0.1.2.tgz",
- "integrity": "sha1-AHiDYGP/rxdBI0m7o2QIfgrALsk=",
- "dev": true
- },
- "@types/tapable": {
- "version": "1.0.6",
- "resolved": "https://registry.npm.taobao.org/@types/tapable/download/@types/tapable-1.0.6.tgz",
- "integrity": "sha1-qcpLcKGLJwzLK8Cqr+/R1Ia36nQ=",
- "dev": true
- },
- "@types/uglify-js": {
- "version": "3.9.3",
- "resolved": "https://registry.npm.taobao.org/@types/uglify-js/download/@types/uglify-js-3.9.3.tgz?cache=0&sync_timestamp=1596841575547&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fuglify-js%2Fdownload%2F%40types%2Fuglify-js-3.9.3.tgz",
- "integrity": "sha1-2U7WCOKVvFQkyWAOa4VlQHtrS2s=",
- "dev": true,
- "requires": {
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "@types/webpack": {
- "version": "4.41.21",
- "resolved": "https://registry.npm.taobao.org/@types/webpack/download/@types/webpack-4.41.21.tgz?cache=0&sync_timestamp=1596841577093&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack%2Fdownload%2F%40types%2Fwebpack-4.41.21.tgz",
- "integrity": "sha1-zGhbMywz8VO7L1/B+jrIretZLe4=",
- "dev": true,
- "requires": {
- "@types/anymatch": "*",
- "@types/node": "*",
- "@types/tapable": "*",
- "@types/uglify-js": "*",
- "@types/webpack-sources": "*",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "@types/webpack-dev-server": {
- "version": "3.11.0",
- "resolved": "https://registry.npm.taobao.org/@types/webpack-dev-server/download/@types/webpack-dev-server-3.11.0.tgz?cache=0&sync_timestamp=1596841577349&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack-dev-server%2Fdownload%2F%40types%2Fwebpack-dev-server-3.11.0.tgz",
- "integrity": "sha1-vMO4Xn3GrC2yUzBhBRPyIowvz7I=",
- "dev": true,
- "requires": {
- "@types/connect-history-api-fallback": "*",
- "@types/express": "*",
- "@types/http-proxy-middleware": "*",
- "@types/serve-static": "*",
- "@types/webpack": "*"
- }
- },
- "@types/webpack-sources": {
- "version": "1.4.2",
- "resolved": "https://registry.npm.taobao.org/@types/webpack-sources/download/@types/webpack-sources-1.4.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fwebpack-sources%2Fdownload%2F%40types%2Fwebpack-sources-1.4.2.tgz",
- "integrity": "sha1-XT1N6gQAineakBNf+W+1wMnmKSw=",
- "dev": true,
- "requires": {
- "@types/node": "*",
- "@types/source-list-map": "*",
- "source-map": "^0.7.3"
- },
- "dependencies": {
- "source-map": {
- "version": "0.7.3",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.7.3.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.7.3.tgz",
- "integrity": "sha1-UwL4FpAxc1ImVECS5kmB91F1A4M=",
- "dev": true
- }
- }
- },
- "@vue/babel-helper-vue-jsx-merge-props": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-helper-vue-jsx-merge-props/download/@vue/babel-helper-vue-jsx-merge-props-1.0.0.tgz?cache=0&sync_timestamp=1596768129236&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fbabel-helper-vue-jsx-merge-props%2Fdownload%2F%40vue%2Fbabel-helper-vue-jsx-merge-props-1.0.0.tgz",
- "integrity": "sha1-BI/leZWNpAj7eosqPsBQtQpmEEA=",
- "dev": true
- },
- "@vue/babel-plugin-transform-vue-jsx": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-plugin-transform-vue-jsx/download/@vue/babel-plugin-transform-vue-jsx-1.1.2.tgz",
- "integrity": "sha1-wKPm78Ai515CR7RIqPxrhvA+kcA=",
- "dev": true,
- "requires": {
- "@babel/helper-module-imports": "^7.0.0",
- "@babel/plugin-syntax-jsx": "^7.2.0",
- "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
- "html-tags": "^2.0.0",
- "lodash.kebabcase": "^4.1.1",
- "svg-tags": "^1.0.0"
- },
- "dependencies": {
- "html-tags": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/html-tags/download/html-tags-2.0.0.tgz",
- "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=",
- "dev": true
- }
- }
- },
- "@vue/babel-preset-app": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-preset-app/download/@vue/babel-preset-app-4.5.4.tgz",
- "integrity": "sha1-uxZOirVWc8Vh5ug1EWMe2hnv1+Q=",
- "dev": true,
- "requires": {
- "@ant-design-vue/babel-plugin-jsx": "^1.0.0-0",
- "@babel/core": "^7.11.0",
- "@babel/helper-compilation-targets": "^7.9.6",
- "@babel/helper-module-imports": "^7.8.3",
- "@babel/plugin-proposal-class-properties": "^7.8.3",
- "@babel/plugin-proposal-decorators": "^7.8.3",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-jsx": "^7.8.3",
- "@babel/plugin-transform-runtime": "^7.11.0",
- "@babel/preset-env": "^7.11.0",
- "@babel/runtime": "^7.11.0",
- "@vue/babel-preset-jsx": "^1.1.2",
- "babel-plugin-dynamic-import-node": "^2.3.3",
- "core-js": "^3.6.5",
- "core-js-compat": "^3.6.5",
- "semver": "^6.1.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- }
- }
- },
- "@vue/babel-preset-jsx": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-preset-jsx/download/@vue/babel-preset-jsx-1.1.2.tgz",
- "integrity": "sha1-LhaetMIE6jfKZsLqhaiAv8mdTyA=",
- "dev": true,
- "requires": {
- "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
- "@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
- "@vue/babel-sugar-functional-vue": "^1.1.2",
- "@vue/babel-sugar-inject-h": "^1.1.2",
- "@vue/babel-sugar-v-model": "^1.1.2",
- "@vue/babel-sugar-v-on": "^1.1.2"
- }
- },
- "@vue/babel-sugar-functional-vue": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-sugar-functional-vue/download/@vue/babel-sugar-functional-vue-1.1.2.tgz",
- "integrity": "sha1-9+JPugnm8e5wEEVgqICAV1VfGpo=",
- "dev": true,
- "requires": {
- "@babel/plugin-syntax-jsx": "^7.2.0"
- }
- },
- "@vue/babel-sugar-inject-h": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-sugar-inject-h/download/@vue/babel-sugar-inject-h-1.1.2.tgz",
- "integrity": "sha1-ilJ2ttji7Rb/yAeKrZQjYnTm7fA=",
- "dev": true,
- "requires": {
- "@babel/plugin-syntax-jsx": "^7.2.0"
- }
- },
- "@vue/babel-sugar-v-model": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-sugar-v-model/download/@vue/babel-sugar-v-model-1.1.2.tgz",
- "integrity": "sha1-H/b9G4ACI/ycsehNzrXlLXN6gZI=",
- "dev": true,
- "requires": {
- "@babel/plugin-syntax-jsx": "^7.2.0",
- "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
- "@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
- "camelcase": "^5.0.0",
- "html-tags": "^2.0.0",
- "svg-tags": "^1.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-5.3.1.tgz",
- "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=",
- "dev": true
- },
- "html-tags": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/html-tags/download/html-tags-2.0.0.tgz",
- "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=",
- "dev": true
- }
- }
- },
- "@vue/babel-sugar-v-on": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/babel-sugar-v-on/download/@vue/babel-sugar-v-on-1.1.2.tgz",
- "integrity": "sha1-su+ZuPL6sJ++rSWq1w70Lhz1sTs=",
- "dev": true,
- "requires": {
- "@babel/plugin-syntax-jsx": "^7.2.0",
- "@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
- "camelcase": "^5.0.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-5.3.1.tgz",
- "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=",
- "dev": true
- }
- }
- },
- "@vue/cli-overlay": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-overlay/download/@vue/cli-overlay-4.5.4.tgz?cache=0&sync_timestamp=1597717221287&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcli-overlay%2Fdownload%2F%40vue%2Fcli-overlay-4.5.4.tgz",
- "integrity": "sha1-4H48zC5Ndw1P29Rc3ed31ZKCLBk=",
- "dev": true
- },
- "@vue/cli-plugin-babel": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-plugin-babel/download/@vue/cli-plugin-babel-4.5.4.tgz",
- "integrity": "sha1-oBzcs9RgZGdd2I1htkCtrcyFHis=",
- "dev": true,
- "requires": {
- "@babel/core": "^7.11.0",
- "@vue/babel-preset-app": "^4.5.4",
- "@vue/cli-shared-utils": "^4.5.4",
- "babel-loader": "^8.1.0",
- "cache-loader": "^4.1.0",
- "thread-loader": "^2.1.3",
- "webpack": "^4.0.0"
- }
- },
- "@vue/cli-plugin-eslint": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-plugin-eslint/download/@vue/cli-plugin-eslint-4.5.4.tgz",
- "integrity": "sha1-Dx8wer/h5K1n3Ll2k2QJQrFfrnY=",
- "dev": true,
- "requires": {
- "@vue/cli-shared-utils": "^4.5.4",
- "eslint-loader": "^2.2.1",
- "globby": "^9.2.0",
- "inquirer": "^7.1.0",
- "webpack": "^4.0.0",
- "yorkie": "^2.0.0"
- }
- },
- "@vue/cli-plugin-router": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-plugin-router/download/@vue/cli-plugin-router-4.5.4.tgz",
- "integrity": "sha1-BvIkCMftas7dv3MCy0eik7evQ0c=",
- "dev": true,
- "requires": {
- "@vue/cli-shared-utils": "^4.5.4"
- }
- },
- "@vue/cli-plugin-vuex": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-plugin-vuex/download/@vue/cli-plugin-vuex-4.5.4.tgz",
- "integrity": "sha1-YpbjBziPYRMhF+CsAxiAE2UrDFU=",
- "dev": true
- },
- "@vue/cli-service": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-service/download/@vue/cli-service-4.5.4.tgz?cache=0&sync_timestamp=1597717222018&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcli-service%2Fdownload%2F%40vue%2Fcli-service-4.5.4.tgz",
- "integrity": "sha1-+QPt9VXRB0BGJN4v7VmW2oztxSQ=",
- "dev": true,
- "requires": {
- "@intervolga/optimize-cssnano-plugin": "^1.0.5",
- "@soda/friendly-errors-webpack-plugin": "^1.7.1",
- "@soda/get-current-script": "^1.0.0",
- "@types/minimist": "^1.2.0",
- "@types/webpack": "^4.0.0",
- "@types/webpack-dev-server": "^3.11.0",
- "@vue/cli-overlay": "^4.5.4",
- "@vue/cli-plugin-router": "^4.5.4",
- "@vue/cli-plugin-vuex": "^4.5.4",
- "@vue/cli-shared-utils": "^4.5.4",
- "@vue/component-compiler-utils": "^3.1.2",
- "@vue/preload-webpack-plugin": "^1.1.0",
- "@vue/web-component-wrapper": "^1.2.0",
- "acorn": "^7.4.0",
- "acorn-walk": "^7.1.1",
- "address": "^1.1.2",
- "autoprefixer": "^9.8.6",
- "browserslist": "^4.12.0",
- "cache-loader": "^4.1.0",
- "case-sensitive-paths-webpack-plugin": "^2.3.0",
- "cli-highlight": "^2.1.4",
- "clipboardy": "^2.3.0",
- "cliui": "^6.0.0",
- "copy-webpack-plugin": "^5.1.1",
- "css-loader": "^3.5.3",
- "cssnano": "^4.1.10",
- "debug": "^4.1.1",
- "default-gateway": "^5.0.5",
- "dotenv": "^8.2.0",
- "dotenv-expand": "^5.1.0",
- "file-loader": "^4.2.0",
- "fs-extra": "^7.0.1",
- "globby": "^9.2.0",
- "hash-sum": "^2.0.0",
- "html-webpack-plugin": "^3.2.0",
- "launch-editor-middleware": "^2.2.1",
- "lodash.defaultsdeep": "^4.6.1",
- "lodash.mapvalues": "^4.6.0",
- "lodash.transform": "^4.6.0",
- "mini-css-extract-plugin": "^0.9.0",
- "minimist": "^1.2.5",
- "pnp-webpack-plugin": "^1.6.4",
- "portfinder": "^1.0.26",
- "postcss-loader": "^3.0.0",
- "ssri": "^7.1.0",
- "terser-webpack-plugin": "^2.3.6",
- "thread-loader": "^2.1.3",
- "url-loader": "^2.2.0",
- "vue-loader": "^15.9.2",
- "vue-loader-v16": "npm:vue-loader@^16.0.0-beta.3",
- "vue-style-loader": "^4.1.2",
- "webpack": "^4.0.0",
- "webpack-bundle-analyzer": "^3.8.0",
- "webpack-chain": "^6.4.0",
- "webpack-dev-server": "^3.11.0",
- "webpack-merge": "^4.2.2"
- },
- "dependencies": {
- "acorn": {
- "version": "7.4.0",
- "resolved": "https://registry.npm.taobao.org/acorn/download/acorn-7.4.0.tgz?cache=0&sync_timestamp=1597235774928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn%2Fdownload%2Facorn-7.4.0.tgz",
- "integrity": "sha1-4a1IbmxUUBY0xsOXxcEh2qODYHw=",
- "dev": true
- },
- "cacache": {
- "version": "13.0.1",
- "resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1594429684526&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
- "integrity": "sha1-qAAMIWlwiQgvhSh6GuxuOCAkpxw=",
- "dev": true,
- "requires": {
- "chownr": "^1.1.2",
- "figgy-pudding": "^3.5.1",
- "fs-minipass": "^2.0.0",
- "glob": "^7.1.4",
- "graceful-fs": "^4.2.2",
- "infer-owner": "^1.0.4",
- "lru-cache": "^5.1.1",
- "minipass": "^3.0.0",
- "minipass-collect": "^1.0.2",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.2",
- "mkdirp": "^0.5.1",
- "move-concurrently": "^1.0.1",
- "p-map": "^3.0.0",
- "promise-inflight": "^1.0.1",
- "rimraf": "^2.7.1",
- "ssri": "^7.0.0",
- "unique-filename": "^1.1.1"
- }
- },
- "find-cache-dir": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz",
- "integrity": "sha1-ibM/rUpGcNqpT4Vff74x1thP6IA=",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.2",
- "pkg-dir": "^4.1.0"
- }
- },
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-4.1.0.tgz?cache=0&sync_timestamp=1597169842138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-up%2Fdownload%2Ffind-up-4.1.0.tgz",
- "integrity": "sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz",
- "integrity": "sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/make-dir/download/make-dir-3.1.0.tgz",
- "integrity": "sha1-QV6WcEazp/HRhSd9hKpYIDcmoT8=",
- "dev": true,
- "requires": {
- "semver": "^6.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/p-locate/download/p-locate-4.1.0.tgz",
- "integrity": "sha1-o0KLtwiLOmApL2aRkni3wpetTwc=",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-4.0.0.tgz",
- "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=",
- "dev": true
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-4.2.0.tgz",
- "integrity": "sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM=",
- "dev": true,
- "requires": {
- "find-up": "^4.0.0"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- },
- "ssri": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/ssri/download/ssri-7.1.0.tgz",
- "integrity": "sha1-ksJBv23oI2W1x/tL126XVSLhKU0=",
- "dev": true,
- "requires": {
- "figgy-pudding": "^3.5.1",
- "minipass": "^3.1.1"
- }
- },
- "terser-webpack-plugin": {
- "version": "2.3.8",
- "resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1597229593156&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz",
- "integrity": "sha1-iUdkoZsHQ/L3BOfCqEjFKDppZyQ=",
- "dev": true,
- "requires": {
- "cacache": "^13.0.1",
- "find-cache-dir": "^3.3.1",
- "jest-worker": "^25.4.0",
- "p-limit": "^2.3.0",
- "schema-utils": "^2.6.6",
- "serialize-javascript": "^4.0.0",
- "source-map": "^0.6.1",
- "terser": "^4.6.12",
- "webpack-sources": "^1.4.3"
- }
- }
- }
- },
- "@vue/cli-shared-utils": {
- "version": "4.5.4",
- "resolved": "https://registry.npm.taobao.org/@vue/cli-shared-utils/download/@vue/cli-shared-utils-4.5.4.tgz?cache=0&sync_timestamp=1597717221168&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcli-shared-utils%2Fdownload%2F%40vue%2Fcli-shared-utils-4.5.4.tgz",
- "integrity": "sha1-7Taylx3AJlP38q1OZrvpUQ4b1BQ=",
- "dev": true,
- "requires": {
- "@hapi/joi": "^15.0.1",
- "chalk": "^2.4.2",
- "execa": "^1.0.0",
- "launch-editor": "^2.2.1",
- "lru-cache": "^5.1.1",
- "node-ipc": "^9.1.1",
- "open": "^6.3.0",
- "ora": "^3.4.0",
- "read-pkg": "^5.1.1",
- "request": "^2.88.2",
- "semver": "^6.1.0",
- "strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- }
- }
- },
- "@vue/compiler-core": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/compiler-core/download/@vue/compiler-core-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985153463&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcompiler-core%2Fdownload%2F%40vue%2Fcompiler-core-3.0.0-rc.5.tgz",
- "integrity": "sha1-3U8YFvyuNKgbxg5YT5eZPK0oTVQ=",
- "requires": {
- "@babel/parser": "^7.10.4",
- "@babel/types": "^7.10.4",
- "@vue/shared": "3.0.0-rc.5",
- "estree-walker": "^2.0.1",
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM="
- }
- }
- },
- "@vue/compiler-dom": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/compiler-dom/download/@vue/compiler-dom-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985152508&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcompiler-dom%2Fdownload%2F%40vue%2Fcompiler-dom-3.0.0-rc.5.tgz",
- "integrity": "sha1-g5BehgESOjZUuQ+9gHCKFlMM4ho=",
- "requires": {
- "@vue/compiler-core": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5"
- }
- },
- "@vue/compiler-sfc": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/compiler-sfc/download/@vue/compiler-sfc-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985044175&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcompiler-sfc%2Fdownload%2F%40vue%2Fcompiler-sfc-3.0.0-rc.5.tgz",
- "integrity": "sha1-N05Spvv4+5ruEhMCYFCg8cSW/s8=",
- "dev": true,
- "requires": {
- "@babel/parser": "^7.10.4",
- "@babel/types": "^7.10.4",
- "@vue/compiler-core": "3.0.0-rc.5",
- "@vue/compiler-dom": "3.0.0-rc.5",
- "@vue/compiler-ssr": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5",
- "consolidate": "^0.15.1",
- "estree-walker": "^2.0.1",
- "hash-sum": "^2.0.0",
- "lru-cache": "^5.1.1",
- "magic-string": "^0.25.7",
- "merge-source-map": "^1.1.0",
- "postcss": "^7.0.27",
- "postcss-modules": "^3.1.0",
- "postcss-selector-parser": "^6.0.2",
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "@vue/compiler-ssr": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/compiler-ssr/download/@vue/compiler-ssr-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985044376&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcompiler-ssr%2Fdownload%2F%40vue%2Fcompiler-ssr-3.0.0-rc.5.tgz",
- "integrity": "sha1-h4QGxZ2v82Ls3LGZ+5Rnp2nKjeU=",
- "dev": true,
- "requires": {
- "@vue/compiler-dom": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5"
- }
- },
- "@vue/component-compiler-utils": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/@vue/component-compiler-utils/download/@vue/component-compiler-utils-3.2.0.tgz?cache=0&sync_timestamp=1595427628913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fcomponent-compiler-utils%2Fdownload%2F%40vue%2Fcomponent-compiler-utils-3.2.0.tgz",
- "integrity": "sha1-j4UYLO7Sjps8dTE95mn4MWbRHl0=",
- "dev": true,
- "requires": {
- "consolidate": "^0.15.1",
- "hash-sum": "^1.0.2",
- "lru-cache": "^4.1.2",
- "merge-source-map": "^1.1.0",
- "postcss": "^7.0.14",
- "postcss-selector-parser": "^6.0.2",
- "prettier": "^1.18.2",
- "source-map": "~0.6.1",
- "vue-template-es2015-compiler": "^1.9.0"
- },
- "dependencies": {
- "hash-sum": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/hash-sum/download/hash-sum-1.0.2.tgz",
- "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
- "dev": true
- },
- "lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.5.tgz?cache=0&sync_timestamp=1594427582110&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-4.1.5.tgz",
- "integrity": "sha1-i75Q6oW+1ZvJ4z3KuCNe6bz0Q80=",
- "dev": true,
- "requires": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
- }
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- },
- "yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
- }
- }
- },
- "@vue/preload-webpack-plugin": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/@vue/preload-webpack-plugin/download/@vue/preload-webpack-plugin-1.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fpreload-webpack-plugin%2Fdownload%2F%40vue%2Fpreload-webpack-plugin-1.1.2.tgz",
- "integrity": "sha1-zrkktOyzucQ4ccekKaAvhCPmIas=",
- "dev": true
- },
- "@vue/reactivity": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/reactivity/download/@vue/reactivity-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985154146&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Freactivity%2Fdownload%2F%40vue%2Freactivity-3.0.0-rc.5.tgz",
- "integrity": "sha1-Rc/42DnXrRMLHkmSOQkAUP3s/xM=",
- "requires": {
- "@vue/shared": "3.0.0-rc.5"
- }
- },
- "@vue/runtime-core": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/runtime-core/download/@vue/runtime-core-3.0.0-rc.5.tgz",
- "integrity": "sha1-3VmvOl/AidHNwFplcyDA3Bflw2I=",
- "requires": {
- "@vue/reactivity": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5"
- }
- },
- "@vue/runtime-dom": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/runtime-dom/download/@vue/runtime-dom-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985153167&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fruntime-dom%2Fdownload%2F%40vue%2Fruntime-dom-3.0.0-rc.5.tgz",
- "integrity": "sha1-L9daHymyOr8P/lzN7avaEXIcW1s=",
- "requires": {
- "@vue/runtime-core": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5",
- "csstype": "^2.6.8"
- }
- },
- "@vue/shared": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/@vue/shared/download/@vue/shared-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595985152851&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fshared%2Fdownload%2F%40vue%2Fshared-3.0.0-rc.5.tgz",
- "integrity": "sha1-zqI3jj43Nj3cH13RWO3JybWz//A="
- },
- "@vue/web-component-wrapper": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/@vue/web-component-wrapper/download/@vue/web-component-wrapper-1.2.0.tgz",
- "integrity": "sha1-uw5G8VhafiibTuYGfcxaauYvHdE=",
- "dev": true
- },
- "@webassemblyjs/ast": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/ast/download/@webassemblyjs/ast-1.9.0.tgz",
- "integrity": "sha1-vYUGBLQEJFmlpBzX0zjL7Wle2WQ=",
- "dev": true,
- "requires": {
- "@webassemblyjs/helper-module-context": "1.9.0",
- "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
- "@webassemblyjs/wast-parser": "1.9.0"
- }
- },
- "@webassemblyjs/floating-point-hex-parser": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/floating-point-hex-parser/download/@webassemblyjs/floating-point-hex-parser-1.9.0.tgz",
- "integrity": "sha1-PD07Jxvd/ITesA9xNEQ4MR1S/7Q=",
- "dev": true
- },
- "@webassemblyjs/helper-api-error": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-api-error/download/@webassemblyjs/helper-api-error-1.9.0.tgz",
- "integrity": "sha1-ID9nbjM7lsnaLuqzzO8zxFkotqI=",
- "dev": true
- },
- "@webassemblyjs/helper-buffer": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-buffer/download/@webassemblyjs/helper-buffer-1.9.0.tgz",
- "integrity": "sha1-oUQtJpxf6yP8vJ73WdrDVH8p3gA=",
- "dev": true
- },
- "@webassemblyjs/helper-code-frame": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-code-frame/download/@webassemblyjs/helper-code-frame-1.9.0.tgz",
- "integrity": "sha1-ZH+Iks0gQ6gqwMjF51w28dkVnyc=",
- "dev": true,
- "requires": {
- "@webassemblyjs/wast-printer": "1.9.0"
- }
- },
- "@webassemblyjs/helper-fsm": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-fsm/download/@webassemblyjs/helper-fsm-1.9.0.tgz",
- "integrity": "sha1-wFJWtxJEIUZx9LCOwQitY7cO3bg=",
- "dev": true
- },
- "@webassemblyjs/helper-module-context": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-module-context/download/@webassemblyjs/helper-module-context-1.9.0.tgz",
- "integrity": "sha1-JdiIS3aDmHGgimxvgGw5ee9xLwc=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0"
- }
- },
- "@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-wasm-bytecode/download/@webassemblyjs/helper-wasm-bytecode-1.9.0.tgz",
- "integrity": "sha1-T+2L6sm4wU+MWLcNEk1UndH+V5A=",
- "dev": true
- },
- "@webassemblyjs/helper-wasm-section": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/helper-wasm-section/download/@webassemblyjs/helper-wasm-section-1.9.0.tgz",
- "integrity": "sha1-WkE41aYpK6GLBMWuSXF+QWeWU0Y=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-buffer": "1.9.0",
- "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
- "@webassemblyjs/wasm-gen": "1.9.0"
- }
- },
- "@webassemblyjs/ieee754": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/ieee754/download/@webassemblyjs/ieee754-1.9.0.tgz",
- "integrity": "sha1-Fceg+6roP7JhQ7us9tbfFwKtOeQ=",
- "dev": true,
- "requires": {
- "@xtuc/ieee754": "^1.2.0"
- }
- },
- "@webassemblyjs/leb128": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/leb128/download/@webassemblyjs/leb128-1.9.0.tgz",
- "integrity": "sha1-8Zygt2ptxVYjoJz/p2noOPoeHJU=",
- "dev": true,
- "requires": {
- "@xtuc/long": "4.2.2"
- }
- },
- "@webassemblyjs/utf8": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/utf8/download/@webassemblyjs/utf8-1.9.0.tgz",
- "integrity": "sha1-BNM7Y2945qaBMifoJAL3Y3tiKas=",
- "dev": true
- },
- "@webassemblyjs/wasm-edit": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wasm-edit/download/@webassemblyjs/wasm-edit-1.9.0.tgz",
- "integrity": "sha1-P+bXnT8PkiGDqoYALELdJWz+6c8=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-buffer": "1.9.0",
- "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
- "@webassemblyjs/helper-wasm-section": "1.9.0",
- "@webassemblyjs/wasm-gen": "1.9.0",
- "@webassemblyjs/wasm-opt": "1.9.0",
- "@webassemblyjs/wasm-parser": "1.9.0",
- "@webassemblyjs/wast-printer": "1.9.0"
- }
- },
- "@webassemblyjs/wasm-gen": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wasm-gen/download/@webassemblyjs/wasm-gen-1.9.0.tgz",
- "integrity": "sha1-ULxw7Gje2OJ2OwGhQYv0NJGnpJw=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
- "@webassemblyjs/ieee754": "1.9.0",
- "@webassemblyjs/leb128": "1.9.0",
- "@webassemblyjs/utf8": "1.9.0"
- }
- },
- "@webassemblyjs/wasm-opt": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wasm-opt/download/@webassemblyjs/wasm-opt-1.9.0.tgz",
- "integrity": "sha1-IhEYHlsxMmRDzIES658LkChyGmE=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-buffer": "1.9.0",
- "@webassemblyjs/wasm-gen": "1.9.0",
- "@webassemblyjs/wasm-parser": "1.9.0"
- }
- },
- "@webassemblyjs/wasm-parser": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wasm-parser/download/@webassemblyjs/wasm-parser-1.9.0.tgz",
- "integrity": "sha1-nUjkSCbfSmWYKUqmyHRp1kL/9l4=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-api-error": "1.9.0",
- "@webassemblyjs/helper-wasm-bytecode": "1.9.0",
- "@webassemblyjs/ieee754": "1.9.0",
- "@webassemblyjs/leb128": "1.9.0",
- "@webassemblyjs/utf8": "1.9.0"
- }
- },
- "@webassemblyjs/wast-parser": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wast-parser/download/@webassemblyjs/wast-parser-1.9.0.tgz",
- "integrity": "sha1-MDERXXmsW9JhVWzsw/qQo+9FGRQ=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/floating-point-hex-parser": "1.9.0",
- "@webassemblyjs/helper-api-error": "1.9.0",
- "@webassemblyjs/helper-code-frame": "1.9.0",
- "@webassemblyjs/helper-fsm": "1.9.0",
- "@xtuc/long": "4.2.2"
- }
- },
- "@webassemblyjs/wast-printer": {
- "version": "1.9.0",
- "resolved": "https://registry.npm.taobao.org/@webassemblyjs/wast-printer/download/@webassemblyjs/wast-printer-1.9.0.tgz",
- "integrity": "sha1-STXVTIX+9jewDOn1I3dFHQDUeJk=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/wast-parser": "1.9.0",
- "@xtuc/long": "4.2.2"
- }
- },
- "@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/@xtuc/ieee754/download/@xtuc/ieee754-1.2.0.tgz",
- "integrity": "sha1-7vAUoxRa5Hehy8AM0eVSM23Ot5A=",
- "dev": true
- },
- "@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npm.taobao.org/@xtuc/long/download/@xtuc/long-4.2.2.tgz",
- "integrity": "sha1-0pHGpOl5ibXGHZrPOWrk/hM6cY0=",
- "dev": true
- },
- "accepts": {
- "version": "1.3.7",
- "resolved": "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz",
- "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=",
- "dev": true,
- "requires": {
- "mime-types": "~2.1.24",
- "negotiator": "0.6.2"
- }
- },
- "acorn": {
- "version": "6.4.1",
- "resolved": "https://registry.npm.taobao.org/acorn/download/acorn-6.4.1.tgz?cache=0&sync_timestamp=1597235774928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn%2Fdownload%2Facorn-6.4.1.tgz",
- "integrity": "sha1-Ux5Yuj9RudrLmmZGyk3r9bFMpHQ=",
- "dev": true
- },
- "acorn-jsx": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/acorn-jsx/download/acorn-jsx-5.2.0.tgz?cache=0&sync_timestamp=1589684116279&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn-jsx%2Fdownload%2Facorn-jsx-5.2.0.tgz",
- "integrity": "sha1-TGYGkXPW/daO2FI5/CViJhgrLr4=",
- "dev": true
- },
- "acorn-walk": {
- "version": "7.2.0",
- "resolved": "https://registry.npm.taobao.org/acorn-walk/download/acorn-walk-7.2.0.tgz?cache=0&sync_timestamp=1597235855275&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn-walk%2Fdownload%2Facorn-walk-7.2.0.tgz",
- "integrity": "sha1-DeiJpgEgOQmw++B7iTjcIdLpZ7w=",
- "dev": true
- },
- "address": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/address/download/address-1.1.2.tgz",
- "integrity": "sha1-vxEWycdYxRt6kz0pa3LCIe2UKLY=",
- "dev": true
- },
- "aggregate-error": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/aggregate-error/download/aggregate-error-3.0.1.tgz",
- "integrity": "sha1-2y/nJG5Tb0DZtUQqOeEX191qJOA=",
- "dev": true,
- "requires": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
- }
- },
- "ajv": {
- "version": "6.12.4",
- "resolved": "https://registry.npm.taobao.org/ajv/download/ajv-6.12.4.tgz?cache=0&sync_timestamp=1597480759610&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fajv%2Fdownload%2Fajv-6.12.4.tgz",
- "integrity": "sha1-BhT6zEUiEn+nE0Rca/0+vTduIjQ=",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "ajv-errors": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/ajv-errors/download/ajv-errors-1.0.1.tgz",
- "integrity": "sha1-81mGrOuRr63sQQL72FAUlQzvpk0=",
- "dev": true
- },
- "ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npm.taobao.org/ajv-keywords/download/ajv-keywords-3.5.2.tgz?cache=0&sync_timestamp=1595907068314&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fajv-keywords%2Fdownload%2Fajv-keywords-3.5.2.tgz",
- "integrity": "sha1-MfKdpatuANHC0yms97WSlhTVAU0=",
- "dev": true
- },
- "alphanum-sort": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/alphanum-sort/download/alphanum-sort-1.0.2.tgz",
- "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
- "dev": true
- },
- "ansi-colors": {
- "version": "3.2.4",
- "resolved": "https://registry.npm.taobao.org/ansi-colors/download/ansi-colors-3.2.4.tgz",
- "integrity": "sha1-46PaS/uubIapwoViXeEkojQCb78=",
- "dev": true
- },
- "ansi-escapes": {
- "version": "4.3.1",
- "resolved": "https://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-4.3.1.tgz",
- "integrity": "sha1-pcR8xDGB8fOP/XB2g3cA05VSKmE=",
- "dev": true,
- "requires": {
- "type-fest": "^0.11.0"
- },
- "dependencies": {
- "type-fest": {
- "version": "0.11.0",
- "resolved": "https://registry.npm.taobao.org/type-fest/download/type-fest-0.11.0.tgz",
- "integrity": "sha1-l6vwhyMQ/tiKXEZrJWgVdhReM/E=",
- "dev": true
- }
- }
- },
- "ansi-html": {
- "version": "0.0.7",
- "resolved": "https://registry.npm.taobao.org/ansi-html/download/ansi-html-0.0.7.tgz?cache=0&sync_timestamp=1589682753624&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-html%2Fdownload%2Fansi-html-0.0.7.tgz",
- "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
- "dev": true
- },
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz",
- "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=",
- "dev": true
- },
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-3.2.1.tgz",
- "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=",
- "dev": true,
- "requires": {
- "color-convert": "^1.9.0"
- }
- },
- "any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz?cache=0&sync_timestamp=1589682733115&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fany-promise%2Fdownload%2Fany-promise-1.3.0.tgz",
- "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=",
- "dev": true
- },
- "anymatch": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-3.1.1.tgz",
- "integrity": "sha1-xV7PAhheJGklk5kxDBc84xIzsUI=",
- "dev": true,
- "optional": true,
- "requires": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- }
- },
- "aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/aproba/download/aproba-1.2.0.tgz",
- "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=",
- "dev": true
- },
- "arch": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/arch/download/arch-2.1.2.tgz",
- "integrity": "sha1-DFK75zRLtPomDEQ9LLrZwA/y8L8=",
- "dev": true
- },
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz?cache=0&sync_timestamp=1597414329190&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fargparse%2Fdownload%2Fargparse-1.0.10.tgz",
- "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=",
- "dev": true,
- "requires": {
- "sprintf-js": "~1.0.2"
- }
- },
- "arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/arr-diff/download/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
- },
- "arr-flatten": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/arr-flatten/download/arr-flatten-1.1.0.tgz",
- "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
- "dev": true
- },
- "arr-union": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/arr-union/download/arr-union-3.1.0.tgz",
- "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
- "dev": true
- },
- "array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/array-flatten/download/array-flatten-1.1.1.tgz",
- "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
- "dev": true
- },
- "array-union": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz",
- "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
- "dev": true,
- "requires": {
- "array-uniq": "^1.0.1"
- }
- },
- "array-uniq": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/array-uniq/download/array-uniq-1.0.3.tgz",
- "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
- "dev": true
- },
- "array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npm.taobao.org/array-unique/download/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
- },
- "asn1": {
- "version": "0.2.4",
- "resolved": "https://registry.npm.taobao.org/asn1/download/asn1-0.2.4.tgz",
- "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=",
- "dev": true,
- "requires": {
- "safer-buffer": "~2.1.0"
- }
- },
- "asn1.js": {
- "version": "5.4.1",
- "resolved": "https://registry.npm.taobao.org/asn1.js/download/asn1.js-5.4.1.tgz",
- "integrity": "sha1-EamAuE67kXgc41sP3C7ilON4Pwc=",
- "dev": true,
- "requires": {
- "bn.js": "^4.0.0",
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "safer-buffer": "^2.1.0"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "assert": {
- "version": "1.5.0",
- "resolved": "https://registry.npm.taobao.org/assert/download/assert-1.5.0.tgz",
- "integrity": "sha1-VcEJqvbgrv2z3EtxJAxwv1dLGOs=",
- "dev": true,
- "requires": {
- "object-assign": "^4.1.1",
- "util": "0.10.3"
- },
- "dependencies": {
- "inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.1.tgz",
- "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
- "dev": true
- },
- "util": {
- "version": "0.10.3",
- "resolved": "https://registry.npm.taobao.org/util/download/util-0.10.3.tgz",
- "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
- "dev": true,
- "requires": {
- "inherits": "2.0.1"
- }
- }
- }
- },
- "assert-plus": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/assert-plus/download/assert-plus-1.0.0.tgz",
- "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true
- },
- "assign-symbols": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/assign-symbols/download/assign-symbols-1.0.0.tgz",
- "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
- "dev": true
- },
- "astral-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/astral-regex/download/astral-regex-1.0.0.tgz",
- "integrity": "sha1-bIw/uCfdQ+45GPJ7gngqt2WKb9k=",
- "dev": true
- },
- "async": {
- "version": "2.6.3",
- "resolved": "https://registry.npm.taobao.org/async/download/async-2.6.3.tgz",
- "integrity": "sha1-1yYl4jRKNlbjo61Pp0n6gymdgv8=",
- "dev": true,
- "requires": {
- "lodash": "^4.17.14"
- }
- },
- "async-each": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/async-each/download/async-each-1.0.3.tgz",
- "integrity": "sha1-tyfb+H12UWAvBvTUrDh/R9kbDL8=",
- "dev": true
- },
- "async-limiter": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/async-limiter/download/async-limiter-1.0.1.tgz",
- "integrity": "sha1-3TeelPDbgxCwgpH51kwyCXZmF/0=",
- "dev": true
- },
- "asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz",
- "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
- "dev": true
- },
- "atob": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/atob/download/atob-2.1.2.tgz",
- "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=",
- "dev": true
- },
- "autoprefixer": {
- "version": "9.8.6",
- "resolved": "https://registry.npm.taobao.org/autoprefixer/download/autoprefixer-9.8.6.tgz?cache=0&sync_timestamp=1596143120138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fautoprefixer%2Fdownload%2Fautoprefixer-9.8.6.tgz",
- "integrity": "sha1-O3NZTKG/kmYyDFrPFYjXTep0IQ8=",
- "dev": true,
- "requires": {
- "browserslist": "^4.12.0",
- "caniuse-lite": "^1.0.30001109",
- "colorette": "^1.2.1",
- "normalize-range": "^0.1.2",
- "num2fraction": "^1.2.2",
- "postcss": "^7.0.32",
- "postcss-value-parser": "^4.1.0"
- }
- },
- "aws-sign2": {
- "version": "0.7.0",
- "resolved": "https://registry.npm.taobao.org/aws-sign2/download/aws-sign2-0.7.0.tgz?cache=0&sync_timestamp=1589682812085&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Faws-sign2%2Fdownload%2Faws-sign2-0.7.0.tgz",
- "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
- "dev": true
- },
- "aws4": {
- "version": "1.10.1",
- "resolved": "https://registry.npm.taobao.org/aws4/download/aws4-1.10.1.tgz?cache=0&sync_timestamp=1597236947743&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Faws4%2Fdownload%2Faws4-1.10.1.tgz",
- "integrity": "sha1-4eguTz6Zniz9YbFhKA0WoRH4ZCg=",
- "dev": true
- },
- "babel-eslint": {
- "version": "10.1.0",
- "resolved": "https://registry.npm.taobao.org/babel-eslint/download/babel-eslint-10.1.0.tgz",
- "integrity": "sha1-aWjlaKkQt4+zd5zdi2rC9HmUMjI=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "@babel/parser": "^7.7.0",
- "@babel/traverse": "^7.7.0",
- "@babel/types": "^7.7.0",
- "eslint-visitor-keys": "^1.0.0",
- "resolve": "^1.12.0"
- }
- },
- "babel-loader": {
- "version": "8.1.0",
- "resolved": "https://registry.npm.taobao.org/babel-loader/download/babel-loader-8.1.0.tgz",
- "integrity": "sha1-xhHVESvVIJq+i5+oTD5NolJ18cM=",
- "dev": true,
- "requires": {
- "find-cache-dir": "^2.1.0",
- "loader-utils": "^1.4.0",
- "mkdirp": "^0.5.3",
- "pify": "^4.0.1",
- "schema-utils": "^2.6.5"
- }
- },
- "babel-plugin-dynamic-import-node": {
- "version": "2.3.3",
- "resolved": "https://registry.npm.taobao.org/babel-plugin-dynamic-import-node/download/babel-plugin-dynamic-import-node-2.3.3.tgz",
- "integrity": "sha1-hP2hnJduxcbe/vV/lCez3vZuF6M=",
- "dev": true,
- "requires": {
- "object.assign": "^4.1.0"
- }
- },
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
- },
- "base": {
- "version": "0.11.2",
- "resolved": "https://registry.npm.taobao.org/base/download/base-0.11.2.tgz",
- "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=",
- "dev": true,
- "requires": {
- "cache-base": "^1.0.1",
- "class-utils": "^0.3.5",
- "component-emitter": "^1.2.1",
- "define-property": "^1.0.0",
- "isobject": "^3.0.1",
- "mixin-deep": "^1.2.0",
- "pascalcase": "^0.1.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.0"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-descriptor/download/is-descriptor-1.0.2.tgz",
- "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
- }
- },
- "base64-js": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/base64-js/download/base64-js-1.3.1.tgz",
- "integrity": "sha1-WOzoy3XdB+ce0IxzarxfrE2/jfE=",
- "dev": true
- },
- "batch": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/batch/download/batch-0.6.1.tgz",
- "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
- "dev": true
- },
- "bcrypt-pbkdf": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/bcrypt-pbkdf/download/bcrypt-pbkdf-1.0.2.tgz?cache=0&sync_timestamp=1589682746075&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbcrypt-pbkdf%2Fdownload%2Fbcrypt-pbkdf-1.0.2.tgz",
- "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
- "dev": true,
- "requires": {
- "tweetnacl": "^0.14.3"
- }
- },
- "bfj": {
- "version": "6.1.2",
- "resolved": "https://registry.npm.taobao.org/bfj/download/bfj-6.1.2.tgz",
- "integrity": "sha1-MlyGGoIryzWKQceKM7jm4ght3n8=",
- "dev": true,
- "requires": {
- "bluebird": "^3.5.5",
- "check-types": "^8.0.3",
- "hoopy": "^0.1.4",
- "tryer": "^1.0.1"
- }
- },
- "big.js": {
- "version": "5.2.2",
- "resolved": "https://registry.npm.taobao.org/big.js/download/big.js-5.2.2.tgz",
- "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=",
- "dev": true
- },
- "binary-extensions": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-2.1.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-2.1.0.tgz",
- "integrity": "sha1-MPpAyef+B9vIlWeM0ocCTeokHdk=",
- "dev": true,
- "optional": true
- },
- "bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npm.taobao.org/bindings/download/bindings-1.5.0.tgz?cache=0&sync_timestamp=1589682780212&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbindings%2Fdownload%2Fbindings-1.5.0.tgz",
- "integrity": "sha1-EDU8npRTNLwFEabZCzj7x8nFBN8=",
- "dev": true,
- "optional": true,
- "requires": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "bluebird": {
- "version": "3.7.2",
- "resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz?cache=0&sync_timestamp=1589682744631&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbluebird%2Fdownload%2Fbluebird-3.7.2.tgz",
- "integrity": "sha1-nyKcFb4nJFT/qXOs4NvueaGww28=",
- "dev": true
- },
- "bn.js": {
- "version": "5.1.3",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-5.1.3.tgz",
- "integrity": "sha1-vsoAVAj2Quvr6oCwQrTRjSrA7ms=",
- "dev": true
- },
- "body-parser": {
- "version": "1.19.0",
- "resolved": "https://registry.npm.taobao.org/body-parser/download/body-parser-1.19.0.tgz",
- "integrity": "sha1-lrJwnlfJxOCab9Zqj9l5hE9p8Io=",
- "dev": true,
- "requires": {
- "bytes": "3.1.0",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "http-errors": "1.7.2",
- "iconv-lite": "0.4.24",
- "on-finished": "~2.3.0",
- "qs": "6.7.0",
- "raw-body": "2.4.0",
- "type-is": "~1.6.17"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
- "qs": {
- "version": "6.7.0",
- "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
- "integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=",
- "dev": true
- }
- }
- },
- "bonjour": {
- "version": "3.5.0",
- "resolved": "https://registry.npm.taobao.org/bonjour/download/bonjour-3.5.0.tgz",
- "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
- "dev": true,
- "requires": {
- "array-flatten": "^2.1.0",
- "deep-equal": "^1.0.1",
- "dns-equal": "^1.0.0",
- "dns-txt": "^2.0.2",
- "multicast-dns": "^6.0.1",
- "multicast-dns-service-types": "^1.1.0"
- },
- "dependencies": {
- "array-flatten": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/array-flatten/download/array-flatten-2.1.2.tgz",
- "integrity": "sha1-JO+AoowaiTYX4hSbDG0NeIKTsJk=",
- "dev": true
- }
- }
- },
- "boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/boolbase/download/boolbase-1.0.0.tgz",
- "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.11.tgz",
- "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npm.taobao.org/braces/download/braces-2.3.2.tgz",
- "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=",
- "dev": true,
- "requires": {
- "arr-flatten": "^1.1.0",
- "array-unique": "^0.3.2",
- "extend-shallow": "^2.0.1",
- "fill-range": "^4.0.0",
- "isobject": "^3.0.1",
- "repeat-element": "^1.1.2",
- "snapdragon": "^0.8.1",
- "snapdragon-node": "^2.0.1",
- "split-string": "^3.0.2",
- "to-regex": "^3.0.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- }
- }
- },
- "brorand": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/brorand/download/brorand-1.1.0.tgz",
- "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
- "dev": true
- },
- "browserify-aes": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/browserify-aes/download/browserify-aes-1.2.0.tgz",
- "integrity": "sha1-Mmc0ZC9APavDADIJhTu3CtQo70g=",
- "dev": true,
- "requires": {
- "buffer-xor": "^1.0.3",
- "cipher-base": "^1.0.0",
- "create-hash": "^1.1.0",
- "evp_bytestokey": "^1.0.3",
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
- "browserify-cipher": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/browserify-cipher/download/browserify-cipher-1.0.1.tgz",
- "integrity": "sha1-jWR0wbhwv9q807z8wZNKEOlPFfA=",
- "dev": true,
- "requires": {
- "browserify-aes": "^1.0.4",
- "browserify-des": "^1.0.0",
- "evp_bytestokey": "^1.0.0"
- }
- },
- "browserify-des": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/browserify-des/download/browserify-des-1.0.2.tgz",
- "integrity": "sha1-OvTx9Zg5QDVy8cZiBDdfen9wPpw=",
- "dev": true,
- "requires": {
- "cipher-base": "^1.0.1",
- "des.js": "^1.0.0",
- "inherits": "^2.0.1",
- "safe-buffer": "^5.1.2"
- }
- },
- "browserify-rsa": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/browserify-rsa/download/browserify-rsa-4.0.1.tgz",
- "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "randombytes": "^2.0.1"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "browserify-sign": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/browserify-sign/download/browserify-sign-4.2.1.tgz?cache=0&sync_timestamp=1596557839219&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbrowserify-sign%2Fdownload%2Fbrowserify-sign-4.2.1.tgz",
- "integrity": "sha1-6vSt1G3VS+O7OzbAzxWrvrp5VsM=",
- "dev": true,
- "requires": {
- "bn.js": "^5.1.1",
- "browserify-rsa": "^4.0.1",
- "create-hash": "^1.2.0",
- "create-hmac": "^1.1.7",
- "elliptic": "^6.5.3",
- "inherits": "^2.0.4",
- "parse-asn1": "^5.1.5",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
- },
- "dependencies": {
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.6.0.tgz?cache=0&sync_timestamp=1589682741447&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-3.6.0.tgz",
- "integrity": "sha1-M3u9o63AcGvT4CRCaihtS0sskZg=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- },
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.2.1.tgz?cache=0&sync_timestamp=1589682795646&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafe-buffer%2Fdownload%2Fsafe-buffer-5.2.1.tgz",
- "integrity": "sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY=",
- "dev": true
- }
- }
- },
- "browserify-zlib": {
- "version": "0.2.0",
- "resolved": "https://registry.npm.taobao.org/browserify-zlib/download/browserify-zlib-0.2.0.tgz",
- "integrity": "sha1-KGlFnZqjviRf6P4sofRuLn9U1z8=",
- "dev": true,
- "requires": {
- "pako": "~1.0.5"
- }
- },
- "browserslist": {
- "version": "4.14.0",
- "resolved": "https://registry.npm.taobao.org/browserslist/download/browserslist-4.14.0.tgz?cache=0&sync_timestamp=1596756179725&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbrowserslist%2Fdownload%2Fbrowserslist-4.14.0.tgz",
- "integrity": "sha1-KQiVGr/k7Jhze3LzTDvO3I1DsAA=",
- "dev": true,
- "requires": {
- "caniuse-lite": "^1.0.30001111",
- "electron-to-chromium": "^1.3.523",
- "escalade": "^3.0.2",
- "node-releases": "^1.1.60"
- }
- },
- "buffer": {
- "version": "4.9.2",
- "resolved": "https://registry.npm.taobao.org/buffer/download/buffer-4.9.2.tgz",
- "integrity": "sha1-Iw6tNEACmIZEhBqwJEr4xEu+Pvg=",
- "dev": true,
- "requires": {
- "base64-js": "^1.0.2",
- "ieee754": "^1.1.4",
- "isarray": "^1.0.0"
- }
- },
- "buffer-from": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz",
- "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=",
- "dev": true
- },
- "buffer-indexof": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/buffer-indexof/download/buffer-indexof-1.1.1.tgz",
- "integrity": "sha1-Uvq8xqYG0aADAoAmSO9o9jnaJow=",
- "dev": true
- },
- "buffer-json": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/buffer-json/download/buffer-json-2.0.0.tgz",
- "integrity": "sha1-9z4TseQvGW/i/WfQAcfXEH7dfCM=",
- "dev": true
- },
- "buffer-xor": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/buffer-xor/download/buffer-xor-1.0.3.tgz",
- "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
- "dev": true
- },
- "builtin-status-codes": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/builtin-status-codes/download/builtin-status-codes-3.0.0.tgz",
- "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
- "dev": true
- },
- "bytes": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/bytes/download/bytes-3.1.0.tgz?cache=0&sync_timestamp=1589682741197&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbytes%2Fdownload%2Fbytes-3.1.0.tgz",
- "integrity": "sha1-9s95M6Ng4FiPqf3oVlHNx/gF0fY=",
- "dev": true
- },
- "cacache": {
- "version": "12.0.4",
- "resolved": "https://registry.npm.taobao.org/cacache/download/cacache-12.0.4.tgz?cache=0&sync_timestamp=1594429684526&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-12.0.4.tgz",
- "integrity": "sha1-ZovL0QWutfHZL+JVcOyVJcj6pAw=",
- "dev": true,
- "requires": {
- "bluebird": "^3.5.5",
- "chownr": "^1.1.1",
- "figgy-pudding": "^3.5.1",
- "glob": "^7.1.4",
- "graceful-fs": "^4.1.15",
- "infer-owner": "^1.0.3",
- "lru-cache": "^5.1.1",
- "mississippi": "^3.0.0",
- "mkdirp": "^0.5.1",
- "move-concurrently": "^1.0.1",
- "promise-inflight": "^1.0.1",
- "rimraf": "^2.6.3",
- "ssri": "^6.0.1",
- "unique-filename": "^1.1.1",
- "y18n": "^4.0.0"
- }
- },
- "cache-base": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/cache-base/download/cache-base-1.0.1.tgz",
- "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=",
- "dev": true,
- "requires": {
- "collection-visit": "^1.0.0",
- "component-emitter": "^1.2.1",
- "get-value": "^2.0.6",
- "has-value": "^1.0.0",
- "isobject": "^3.0.1",
- "set-value": "^2.0.0",
- "to-object-path": "^0.3.0",
- "union-value": "^1.0.0",
- "unset-value": "^1.0.0"
- }
- },
- "cache-loader": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/cache-loader/download/cache-loader-4.1.0.tgz",
- "integrity": "sha1-mUjK41OuwKH8ser9ojAIFuyFOH4=",
- "dev": true,
- "requires": {
- "buffer-json": "^2.0.0",
- "find-cache-dir": "^3.0.0",
- "loader-utils": "^1.2.3",
- "mkdirp": "^0.5.1",
- "neo-async": "^2.6.1",
- "schema-utils": "^2.0.0"
- },
- "dependencies": {
- "find-cache-dir": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz",
- "integrity": "sha1-ibM/rUpGcNqpT4Vff74x1thP6IA=",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.2",
- "pkg-dir": "^4.1.0"
- }
- },
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-4.1.0.tgz?cache=0&sync_timestamp=1597169842138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-up%2Fdownload%2Ffind-up-4.1.0.tgz",
- "integrity": "sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz",
- "integrity": "sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/make-dir/download/make-dir-3.1.0.tgz",
- "integrity": "sha1-QV6WcEazp/HRhSd9hKpYIDcmoT8=",
- "dev": true,
- "requires": {
- "semver": "^6.0.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/p-locate/download/p-locate-4.1.0.tgz",
- "integrity": "sha1-o0KLtwiLOmApL2aRkni3wpetTwc=",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-4.0.0.tgz",
- "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=",
- "dev": true
- },
- "pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-4.2.0.tgz",
- "integrity": "sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM=",
- "dev": true,
- "requires": {
- "find-up": "^4.0.0"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- }
- }
- },
- "call-me-maybe": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/call-me-maybe/download/call-me-maybe-1.0.1.tgz",
- "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
- "dev": true
- },
- "caller-callsite": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/caller-callsite/download/caller-callsite-2.0.0.tgz",
- "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
- "dev": true,
- "requires": {
- "callsites": "^2.0.0"
- }
- },
- "caller-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/caller-path/download/caller-path-2.0.0.tgz",
- "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
- "dev": true,
- "requires": {
- "caller-callsite": "^2.0.0"
- }
- },
- "callsites": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/callsites/download/callsites-2.0.0.tgz",
- "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
- "dev": true
- },
- "camel-case": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/camel-case/download/camel-case-3.0.0.tgz",
- "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
- "dev": true,
- "requires": {
- "no-case": "^2.2.0",
- "upper-case": "^1.1.1"
- }
- },
- "camelcase": {
- "version": "6.0.0",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-6.0.0.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-6.0.0.tgz",
- "integrity": "sha1-Uln3ww414njxvcKk2RIws3ytmB4=",
- "dev": true
- },
- "caniuse-api": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/caniuse-api/download/caniuse-api-3.0.0.tgz",
- "integrity": "sha1-Xk2Q4idJYdRikZl99Znj7QCO5MA=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "caniuse-lite": "^1.0.0",
- "lodash.memoize": "^4.1.2",
- "lodash.uniq": "^4.5.0"
- }
- },
- "caniuse-lite": {
- "version": "1.0.30001116",
- "resolved": "https://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30001116.tgz?cache=0&sync_timestamp=1597783991061&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001116.tgz",
- "integrity": "sha1-86Peo0f5KUo73EKSMJA5zIQRf7g=",
- "dev": true
- },
- "case-sensitive-paths-webpack-plugin": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/case-sensitive-paths-webpack-plugin/download/case-sensitive-paths-webpack-plugin-2.3.0.tgz",
- "integrity": "sha1-I6xhPMmoVuT4j/i7c7u16YmCXPc=",
- "dev": true
- },
- "caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npm.taobao.org/caseless/download/caseless-0.12.0.tgz",
- "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
- "dev": true
- },
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-2.4.2.tgz",
- "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- }
- },
- "chardet": {
- "version": "0.7.0",
- "resolved": "https://registry.npm.taobao.org/chardet/download/chardet-0.7.0.tgz?cache=0&sync_timestamp=1594010664806&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchardet%2Fdownload%2Fchardet-0.7.0.tgz",
- "integrity": "sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=",
- "dev": true
- },
- "check-types": {
- "version": "8.0.3",
- "resolved": "https://registry.npm.taobao.org/check-types/download/check-types-8.0.3.tgz",
- "integrity": "sha1-M1bMoZyIlUTy16le1JzlCKDs9VI=",
- "dev": true
- },
- "chokidar": {
- "version": "3.4.2",
- "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-3.4.2.tgz",
- "integrity": "sha1-ONyOZY3sOAl0HrPve7Ckf+QkIy0=",
- "dev": true,
- "optional": true,
- "requires": {
- "anymatch": "~3.1.1",
- "braces": "~3.0.2",
- "fsevents": "~2.1.2",
- "glob-parent": "~5.1.0",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.4.0"
- },
- "dependencies": {
- "braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npm.taobao.org/braces/download/braces-3.0.2.tgz",
- "integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=",
- "dev": true,
- "optional": true,
- "requires": {
- "fill-range": "^7.0.1"
- }
- },
- "fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-7.0.1.tgz",
- "integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=",
- "dev": true,
- "optional": true,
- "requires": {
- "to-regex-range": "^5.0.1"
- }
- },
- "is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npm.taobao.org/is-number/download/is-number-7.0.0.tgz",
- "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=",
- "dev": true,
- "optional": true
- },
- "to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-5.0.1.tgz",
- "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=",
- "dev": true,
- "optional": true,
- "requires": {
- "is-number": "^7.0.0"
- }
- }
- }
- },
- "chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/chownr/download/chownr-1.1.4.tgz",
- "integrity": "sha1-b8nXtC0ypYNZYzdmbn0ICE2izGs=",
- "dev": true
- },
- "chrome-trace-event": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/chrome-trace-event/download/chrome-trace-event-1.0.2.tgz",
- "integrity": "sha1-I0CQ7pfH1K0aLEvq4nUF3v/GCKQ=",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
- "ci-info": {
- "version": "1.6.0",
- "resolved": "https://registry.npm.taobao.org/ci-info/download/ci-info-1.6.0.tgz",
- "integrity": "sha1-LKINu5zrMtRSSmgzAzE/AwSx5Jc=",
- "dev": true
- },
- "cipher-base": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/cipher-base/download/cipher-base-1.0.4.tgz",
- "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
- "class-utils": {
- "version": "0.3.6",
- "resolved": "https://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz",
- "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=",
- "dev": true,
- "requires": {
- "arr-union": "^3.1.0",
- "define-property": "^0.2.5",
- "isobject": "^3.0.0",
- "static-extend": "^0.1.1"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- }
- }
- },
- "clean-css": {
- "version": "4.2.3",
- "resolved": "https://registry.npm.taobao.org/clean-css/download/clean-css-4.2.3.tgz",
- "integrity": "sha1-UHtd59l7SO5T2ErbAWD/YhY4D3g=",
- "dev": true,
- "requires": {
- "source-map": "~0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "clean-stack": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/clean-stack/download/clean-stack-2.2.0.tgz?cache=0&sync_timestamp=1592035524745&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fclean-stack%2Fdownload%2Fclean-stack-2.2.0.tgz",
- "integrity": "sha1-7oRy27Ep5yezHooQpCfe6d/kAIs=",
- "dev": true
- },
- "cli-cursor": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/cli-cursor/download/cli-cursor-2.1.0.tgz",
- "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
- "dev": true,
- "requires": {
- "restore-cursor": "^2.0.0"
- }
- },
- "cli-highlight": {
- "version": "2.1.4",
- "resolved": "https://registry.npm.taobao.org/cli-highlight/download/cli-highlight-2.1.4.tgz",
- "integrity": "sha1-CYy2Qs8X9CrcHBFF4H+WDsTXUis=",
- "dev": true,
- "requires": {
- "chalk": "^3.0.0",
- "highlight.js": "^9.6.0",
- "mz": "^2.4.0",
- "parse5": "^5.1.1",
- "parse5-htmlparser2-tree-adapter": "^5.1.1",
- "yargs": "^15.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.2.1.tgz",
- "integrity": "sha1-kK51xCTQCNJiTFvynq0xd+v881k=",
- "dev": true,
- "requires": {
- "@types/color-name": "^1.1.1",
- "color-convert": "^2.0.1"
- }
- },
- "chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-3.0.0.tgz",
- "integrity": "sha1-P3PCv1JlkfV0zEksUeJFY0n4ROQ=",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
- "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
- "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
- "dev": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
- "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
- "dev": true
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.1.0.tgz",
- "integrity": "sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
- "cli-spinners": {
- "version": "2.4.0",
- "resolved": "https://registry.npm.taobao.org/cli-spinners/download/cli-spinners-2.4.0.tgz?cache=0&sync_timestamp=1595080377121&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcli-spinners%2Fdownload%2Fcli-spinners-2.4.0.tgz",
- "integrity": "sha1-xiVtsha4eM+6RyDnGc7Hz3JoXX8=",
- "dev": true
- },
- "cli-width": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/cli-width/download/cli-width-3.0.0.tgz",
- "integrity": "sha1-ovSEN6LKqaIkNueUvwceyeYc7fY=",
- "dev": true
- },
- "clipboardy": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/clipboardy/download/clipboardy-2.3.0.tgz",
- "integrity": "sha1-PCkDZQxo5GqRs4iYW8J3QofbopA=",
- "dev": true,
- "requires": {
- "arch": "^2.1.1",
- "execa": "^1.0.0",
- "is-wsl": "^2.1.1"
- },
- "dependencies": {
- "is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/is-wsl/download/is-wsl-2.2.0.tgz",
- "integrity": "sha1-dKTHbnfKn9P5MvKQwX6jJs0VcnE=",
- "dev": true,
- "requires": {
- "is-docker": "^2.0.0"
- }
- }
- }
- },
- "cliui": {
- "version": "6.0.0",
- "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-6.0.0.tgz?cache=0&sync_timestamp=1597608006561&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcliui%2Fdownload%2Fcliui-6.0.0.tgz",
- "integrity": "sha1-UR1wLAxOQcoVbX0OlgIfI+EyJbE=",
- "dev": true,
- "requires": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^6.2.0"
- }
- },
- "clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/clone/download/clone-1.0.4.tgz?cache=0&sync_timestamp=1589682821772&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fclone%2Fdownload%2Fclone-1.0.4.tgz",
- "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
- "dev": true
- },
- "coa": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/coa/download/coa-2.0.2.tgz",
- "integrity": "sha1-Q/bCEVG07yv1cYfbDXPeIp4+fsM=",
- "dev": true,
- "requires": {
- "@types/q": "^1.5.1",
- "chalk": "^2.4.1",
- "q": "^1.1.2"
- }
- },
- "collection-visit": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/collection-visit/download/collection-visit-1.0.0.tgz",
- "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
- "dev": true,
- "requires": {
- "map-visit": "^1.0.0",
- "object-visit": "^1.0.0"
- }
- },
- "color": {
- "version": "3.1.2",
- "resolved": "https://registry.npm.taobao.org/color/download/color-3.1.2.tgz",
- "integrity": "sha1-aBSOf4XUGtdknF+oyBBvCY0inhA=",
- "dev": true,
- "requires": {
- "color-convert": "^1.9.1",
- "color-string": "^1.5.2"
- }
- },
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-1.9.3.tgz",
- "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=",
- "dev": true,
- "requires": {
- "color-name": "1.1.3"
- }
- },
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
- "dev": true
- },
- "color-string": {
- "version": "1.5.3",
- "resolved": "https://registry.npm.taobao.org/color-string/download/color-string-1.5.3.tgz",
- "integrity": "sha1-ybvF8BtYtUkvPWhXRZy2WQziBMw=",
- "dev": true,
- "requires": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
- "colorette": {
- "version": "1.2.1",
- "resolved": "https://registry.npm.taobao.org/colorette/download/colorette-1.2.1.tgz?cache=0&sync_timestamp=1593955763917&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolorette%2Fdownload%2Fcolorette-1.2.1.tgz",
- "integrity": "sha1-TQuSEyXBT6+SYzCGpTbbbolWSxs=",
- "dev": true
- },
- "combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.8.tgz",
- "integrity": "sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=",
- "dev": true,
- "requires": {
- "delayed-stream": "~1.0.0"
- }
- },
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz",
- "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=",
- "dev": true
- },
- "commondir": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz",
- "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
- "dev": true
- },
- "component-emitter": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/component-emitter/download/component-emitter-1.3.0.tgz",
- "integrity": "sha1-FuQHD7qK4ptnnyIVhT7hgasuq8A=",
- "dev": true
- },
- "compressible": {
- "version": "2.0.18",
- "resolved": "https://registry.npm.taobao.org/compressible/download/compressible-2.0.18.tgz",
- "integrity": "sha1-r1PMprBw1MPAdQ+9dyhqbXzEb7o=",
- "dev": true,
- "requires": {
- "mime-db": ">= 1.43.0 < 2"
- }
- },
- "compression": {
- "version": "1.7.4",
- "resolved": "https://registry.npm.taobao.org/compression/download/compression-1.7.4.tgz",
- "integrity": "sha1-lVI+/xcMpXwpoMpB5v4TH0Hlu48=",
- "dev": true,
- "requires": {
- "accepts": "~1.3.5",
- "bytes": "3.0.0",
- "compressible": "~2.0.16",
- "debug": "2.6.9",
- "on-headers": "~1.0.2",
- "safe-buffer": "5.1.2",
- "vary": "~1.1.2"
- },
- "dependencies": {
- "bytes": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/bytes/download/bytes-3.0.0.tgz?cache=0&sync_timestamp=1589682741197&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbytes%2Fdownload%2Fbytes-3.0.0.tgz",
- "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
- "dev": true
- },
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npm.taobao.org/concat-stream/download/concat-stream-1.6.2.tgz?cache=0&sync_timestamp=1589682751334&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fconcat-stream%2Fdownload%2Fconcat-stream-1.6.2.tgz",
- "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
- },
- "connect-history-api-fallback": {
- "version": "1.6.0",
- "resolved": "https://registry.npm.taobao.org/connect-history-api-fallback/download/connect-history-api-fallback-1.6.0.tgz",
- "integrity": "sha1-izIIk1kwjRERFdgcrT/Oq4iPl7w=",
- "dev": true
- },
- "console-browserify": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/console-browserify/download/console-browserify-1.2.0.tgz",
- "integrity": "sha1-ZwY871fOts9Jk6KrOlWECujEkzY=",
- "dev": true
- },
- "consolidate": {
- "version": "0.15.1",
- "resolved": "https://registry.npm.taobao.org/consolidate/download/consolidate-0.15.1.tgz",
- "integrity": "sha1-IasEMjXHGgfUXZqtmFk7DbpWurc=",
- "dev": true,
- "requires": {
- "bluebird": "^3.1.1"
- }
- },
- "constants-browserify": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/constants-browserify/download/constants-browserify-1.0.0.tgz?cache=0&sync_timestamp=1589682802723&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fconstants-browserify%2Fdownload%2Fconstants-browserify-1.0.0.tgz",
- "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
- "dev": true
- },
- "content-disposition": {
- "version": "0.5.3",
- "resolved": "https://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz",
- "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=",
- "dev": true,
- "requires": {
- "safe-buffer": "5.1.2"
- }
- },
- "content-type": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz",
- "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=",
- "dev": true
- },
- "convert-source-map": {
- "version": "1.7.0",
- "resolved": "https://registry.npm.taobao.org/convert-source-map/download/convert-source-map-1.7.0.tgz?cache=0&sync_timestamp=1589682764242&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fconvert-source-map%2Fdownload%2Fconvert-source-map-1.7.0.tgz",
- "integrity": "sha1-F6LLiC1/d9NJBYXizmxSRCSjpEI=",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.1.1"
- }
- },
- "cookie": {
- "version": "0.4.0",
- "resolved": "https://registry.npm.taobao.org/cookie/download/cookie-0.4.0.tgz",
- "integrity": "sha1-vrQ35wIrO21JAZ0IhmUwPr6cFLo=",
- "dev": true
- },
- "cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npm.taobao.org/cookie-signature/download/cookie-signature-1.0.6.tgz",
- "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
- "dev": true
- },
- "copy-concurrently": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/copy-concurrently/download/copy-concurrently-1.0.5.tgz",
- "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=",
- "dev": true,
- "requires": {
- "aproba": "^1.1.1",
- "fs-write-stream-atomic": "^1.0.8",
- "iferr": "^0.1.5",
- "mkdirp": "^0.5.1",
- "rimraf": "^2.5.4",
- "run-queue": "^1.0.0"
- }
- },
- "copy-descriptor": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/copy-descriptor/download/copy-descriptor-0.1.1.tgz",
- "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
- "dev": true
- },
- "copy-webpack-plugin": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/copy-webpack-plugin/download/copy-webpack-plugin-5.1.1.tgz",
- "integrity": "sha1-VIGgPeoRI9iKmIxv+LeCRyFPC4g=",
- "dev": true,
- "requires": {
- "cacache": "^12.0.3",
- "find-cache-dir": "^2.1.0",
- "glob-parent": "^3.1.0",
- "globby": "^7.1.1",
- "is-glob": "^4.0.1",
- "loader-utils": "^1.2.3",
- "minimatch": "^3.0.4",
- "normalize-path": "^3.0.0",
- "p-limit": "^2.2.1",
- "schema-utils": "^1.0.0",
- "serialize-javascript": "^2.1.2",
- "webpack-log": "^2.0.0"
- },
- "dependencies": {
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "requires": {
- "is-glob": "^3.1.0",
- "path-dirname": "^1.0.0"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.0"
- }
- }
- }
- },
- "globby": {
- "version": "7.1.1",
- "resolved": "https://registry.npm.taobao.org/globby/download/globby-7.1.1.tgz?cache=0&sync_timestamp=1591083812416&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglobby%2Fdownload%2Fglobby-7.1.1.tgz",
- "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
- "dev": true,
- "requires": {
- "array-union": "^1.0.1",
- "dir-glob": "^2.0.0",
- "glob": "^7.1.2",
- "ignore": "^3.3.5",
- "pify": "^3.0.0",
- "slash": "^1.0.0"
- }
- },
- "ignore": {
- "version": "3.3.10",
- "resolved": "https://registry.npm.taobao.org/ignore/download/ignore-3.3.10.tgz",
- "integrity": "sha1-Cpf7h2mG6AgcYxFg+PnziRV/AEM=",
- "dev": true
- },
- "pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
- },
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- },
- "serialize-javascript": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/serialize-javascript/download/serialize-javascript-2.1.2.tgz",
- "integrity": "sha1-7OxTsOAxe9yV73arcHS3OEeF+mE=",
- "dev": true
- },
- "slash": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/slash/download/slash-1.0.0.tgz?cache=0&sync_timestamp=1589682715547&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fslash%2Fdownload%2Fslash-1.0.0.tgz",
- "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
- "dev": true
- }
- }
- },
- "core-js": {
- "version": "3.6.5",
- "resolved": "https://registry.npm.taobao.org/core-js/download/core-js-3.6.5.tgz?cache=0&sync_timestamp=1589682726446&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcore-js%2Fdownload%2Fcore-js-3.6.5.tgz",
- "integrity": "sha1-c5XcJzrzf7LlDpvT2f6EEoUjHRo="
- },
- "core-js-compat": {
- "version": "3.6.5",
- "resolved": "https://registry.npm.taobao.org/core-js-compat/download/core-js-compat-3.6.5.tgz",
- "integrity": "sha1-KlHZpOJd/W5pAlGqgfmePAVIHxw=",
- "dev": true,
- "requires": {
- "browserslist": "^4.8.5",
- "semver": "7.0.0"
- },
- "dependencies": {
- "semver": {
- "version": "7.0.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-7.0.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.0.0.tgz",
- "integrity": "sha1-XzyjV2HkfgWyBsba/yz4FPAxa44=",
- "dev": true
- }
- }
- },
- "core-util-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
- "dev": true
- },
- "cosmiconfig": {
- "version": "5.2.1",
- "resolved": "https://registry.npm.taobao.org/cosmiconfig/download/cosmiconfig-5.2.1.tgz?cache=0&sync_timestamp=1596310819353&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcosmiconfig%2Fdownload%2Fcosmiconfig-5.2.1.tgz",
- "integrity": "sha1-BA9yaAnFked6F8CjYmykW08Wixo=",
- "dev": true,
- "requires": {
- "import-fresh": "^2.0.0",
- "is-directory": "^0.3.1",
- "js-yaml": "^3.13.1",
- "parse-json": "^4.0.0"
- },
- "dependencies": {
- "parse-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/parse-json/download/parse-json-4.0.0.tgz",
- "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
- "dev": true,
- "requires": {
- "error-ex": "^1.3.1",
- "json-parse-better-errors": "^1.0.1"
- }
- }
- }
- },
- "create-ecdh": {
- "version": "4.0.4",
- "resolved": "https://registry.npm.taobao.org/create-ecdh/download/create-ecdh-4.0.4.tgz",
- "integrity": "sha1-1uf0v/pmc2CFoHYv06YyaE2rzE4=",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "elliptic": "^6.5.3"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "create-hash": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/create-hash/download/create-hash-1.2.0.tgz",
- "integrity": "sha1-iJB4rxGmN1a8+1m9IhmWvjqe8ZY=",
- "dev": true,
- "requires": {
- "cipher-base": "^1.0.1",
- "inherits": "^2.0.1",
- "md5.js": "^1.3.4",
- "ripemd160": "^2.0.1",
- "sha.js": "^2.4.0"
- }
- },
- "create-hmac": {
- "version": "1.1.7",
- "resolved": "https://registry.npm.taobao.org/create-hmac/download/create-hmac-1.1.7.tgz",
- "integrity": "sha1-aRcMeLOrlXFHsriwRXLkfq0iQ/8=",
- "dev": true,
- "requires": {
- "cipher-base": "^1.0.3",
- "create-hash": "^1.1.0",
- "inherits": "^2.0.1",
- "ripemd160": "^2.0.0",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
- }
- },
- "cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-6.0.5.tgz",
- "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=",
- "dev": true,
- "requires": {
- "nice-try": "^1.0.4",
- "path-key": "^2.0.1",
- "semver": "^5.5.0",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
- "crypto-browserify": {
- "version": "3.12.0",
- "resolved": "https://registry.npm.taobao.org/crypto-browserify/download/crypto-browserify-3.12.0.tgz?cache=0&sync_timestamp=1589682788096&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcrypto-browserify%2Fdownload%2Fcrypto-browserify-3.12.0.tgz",
- "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=",
- "dev": true,
- "requires": {
- "browserify-cipher": "^1.0.0",
- "browserify-sign": "^4.0.0",
- "create-ecdh": "^4.0.0",
- "create-hash": "^1.1.0",
- "create-hmac": "^1.1.0",
- "diffie-hellman": "^5.0.0",
- "inherits": "^2.0.1",
- "pbkdf2": "^3.0.3",
- "public-encrypt": "^4.0.0",
- "randombytes": "^2.0.0",
- "randomfill": "^1.0.3"
- }
- },
- "css-color-names": {
- "version": "0.0.4",
- "resolved": "https://registry.npm.taobao.org/css-color-names/download/css-color-names-0.0.4.tgz",
- "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
- "dev": true
- },
- "css-declaration-sorter": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/css-declaration-sorter/download/css-declaration-sorter-4.0.1.tgz",
- "integrity": "sha1-wZiUD2OnbX42wecQGLABchBUyyI=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.1",
- "timsort": "^0.3.0"
- }
- },
- "css-loader": {
- "version": "3.6.0",
- "resolved": "https://registry.npm.taobao.org/css-loader/download/css-loader-3.6.0.tgz",
- "integrity": "sha1-Lkssfm4tJ/jI8o9hv/zS5ske9kU=",
- "dev": true,
- "requires": {
- "camelcase": "^5.3.1",
- "cssesc": "^3.0.0",
- "icss-utils": "^4.1.1",
- "loader-utils": "^1.2.3",
- "normalize-path": "^3.0.0",
- "postcss": "^7.0.32",
- "postcss-modules-extract-imports": "^2.0.0",
- "postcss-modules-local-by-default": "^3.0.2",
- "postcss-modules-scope": "^2.2.0",
- "postcss-modules-values": "^3.0.0",
- "postcss-value-parser": "^4.1.0",
- "schema-utils": "^2.7.0",
- "semver": "^6.3.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-5.3.1.tgz",
- "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=",
- "dev": true
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- }
- }
- },
- "css-select": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/css-select/download/css-select-2.1.0.tgz",
- "integrity": "sha1-ajRlM1ZjWTSoG6ymjQJVQyEF2+8=",
- "dev": true,
- "requires": {
- "boolbase": "^1.0.0",
- "css-what": "^3.2.1",
- "domutils": "^1.7.0",
- "nth-check": "^1.0.2"
- }
- },
- "css-select-base-adapter": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/css-select-base-adapter/download/css-select-base-adapter-0.1.1.tgz",
- "integrity": "sha1-Oy/0lyzDYquIVhUHqVQIoUMhNdc=",
- "dev": true
- },
- "css-tree": {
- "version": "1.0.0-alpha.37",
- "resolved": "https://registry.npm.taobao.org/css-tree/download/css-tree-1.0.0-alpha.37.tgz",
- "integrity": "sha1-mL69YsTB2flg7DQM+fdSLjBwmiI=",
- "dev": true,
- "requires": {
- "mdn-data": "2.0.4",
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "css-what": {
- "version": "3.3.0",
- "resolved": "https://registry.npm.taobao.org/css-what/download/css-what-3.3.0.tgz",
- "integrity": "sha1-EP7Glqns4uWRrHctdZqsq6w4zTk=",
- "dev": true
- },
- "cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/cssesc/download/cssesc-3.0.0.tgz",
- "integrity": "sha1-N3QZGZA7hoVl4cCep0dEXNGJg+4=",
- "dev": true
- },
- "cssnano": {
- "version": "4.1.10",
- "resolved": "https://registry.npm.taobao.org/cssnano/download/cssnano-4.1.10.tgz?cache=0&sync_timestamp=1597684732243&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcssnano%2Fdownload%2Fcssnano-4.1.10.tgz",
- "integrity": "sha1-CsQfCxPRPUZUh+ERt3jULaYxuLI=",
- "dev": true,
- "requires": {
- "cosmiconfig": "^5.0.0",
- "cssnano-preset-default": "^4.0.7",
- "is-resolvable": "^1.0.0",
- "postcss": "^7.0.0"
- }
- },
- "cssnano-preset-default": {
- "version": "4.0.7",
- "resolved": "https://registry.npm.taobao.org/cssnano-preset-default/download/cssnano-preset-default-4.0.7.tgz?cache=0&sync_timestamp=1597684732598&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcssnano-preset-default%2Fdownload%2Fcssnano-preset-default-4.0.7.tgz",
- "integrity": "sha1-UexmLM/KD4izltzZZ5zbkxvhf3Y=",
- "dev": true,
- "requires": {
- "css-declaration-sorter": "^4.0.1",
- "cssnano-util-raw-cache": "^4.0.1",
- "postcss": "^7.0.0",
- "postcss-calc": "^7.0.1",
- "postcss-colormin": "^4.0.3",
- "postcss-convert-values": "^4.0.1",
- "postcss-discard-comments": "^4.0.2",
- "postcss-discard-duplicates": "^4.0.2",
- "postcss-discard-empty": "^4.0.1",
- "postcss-discard-overridden": "^4.0.1",
- "postcss-merge-longhand": "^4.0.11",
- "postcss-merge-rules": "^4.0.3",
- "postcss-minify-font-values": "^4.0.2",
- "postcss-minify-gradients": "^4.0.2",
- "postcss-minify-params": "^4.0.2",
- "postcss-minify-selectors": "^4.0.2",
- "postcss-normalize-charset": "^4.0.1",
- "postcss-normalize-display-values": "^4.0.2",
- "postcss-normalize-positions": "^4.0.2",
- "postcss-normalize-repeat-style": "^4.0.2",
- "postcss-normalize-string": "^4.0.2",
- "postcss-normalize-timing-functions": "^4.0.2",
- "postcss-normalize-unicode": "^4.0.1",
- "postcss-normalize-url": "^4.0.1",
- "postcss-normalize-whitespace": "^4.0.2",
- "postcss-ordered-values": "^4.1.2",
- "postcss-reduce-initial": "^4.0.3",
- "postcss-reduce-transforms": "^4.0.2",
- "postcss-svgo": "^4.0.2",
- "postcss-unique-selectors": "^4.0.1"
- }
- },
- "cssnano-util-get-arguments": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/cssnano-util-get-arguments/download/cssnano-util-get-arguments-4.0.0.tgz",
- "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=",
- "dev": true
- },
- "cssnano-util-get-match": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/cssnano-util-get-match/download/cssnano-util-get-match-4.0.0.tgz",
- "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=",
- "dev": true
- },
- "cssnano-util-raw-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/cssnano-util-raw-cache/download/cssnano-util-raw-cache-4.0.1.tgz",
- "integrity": "sha1-sm1f1fcqEd/np4RvtMZyYPlr8oI=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "cssnano-util-same-parent": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/cssnano-util-same-parent/download/cssnano-util-same-parent-4.0.1.tgz",
- "integrity": "sha1-V0CC+yhZ0ttDOFWDXZqEVuoYu/M=",
- "dev": true
- },
- "csso": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/csso/download/csso-4.0.3.tgz",
- "integrity": "sha1-DZmF3IUsfMKyys+74QeQFNGo6QM=",
- "dev": true,
- "requires": {
- "css-tree": "1.0.0-alpha.39"
- },
- "dependencies": {
- "css-tree": {
- "version": "1.0.0-alpha.39",
- "resolved": "https://registry.npm.taobao.org/css-tree/download/css-tree-1.0.0-alpha.39.tgz",
- "integrity": "sha1-K/8//huz93bPfu/ZHuXLp3oUnus=",
- "dev": true,
- "requires": {
- "mdn-data": "2.0.6",
- "source-map": "^0.6.1"
- }
- },
- "mdn-data": {
- "version": "2.0.6",
- "resolved": "https://registry.npm.taobao.org/mdn-data/download/mdn-data-2.0.6.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmdn-data%2Fdownload%2Fmdn-data-2.0.6.tgz",
- "integrity": "sha1-hS3GD8ql2qLoz2yRicRA7T4EKXg=",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "csstype": {
- "version": "2.6.13",
- "resolved": "https://registry.npm.taobao.org/csstype/download/csstype-2.6.13.tgz?cache=0&sync_timestamp=1596101681368&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcsstype%2Fdownload%2Fcsstype-2.6.13.tgz",
- "integrity": "sha1-pokwFbkOhN1uhdDjtEKh6E8tvg8="
- },
- "cyclist": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz",
- "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
- "dev": true
- },
- "dashdash": {
- "version": "1.14.1",
- "resolved": "https://registry.npm.taobao.org/dashdash/download/dashdash-1.14.1.tgz?cache=0&sync_timestamp=1589682745367&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdashdash%2Fdownload%2Fdashdash-1.14.1.tgz",
- "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
- "dev": true,
- "requires": {
- "assert-plus": "^1.0.0"
- }
- },
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-4.1.1.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-4.1.1.tgz",
- "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/decamelize/download/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
- },
- "decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npm.taobao.org/decode-uri-component/download/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
- "dev": true
- },
- "deep-equal": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/deep-equal/download/deep-equal-1.1.1.tgz?cache=0&sync_timestamp=1590399968773&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeep-equal%2Fdownload%2Fdeep-equal-1.1.1.tgz",
- "integrity": "sha1-tcmMlCzv+vfLBR4k4UNKJaLmB2o=",
- "dev": true,
- "requires": {
- "is-arguments": "^1.0.4",
- "is-date-object": "^1.0.1",
- "is-regex": "^1.0.4",
- "object-is": "^1.0.1",
- "object-keys": "^1.1.1",
- "regexp.prototype.flags": "^1.2.0"
- }
- },
- "deep-is": {
- "version": "0.1.3",
- "resolved": "https://registry.npm.taobao.org/deep-is/download/deep-is-0.1.3.tgz",
- "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
- "dev": true
- },
- "deepmerge": {
- "version": "1.5.2",
- "resolved": "https://registry.npm.taobao.org/deepmerge/download/deepmerge-1.5.2.tgz",
- "integrity": "sha1-EEmdhohEza1P7ghC34x/bwyVp1M=",
- "dev": true
- },
- "default-gateway": {
- "version": "5.0.5",
- "resolved": "https://registry.npm.taobao.org/default-gateway/download/default-gateway-5.0.5.tgz",
- "integrity": "sha1-T9a9XShV05s0zFpZUFSG6ar8mxA=",
- "dev": true,
- "requires": {
- "execa": "^3.3.0"
- },
- "dependencies": {
- "cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-7.0.3.tgz",
- "integrity": "sha1-9zqFudXUHQRVUcF34ogtSshXKKY=",
- "dev": true,
- "requires": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- }
- },
- "execa": {
- "version": "3.4.0",
- "resolved": "https://registry.npm.taobao.org/execa/download/execa-3.4.0.tgz",
- "integrity": "sha1-wI7UVQ72XYWPrCaf/IVyRG8364k=",
- "dev": true,
- "requires": {
- "cross-spawn": "^7.0.0",
- "get-stream": "^5.0.0",
- "human-signals": "^1.1.1",
- "is-stream": "^2.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^4.0.0",
- "onetime": "^5.1.0",
- "p-finally": "^2.0.0",
- "signal-exit": "^3.0.2",
- "strip-final-newline": "^2.0.0"
- }
- },
- "get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/get-stream/download/get-stream-5.2.0.tgz?cache=0&sync_timestamp=1597056502934&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fget-stream%2Fdownload%2Fget-stream-5.2.0.tgz",
- "integrity": "sha1-SWaheV7lrOZecGxLe+txJX1uItM=",
- "dev": true,
- "requires": {
- "pump": "^3.0.0"
- }
- },
- "is-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-stream/download/is-stream-2.0.0.tgz",
- "integrity": "sha1-venDJoDW+uBBKdasnZIc54FfeOM=",
- "dev": true
- },
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/mimic-fn/download/mimic-fn-2.1.0.tgz?cache=0&sync_timestamp=1596095644798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmimic-fn%2Fdownload%2Fmimic-fn-2.1.0.tgz",
- "integrity": "sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=",
- "dev": true
- },
- "npm-run-path": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/npm-run-path/download/npm-run-path-4.0.1.tgz",
- "integrity": "sha1-t+zR5e1T2o43pV4cImnguX7XSOo=",
- "dev": true,
- "requires": {
- "path-key": "^3.0.0"
- }
- },
- "onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npm.taobao.org/onetime/download/onetime-5.1.2.tgz?cache=0&sync_timestamp=1597005345612&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fonetime%2Fdownload%2Fonetime-5.1.2.tgz",
- "integrity": "sha1-0Oluu1awdHbfHdnEgG5SN5hcpF4=",
- "dev": true,
- "requires": {
- "mimic-fn": "^2.1.0"
- }
- },
- "p-finally": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/p-finally/download/p-finally-2.0.1.tgz",
- "integrity": "sha1-vW/KqcVZoJa2gIBvTWV7Pw8kBWE=",
- "dev": true
- },
- "path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/path-key/download/path-key-3.1.1.tgz",
- "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=",
- "dev": true
- },
- "shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/shebang-command/download/shebang-command-2.0.0.tgz",
- "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=",
- "dev": true,
- "requires": {
- "shebang-regex": "^3.0.0"
- }
- },
- "shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/shebang-regex/download/shebang-regex-3.0.0.tgz",
- "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=",
- "dev": true
- },
- "which": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/which/download/which-2.0.2.tgz?cache=0&sync_timestamp=1589682812246&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwhich%2Fdownload%2Fwhich-2.0.2.tgz",
- "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=",
- "dev": true,
- "requires": {
- "isexe": "^2.0.0"
- }
- }
- }
- },
- "defaults": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/defaults/download/defaults-1.0.3.tgz",
- "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
- "dev": true,
- "requires": {
- "clone": "^1.0.2"
- }
- },
- "define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/define-properties/download/define-properties-1.1.3.tgz",
- "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=",
- "dev": true,
- "requires": {
- "object-keys": "^1.0.12"
- }
- },
- "define-property": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-2.0.2.tgz",
- "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.2",
- "isobject": "^3.0.1"
- },
- "dependencies": {
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-descriptor/download/is-descriptor-1.0.2.tgz",
- "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
- }
- },
- "del": {
- "version": "4.1.1",
- "resolved": "https://registry.npm.taobao.org/del/download/del-4.1.1.tgz?cache=0&sync_timestamp=1589682730753&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdel%2Fdownload%2Fdel-4.1.1.tgz",
- "integrity": "sha1-no8RciLqRKMf86FWwEm5kFKp8LQ=",
- "dev": true,
- "requires": {
- "@types/glob": "^7.1.1",
- "globby": "^6.1.0",
- "is-path-cwd": "^2.0.0",
- "is-path-in-cwd": "^2.0.0",
- "p-map": "^2.0.0",
- "pify": "^4.0.1",
- "rimraf": "^2.6.3"
- },
- "dependencies": {
- "globby": {
- "version": "6.1.0",
- "resolved": "https://registry.npm.taobao.org/globby/download/globby-6.1.0.tgz?cache=0&sync_timestamp=1591083812416&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglobby%2Fdownload%2Fglobby-6.1.0.tgz",
- "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
- "dev": true,
- "requires": {
- "array-union": "^1.0.1",
- "glob": "^7.0.3",
- "object-assign": "^4.0.1",
- "pify": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/pify/download/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
- "p-map": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/p-map/download/p-map-2.1.0.tgz",
- "integrity": "sha1-MQko/u+cnsxltosXaTAYpmXOoXU=",
- "dev": true
- }
- }
- },
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/delayed-stream/download/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
- },
- "depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "dev": true
- },
- "des.js": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/des.js/download/des.js-1.0.1.tgz",
- "integrity": "sha1-U4IULhvcU/hdhtU+X0qn3rkeCEM=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0"
- }
- },
- "destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
- "dev": true
- },
- "detect-node": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/detect-node/download/detect-node-2.0.4.tgz",
- "integrity": "sha1-AU7o+PZpxcWAI9pkuBecCDooxGw=",
- "dev": true
- },
- "diffie-hellman": {
- "version": "5.0.3",
- "resolved": "https://registry.npm.taobao.org/diffie-hellman/download/diffie-hellman-5.0.3.tgz",
- "integrity": "sha1-QOjumPVaIUlgcUaSHGPhrl89KHU=",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "miller-rabin": "^4.0.0",
- "randombytes": "^2.0.0"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "dir-glob": {
- "version": "2.2.2",
- "resolved": "https://registry.npm.taobao.org/dir-glob/download/dir-glob-2.2.2.tgz",
- "integrity": "sha1-+gnwaUFTyJGLGLoN6vrpR2n8UMQ=",
- "dev": true,
- "requires": {
- "path-type": "^3.0.0"
- }
- },
- "dns-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/dns-equal/download/dns-equal-1.0.0.tgz",
- "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
- "dev": true
- },
- "dns-packet": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/dns-packet/download/dns-packet-1.3.1.tgz",
- "integrity": "sha1-EqpCaYEHW+UAuRDu3NC0fdfe2lo=",
- "dev": true,
- "requires": {
- "ip": "^1.1.0",
- "safe-buffer": "^5.0.1"
- }
- },
- "dns-txt": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/dns-txt/download/dns-txt-2.0.2.tgz",
- "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
- "dev": true,
- "requires": {
- "buffer-indexof": "^1.0.0"
- }
- },
- "doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/doctrine/download/doctrine-3.0.0.tgz",
- "integrity": "sha1-rd6+rXKmV023g2OdyHoSF3OXOWE=",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
- "dom-converter": {
- "version": "0.2.0",
- "resolved": "https://registry.npm.taobao.org/dom-converter/download/dom-converter-0.2.0.tgz",
- "integrity": "sha1-ZyGp2u4uKTaClVtq/kFncWJ7t2g=",
- "dev": true,
- "requires": {
- "utila": "~0.4"
- }
- },
- "dom-serializer": {
- "version": "0.2.2",
- "resolved": "https://registry.npm.taobao.org/dom-serializer/download/dom-serializer-0.2.2.tgz",
- "integrity": "sha1-GvuB9TNxcXXUeGVd68XjMtn5u1E=",
- "dev": true,
- "requires": {
- "domelementtype": "^2.0.1",
- "entities": "^2.0.0"
- },
- "dependencies": {
- "domelementtype": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-2.0.1.tgz",
- "integrity": "sha1-H4vf6R9aeAYydOgDtL3O326U+U0=",
- "dev": true
- }
- }
- },
- "domain-browser": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/domain-browser/download/domain-browser-1.2.0.tgz?cache=0&sync_timestamp=1597693536966&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomain-browser%2Fdownload%2Fdomain-browser-1.2.0.tgz",
- "integrity": "sha1-PTH1AZGmdJ3RN1p/Ui6CPULlTto=",
- "dev": true
- },
- "domelementtype": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-1.3.1.tgz",
- "integrity": "sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=",
- "dev": true
- },
- "domhandler": {
- "version": "2.4.2",
- "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-2.4.2.tgz",
- "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=",
- "dev": true,
- "requires": {
- "domelementtype": "1"
- }
- },
- "domutils": {
- "version": "1.7.0",
- "resolved": "https://registry.npm.taobao.org/domutils/download/domutils-1.7.0.tgz?cache=0&sync_timestamp=1597680509643&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomutils%2Fdownload%2Fdomutils-1.7.0.tgz",
- "integrity": "sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=",
- "dev": true,
- "requires": {
- "dom-serializer": "0",
- "domelementtype": "1"
- }
- },
- "dot-prop": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/dot-prop/download/dot-prop-5.2.0.tgz?cache=0&sync_timestamp=1597574828045&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdot-prop%2Fdownload%2Fdot-prop-5.2.0.tgz",
- "integrity": "sha1-w07MKVVtxF8fTCJpe29JBODMT8s=",
- "dev": true,
- "requires": {
- "is-obj": "^2.0.0"
- }
- },
- "dotenv": {
- "version": "8.2.0",
- "resolved": "https://registry.npm.taobao.org/dotenv/download/dotenv-8.2.0.tgz",
- "integrity": "sha1-l+YZJZradQ7qPk6j4mvO6lQksWo=",
- "dev": true
- },
- "dotenv-expand": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/dotenv-expand/download/dotenv-expand-5.1.0.tgz",
- "integrity": "sha1-P7rwIL/XlIhAcuomsel5HUWmKfA=",
- "dev": true
- },
- "duplexer": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/duplexer/download/duplexer-0.1.2.tgz",
- "integrity": "sha1-Or5DrvODX4rgd9E23c4PJ2sEAOY=",
- "dev": true
- },
- "duplexify": {
- "version": "3.7.1",
- "resolved": "https://registry.npm.taobao.org/duplexify/download/duplexify-3.7.1.tgz",
- "integrity": "sha1-Kk31MX9sz9kfhtb9JdjYoQO4gwk=",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.0.0",
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.0",
- "stream-shift": "^1.0.0"
- }
- },
- "easy-stack": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/easy-stack/download/easy-stack-1.0.0.tgz",
- "integrity": "sha1-EskbMIWjfwuqM26UhurEv5Tj54g=",
- "dev": true
- },
- "ecc-jsbn": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/ecc-jsbn/download/ecc-jsbn-0.1.2.tgz?cache=0&sync_timestamp=1589682745945&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fecc-jsbn%2Fdownload%2Fecc-jsbn-0.1.2.tgz",
- "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
- "dev": true,
- "requires": {
- "jsbn": "~0.1.0",
- "safer-buffer": "^2.1.0"
- }
- },
- "ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
- "dev": true
- },
- "ejs": {
- "version": "2.7.4",
- "resolved": "https://registry.npm.taobao.org/ejs/download/ejs-2.7.4.tgz?cache=0&sync_timestamp=1597678506855&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fejs%2Fdownload%2Fejs-2.7.4.tgz",
- "integrity": "sha1-SGYSh1c9zFPjZsehrlLDoSDuybo=",
- "dev": true
- },
- "electron-to-chromium": {
- "version": "1.3.537",
- "resolved": "https://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.537.tgz?cache=0&sync_timestamp=1597808026999&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felectron-to-chromium%2Fdownload%2Felectron-to-chromium-1.3.537.tgz",
- "integrity": "sha1-3+WV9Sg9MRPfiXFYgQ5A9sI1UoM=",
- "dev": true
- },
- "elliptic": {
- "version": "6.5.3",
- "resolved": "https://registry.npm.taobao.org/elliptic/download/elliptic-6.5.3.tgz",
- "integrity": "sha1-y1nrLv2vc6C9eMzXAVpirW4Pk9Y=",
- "dev": true,
- "requires": {
- "bn.js": "^4.4.0",
- "brorand": "^1.0.1",
- "hash.js": "^1.0.0",
- "hmac-drbg": "^1.0.0",
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.0"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-8.0.0.tgz",
- "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=",
- "dev": true
- },
- "emojis-list": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/emojis-list/download/emojis-list-3.0.0.tgz",
- "integrity": "sha1-VXBmIEatKeLpFucariYKvf9Pang=",
- "dev": true
- },
- "encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
- "dev": true
- },
- "end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npm.taobao.org/end-of-stream/download/end-of-stream-1.4.4.tgz",
- "integrity": "sha1-WuZKX0UFe682JuwU2gyl5LJDHrA=",
- "dev": true,
- "requires": {
- "once": "^1.4.0"
- }
- },
- "enhanced-resolve": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/enhanced-resolve/download/enhanced-resolve-4.3.0.tgz",
- "integrity": "sha1-O4BvO/r8HsfeaVUe+TzKRsFwQSY=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "memory-fs": "^0.5.0",
- "tapable": "^1.0.0"
- },
- "dependencies": {
- "memory-fs": {
- "version": "0.5.0",
- "resolved": "https://registry.npm.taobao.org/memory-fs/download/memory-fs-0.5.0.tgz",
- "integrity": "sha1-MkwBKIuIZSlm0WHbd4OHIIRajjw=",
- "dev": true,
- "requires": {
- "errno": "^0.1.3",
- "readable-stream": "^2.0.1"
- }
- }
- }
- },
- "entities": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/entities/download/entities-2.0.3.tgz",
- "integrity": "sha1-XEh+V0Krk8Fau12iJ1m4WQ7AO38=",
- "dev": true
- },
- "errno": {
- "version": "0.1.7",
- "resolved": "https://registry.npm.taobao.org/errno/download/errno-0.1.7.tgz",
- "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=",
- "dev": true,
- "requires": {
- "prr": "~1.0.1"
- }
- },
- "error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npm.taobao.org/error-ex/download/error-ex-1.3.2.tgz",
- "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=",
- "dev": true,
- "requires": {
- "is-arrayish": "^0.2.1"
- }
- },
- "error-stack-parser": {
- "version": "2.0.6",
- "resolved": "https://registry.npm.taobao.org/error-stack-parser/download/error-stack-parser-2.0.6.tgz",
- "integrity": "sha1-WpmnB716TFinl5AtSNgoA+3mqtg=",
- "dev": true,
- "requires": {
- "stackframe": "^1.1.1"
- }
- },
- "es-abstract": {
- "version": "1.17.6",
- "resolved": "https://registry.npm.taobao.org/es-abstract/download/es-abstract-1.17.6.tgz?cache=0&sync_timestamp=1597446082160&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fes-abstract%2Fdownload%2Fes-abstract-1.17.6.tgz",
- "integrity": "sha1-kUIHFweFeyysx7iey2cDFsPi1So=",
- "dev": true,
- "requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1",
- "is-callable": "^1.2.0",
- "is-regex": "^1.1.0",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimend": "^1.0.1",
- "string.prototype.trimstart": "^1.0.1"
- }
- },
- "es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npm.taobao.org/es-to-primitive/download/es-to-primitive-1.2.1.tgz",
- "integrity": "sha1-5VzUyc3BiLzvsDs2bHNjI/xciYo=",
- "dev": true,
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- }
- },
- "escalade": {
- "version": "3.0.2",
- "resolved": "https://registry.npm.taobao.org/escalade/download/escalade-3.0.2.tgz?cache=0&sync_timestamp=1594742958135&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fescalade%2Fdownload%2Fescalade-3.0.2.tgz",
- "integrity": "sha1-algNcO24eIDyK0yR0NVgeN9pYsQ=",
- "dev": true
- },
- "escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
- "dev": true
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "dev": true
- },
- "eslint": {
- "version": "6.8.0",
- "resolved": "https://registry.npm.taobao.org/eslint/download/eslint-6.8.0.tgz",
- "integrity": "sha1-YiYtZylzn5J1cjgkMC+yJ8jJP/s=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "ajv": "^6.10.0",
- "chalk": "^2.1.0",
- "cross-spawn": "^6.0.5",
- "debug": "^4.0.1",
- "doctrine": "^3.0.0",
- "eslint-scope": "^5.0.0",
- "eslint-utils": "^1.4.3",
- "eslint-visitor-keys": "^1.1.0",
- "espree": "^6.1.2",
- "esquery": "^1.0.1",
- "esutils": "^2.0.2",
- "file-entry-cache": "^5.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^5.0.0",
- "globals": "^12.1.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.0.0",
- "imurmurhash": "^0.1.4",
- "inquirer": "^7.0.0",
- "is-glob": "^4.0.0",
- "js-yaml": "^3.13.1",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.3.0",
- "lodash": "^4.17.14",
- "minimatch": "^3.0.4",
- "mkdirp": "^0.5.1",
- "natural-compare": "^1.4.0",
- "optionator": "^0.8.3",
- "progress": "^2.0.0",
- "regexpp": "^2.0.1",
- "semver": "^6.1.2",
- "strip-ansi": "^5.2.0",
- "strip-json-comments": "^3.0.1",
- "table": "^5.2.3",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
- },
- "dependencies": {
- "eslint-scope": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/eslint-scope/download/eslint-scope-5.1.0.tgz?cache=0&sync_timestamp=1591269986906&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-scope%2Fdownload%2Feslint-scope-5.1.0.tgz",
- "integrity": "sha1-0Plx3+WcaeDK2mhLI9Sdv4JgDOU=",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- },
- "globals": {
- "version": "12.4.0",
- "resolved": "https://registry.npm.taobao.org/globals/download/globals-12.4.0.tgz",
- "integrity": "sha1-oYgTV2pBsAokqX5/gVkYwuGZJfg=",
- "dev": true,
- "requires": {
- "type-fest": "^0.8.1"
- }
- },
- "import-fresh": {
- "version": "3.2.1",
- "resolved": "https://registry.npm.taobao.org/import-fresh/download/import-fresh-3.2.1.tgz?cache=0&sync_timestamp=1589682760620&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fimport-fresh%2Fdownload%2Fimport-fresh-3.2.1.tgz",
- "integrity": "sha1-Yz/2GFBueTr1rJG/SLcmd+FcvmY=",
- "dev": true,
- "requires": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- }
- },
- "resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/resolve-from/download/resolve-from-4.0.0.tgz",
- "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=",
- "dev": true
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- },
- "type-fest": {
- "version": "0.8.1",
- "resolved": "https://registry.npm.taobao.org/type-fest/download/type-fest-0.8.1.tgz",
- "integrity": "sha1-CeJJ696FHTseSNJ8EFREZn8XuD0=",
- "dev": true
- }
- }
- },
- "eslint-loader": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/eslint-loader/download/eslint-loader-2.2.1.tgz",
- "integrity": "sha1-KLnBLaVAV68IReKmEScBova/gzc=",
- "dev": true,
- "requires": {
- "loader-fs-cache": "^1.0.0",
- "loader-utils": "^1.0.2",
- "object-assign": "^4.0.1",
- "object-hash": "^1.1.4",
- "rimraf": "^2.6.1"
- }
- },
- "eslint-plugin-vue": {
- "version": "7.0.0-beta.2",
- "resolved": "https://registry.npm.taobao.org/eslint-plugin-vue/download/eslint-plugin-vue-7.0.0-beta.2.tgz?cache=0&sync_timestamp=1597198168566&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-plugin-vue%2Fdownload%2Feslint-plugin-vue-7.0.0-beta.2.tgz",
- "integrity": "sha1-SCiSPsJBkwYPgFZ7+MFbGOE5aLM=",
- "dev": true,
- "requires": {
- "eslint-utils": "^2.1.0",
- "natural-compare": "^1.4.0",
- "semver": "^7.3.2",
- "vue-eslint-parser": "^7.1.0"
- },
- "dependencies": {
- "eslint-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/eslint-utils/download/eslint-utils-2.1.0.tgz",
- "integrity": "sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc=",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^1.1.0"
- }
- },
- "semver": {
- "version": "7.3.2",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-7.3.2.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.3.2.tgz",
- "integrity": "sha1-YElisFK4HtB4aq6EOJ/7pw/9OTg=",
- "dev": true
- }
- }
- },
- "eslint-scope": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/eslint-scope/download/eslint-scope-4.0.3.tgz?cache=0&sync_timestamp=1591269986906&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-scope%2Fdownload%2Feslint-scope-4.0.3.tgz",
- "integrity": "sha1-ygODMxD2iJoyZHgaqC5j65z+eEg=",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- },
- "eslint-utils": {
- "version": "1.4.3",
- "resolved": "https://registry.npm.taobao.org/eslint-utils/download/eslint-utils-1.4.3.tgz",
- "integrity": "sha1-dP7HxU0Hdrb2fgJRBAtYBlZOmB8=",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^1.1.0"
- }
- },
- "eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/eslint-visitor-keys/download/eslint-visitor-keys-1.3.0.tgz?cache=0&sync_timestamp=1597435068105&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-visitor-keys%2Fdownload%2Feslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha1-MOvR73wv3/AcOk8VEESvJfqwUj4=",
- "dev": true
- },
- "espree": {
- "version": "6.2.1",
- "resolved": "https://registry.npm.taobao.org/espree/download/espree-6.2.1.tgz?cache=0&sync_timestamp=1595034145062&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fespree%2Fdownload%2Fespree-6.2.1.tgz",
- "integrity": "sha1-d/xy4f10SiBSwg84pbV1gy6Cc0o=",
- "dev": true,
- "requires": {
- "acorn": "^7.1.1",
- "acorn-jsx": "^5.2.0",
- "eslint-visitor-keys": "^1.1.0"
- },
- "dependencies": {
- "acorn": {
- "version": "7.4.0",
- "resolved": "https://registry.npm.taobao.org/acorn/download/acorn-7.4.0.tgz?cache=0&sync_timestamp=1597235774928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn%2Fdownload%2Facorn-7.4.0.tgz",
- "integrity": "sha1-4a1IbmxUUBY0xsOXxcEh2qODYHw=",
- "dev": true
- }
- }
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/esprima/download/esprima-4.0.1.tgz?cache=0&sync_timestamp=1589682833047&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fesprima%2Fdownload%2Fesprima-4.0.1.tgz",
- "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=",
- "dev": true
- },
- "esquery": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/esquery/download/esquery-1.3.1.tgz",
- "integrity": "sha1-t4tYKKqOIU4p+3TE1bdS4cAz2lc=",
- "dev": true,
- "requires": {
- "estraverse": "^5.1.0"
- },
- "dependencies": {
- "estraverse": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/estraverse/download/estraverse-5.2.0.tgz?cache=0&sync_timestamp=1596642998635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Festraverse%2Fdownload%2Festraverse-5.2.0.tgz",
- "integrity": "sha1-MH30JUfmzHMk088DwVXVzbjFOIA=",
- "dev": true
- }
- }
- },
- "esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/esrecurse/download/esrecurse-4.2.1.tgz",
- "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=",
- "dev": true,
- "requires": {
- "estraverse": "^4.1.0"
- }
- },
- "estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/estraverse/download/estraverse-4.3.0.tgz?cache=0&sync_timestamp=1596642998635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Festraverse%2Fdownload%2Festraverse-4.3.0.tgz",
- "integrity": "sha1-OYrT88WiSUi+dyXoPRGn3ijNvR0=",
- "dev": true
- },
- "estree-walker": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/estree-walker/download/estree-walker-2.0.1.tgz",
- "integrity": "sha1-+OAw+yHO+hg7RLetUWt0dDTno+A="
- },
- "esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/esutils/download/esutils-2.0.3.tgz?cache=0&sync_timestamp=1589682816934&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fesutils%2Fdownload%2Fesutils-2.0.3.tgz",
- "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=",
- "dev": true
- },
- "etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npm.taobao.org/etag/download/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
- "dev": true
- },
- "event-pubsub": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/event-pubsub/download/event-pubsub-4.3.0.tgz",
- "integrity": "sha1-9o2Ba8KfHsAsU53FjI3UDOcss24=",
- "dev": true
- },
- "eventemitter3": {
- "version": "4.0.4",
- "resolved": "https://registry.npm.taobao.org/eventemitter3/download/eventemitter3-4.0.4.tgz",
- "integrity": "sha1-tUY6zmNaCD0Bi9x8kXtMXxCoU4Q=",
- "dev": true
- },
- "events": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/events/download/events-3.2.0.tgz?cache=0&sync_timestamp=1595422577337&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fevents%2Fdownload%2Fevents-3.2.0.tgz",
- "integrity": "sha1-k7h8GPjvzUICpGGuxN/AVWtjk3k=",
- "dev": true
- },
- "eventsource": {
- "version": "1.0.7",
- "resolved": "https://registry.npm.taobao.org/eventsource/download/eventsource-1.0.7.tgz",
- "integrity": "sha1-j7xyyT/NNAiAkLwKTmT0tc7m2NA=",
- "dev": true,
- "requires": {
- "original": "^1.0.0"
- }
- },
- "evp_bytestokey": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/evp_bytestokey/download/evp_bytestokey-1.0.3.tgz",
- "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=",
- "dev": true,
- "requires": {
- "md5.js": "^1.3.4",
- "safe-buffer": "^5.1.1"
- }
- },
- "execa": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/execa/download/execa-1.0.0.tgz",
- "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=",
- "dev": true,
- "requires": {
- "cross-spawn": "^6.0.0",
- "get-stream": "^4.0.0",
- "is-stream": "^1.1.0",
- "npm-run-path": "^2.0.0",
- "p-finally": "^1.0.0",
- "signal-exit": "^3.0.0",
- "strip-eof": "^1.0.0"
- }
- },
- "expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npm.taobao.org/expand-brackets/download/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
- "dev": true,
- "requires": {
- "debug": "^2.3.3",
- "define-property": "^0.2.5",
- "extend-shallow": "^2.0.1",
- "posix-character-classes": "^0.1.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "express": {
- "version": "4.17.1",
- "resolved": "https://registry.npm.taobao.org/express/download/express-4.17.1.tgz?cache=0&sync_timestamp=1589682766604&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fexpress%2Fdownload%2Fexpress-4.17.1.tgz",
- "integrity": "sha1-RJH8OGBc9R+GKdOcK10Cb5ikwTQ=",
- "dev": true,
- "requires": {
- "accepts": "~1.3.7",
- "array-flatten": "1.1.1",
- "body-parser": "1.19.0",
- "content-disposition": "0.5.3",
- "content-type": "~1.0.4",
- "cookie": "0.4.0",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.1.2",
- "fresh": "0.5.2",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.5",
- "qs": "6.7.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.1.2",
- "send": "0.17.1",
- "serve-static": "1.14.1",
- "setprototypeof": "1.1.1",
- "statuses": "~1.5.0",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
- "qs": {
- "version": "6.7.0",
- "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
- "integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=",
- "dev": true
- }
- }
- },
- "extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npm.taobao.org/extend/download/extend-3.0.2.tgz?cache=0&sync_timestamp=1589682707348&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fextend%2Fdownload%2Fextend-3.0.2.tgz",
- "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=",
- "dev": true
- },
- "extend-shallow": {
- "version": "3.0.2",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-3.0.2.tgz",
- "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
- "dev": true,
- "requires": {
- "assign-symbols": "^1.0.0",
- "is-extendable": "^1.0.1"
- },
- "dependencies": {
- "is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/is-extendable/download/is-extendable-1.0.1.tgz",
- "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=",
- "dev": true,
- "requires": {
- "is-plain-object": "^2.0.4"
- }
- }
- }
- },
- "external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/external-editor/download/external-editor-3.1.0.tgz",
- "integrity": "sha1-ywP3QL764D6k0oPK7SdBqD8zVJU=",
- "dev": true,
- "requires": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- }
- },
- "extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/extglob/download/extglob-2.0.4.tgz",
- "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=",
- "dev": true,
- "requires": {
- "array-unique": "^0.3.2",
- "define-property": "^1.0.0",
- "expand-brackets": "^2.1.4",
- "extend-shallow": "^2.0.1",
- "fragment-cache": "^0.2.1",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.0"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-descriptor/download/is-descriptor-1.0.2.tgz",
- "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
- }
- },
- "extsprintf": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/extsprintf/download/extsprintf-1.3.0.tgz",
- "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
- "dev": true
- },
- "fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npm.taobao.org/fast-deep-equal/download/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=",
- "dev": true
- },
- "fast-glob": {
- "version": "2.2.7",
- "resolved": "https://registry.npm.taobao.org/fast-glob/download/fast-glob-2.2.7.tgz?cache=0&sync_timestamp=1592291968616&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffast-glob%2Fdownload%2Ffast-glob-2.2.7.tgz",
- "integrity": "sha1-aVOFfDr6R1//ku5gFdUtpwpM050=",
- "dev": true,
- "requires": {
- "@mrmlnc/readdir-enhanced": "^2.2.1",
- "@nodelib/fs.stat": "^1.1.2",
- "glob-parent": "^3.1.0",
- "is-glob": "^4.0.0",
- "merge2": "^1.2.3",
- "micromatch": "^3.1.10"
- },
- "dependencies": {
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "requires": {
- "is-glob": "^3.1.0",
- "path-dirname": "^1.0.0"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.0"
- }
- }
- }
- }
- }
- },
- "fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/fast-json-stable-stringify/download/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=",
- "dev": true
- },
- "fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
- "dev": true
- },
- "faye-websocket": {
- "version": "0.10.0",
- "resolved": "https://registry.npm.taobao.org/faye-websocket/download/faye-websocket-0.10.0.tgz",
- "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
- "dev": true,
- "requires": {
- "websocket-driver": ">=0.5.1"
- }
- },
- "figgy-pudding": {
- "version": "3.5.2",
- "resolved": "https://registry.npm.taobao.org/figgy-pudding/download/figgy-pudding-3.5.2.tgz",
- "integrity": "sha1-tO7oFIq7Adzx0aw0Nn1Z4S+mHW4=",
- "dev": true
- },
- "figures": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/figures/download/figures-3.2.0.tgz",
- "integrity": "sha1-YlwYvSk8YE3EqN2y/r8MiDQXRq8=",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^1.0.5"
- }
- },
- "file-entry-cache": {
- "version": "5.0.1",
- "resolved": "https://registry.npm.taobao.org/file-entry-cache/download/file-entry-cache-5.0.1.tgz",
- "integrity": "sha1-yg9u+m3T1WEzP7FFFQZcL6/fQ5w=",
- "dev": true,
- "requires": {
- "flat-cache": "^2.0.1"
- }
- },
- "file-loader": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/file-loader/download/file-loader-4.3.0.tgz",
- "integrity": "sha1-eA8ED3KbPRgBnyBgX3I+hEuKWK8=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.2.3",
- "schema-utils": "^2.5.0"
- }
- },
- "file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha1-VTp7hEb/b2hDWcRF8eN6BdrMM90=",
- "dev": true,
- "optional": true
- },
- "filesize": {
- "version": "3.6.1",
- "resolved": "https://registry.npm.taobao.org/filesize/download/filesize-3.6.1.tgz",
- "integrity": "sha1-CQuz7gG2+AGoqL6Z0xcQs0Irsxc=",
- "dev": true
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-number": "^3.0.0",
- "repeat-string": "^1.6.1",
- "to-regex-range": "^2.1.0"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- }
- }
- },
- "finalhandler": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/finalhandler/download/finalhandler-1.1.2.tgz",
- "integrity": "sha1-t+fQAP/RGTjQ/bBTUG9uur6fWH0=",
- "dev": true,
- "requires": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "~2.3.0",
- "parseurl": "~1.3.3",
- "statuses": "~1.5.0",
- "unpipe": "~1.0.0"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "find-cache-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-2.1.0.tgz",
- "integrity": "sha1-jQ+UzRP+Q8bHwmGg2GEVypGMBfc=",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "make-dir": "^2.0.0",
- "pkg-dir": "^3.0.0"
- }
- },
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-3.0.0.tgz?cache=0&sync_timestamp=1597169842138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-up%2Fdownload%2Ffind-up-3.0.0.tgz",
- "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=",
- "dev": true,
- "requires": {
- "locate-path": "^3.0.0"
- }
- },
- "flat-cache": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/flat-cache/download/flat-cache-2.0.1.tgz",
- "integrity": "sha1-XSltbwS9pEpGMKMBQTvbwuwIXsA=",
- "dev": true,
- "requires": {
- "flatted": "^2.0.0",
- "rimraf": "2.6.3",
- "write": "1.0.3"
- },
- "dependencies": {
- "rimraf": {
- "version": "2.6.3",
- "resolved": "https://registry.npm.taobao.org/rimraf/download/rimraf-2.6.3.tgz?cache=0&sync_timestamp=1589682814592&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Frimraf%2Fdownload%2Frimraf-2.6.3.tgz",
- "integrity": "sha1-stEE/g2Psnz54KHNqCYt04M8bKs=",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- }
- }
- },
- "flatted": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/flatted/download/flatted-2.0.2.tgz",
- "integrity": "sha1-RXWyHivO50NKqb5mL0t7X5wrUTg=",
- "dev": true
- },
- "flush-write-stream": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/flush-write-stream/download/flush-write-stream-1.1.1.tgz",
- "integrity": "sha1-jdfYc6G6vCB9lOrQwuDkQnbr8ug=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "readable-stream": "^2.3.6"
- }
- },
- "follow-redirects": {
- "version": "1.13.0",
- "resolved": "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.0.tgz?cache=0&sync_timestamp=1597057976909&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.13.0.tgz",
- "integrity": "sha1-tC6Nk6Kn7qXtiGM2dtZZe8jjhNs=",
- "dev": true
- },
- "for-in": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz",
- "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
- "dev": true
- },
- "forever-agent": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/forever-agent/download/forever-agent-0.6.1.tgz",
- "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
- "dev": true
- },
- "form-data": {
- "version": "2.3.3",
- "resolved": "https://registry.npm.taobao.org/form-data/download/form-data-2.3.3.tgz",
- "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=",
- "dev": true,
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
- }
- },
- "forwarded": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/forwarded/download/forwarded-0.1.2.tgz",
- "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
- "dev": true
- },
- "fragment-cache": {
- "version": "0.2.1",
- "resolved": "https://registry.npm.taobao.org/fragment-cache/download/fragment-cache-0.2.1.tgz",
- "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
- "dev": true,
- "requires": {
- "map-cache": "^0.2.2"
- }
- },
- "fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz?cache=0&sync_timestamp=1589682752100&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffresh%2Fdownload%2Ffresh-0.5.2.tgz",
- "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
- "dev": true
- },
- "from2": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/from2/download/from2-2.3.0.tgz",
- "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.0"
- }
- },
- "fs-extra": {
- "version": "7.0.1",
- "resolved": "https://registry.npm.taobao.org/fs-extra/download/fs-extra-7.0.1.tgz?cache=0&sync_timestamp=1591229972229&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffs-extra%2Fdownload%2Ffs-extra-7.0.1.tgz",
- "integrity": "sha1-TxicRKoSO4lfcigE9V6iPq3DSOk=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
- }
- },
- "fs-minipass": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/fs-minipass/download/fs-minipass-2.1.0.tgz",
- "integrity": "sha1-f1A2/b8SxjwWkZDL5BmchSJx+fs=",
- "dev": true,
- "requires": {
- "minipass": "^3.0.0"
- }
- },
- "fs-write-stream-atomic": {
- "version": "1.0.10",
- "resolved": "https://registry.npm.taobao.org/fs-write-stream-atomic/download/fs-write-stream-atomic-1.0.10.tgz",
- "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.2",
- "iferr": "^0.1.5",
- "imurmurhash": "^0.1.4",
- "readable-stream": "1 || 2"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "fsevents": {
- "version": "2.1.3",
- "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-2.1.3.tgz",
- "integrity": "sha1-+3OHA66NL5/pAMM4Nt3r7ouX8j4=",
- "dev": true,
- "optional": true
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/function-bind/download/function-bind-1.1.1.tgz",
- "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=",
- "dev": true
- },
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/functional-red-black-tree/download/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
- "dev": true
- },
- "generic-names": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/generic-names/download/generic-names-2.0.1.tgz",
- "integrity": "sha1-+KN46tLMqno08DF7BVVIMq5BuHI=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0"
- }
- },
- "gensync": {
- "version": "1.0.0-beta.1",
- "resolved": "https://registry.npm.taobao.org/gensync/download/gensync-1.0.0-beta.1.tgz",
- "integrity": "sha1-WPQ2H/mH5f9uHnohCCeqNx6qwmk=",
- "dev": true
- },
- "get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npm.taobao.org/get-caller-file/download/get-caller-file-2.0.5.tgz",
- "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=",
- "dev": true
- },
- "get-stream": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/get-stream/download/get-stream-4.1.0.tgz?cache=0&sync_timestamp=1597056502934&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fget-stream%2Fdownload%2Fget-stream-4.1.0.tgz",
- "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=",
- "dev": true,
- "requires": {
- "pump": "^3.0.0"
- }
- },
- "get-value": {
- "version": "2.0.6",
- "resolved": "https://registry.npm.taobao.org/get-value/download/get-value-2.0.6.tgz",
- "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
- "dev": true
- },
- "getpass": {
- "version": "0.1.7",
- "resolved": "https://registry.npm.taobao.org/getpass/download/getpass-0.1.7.tgz?cache=0&sync_timestamp=1589682745510&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fgetpass%2Fdownload%2Fgetpass-0.1.7.tgz",
- "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
- "dev": true,
- "requires": {
- "assert-plus": "^1.0.0"
- }
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npm.taobao.org/glob/download/glob-7.1.6.tgz?cache=0&sync_timestamp=1589682812051&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglob%2Fdownload%2Fglob-7.1.6.tgz",
- "integrity": "sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "glob-parent": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-5.1.1.tgz",
- "integrity": "sha1-tsHvQXxOVmPqSY8cRa+saRa7wik=",
- "dev": true,
- "requires": {
- "is-glob": "^4.0.1"
- }
- },
- "glob-to-regexp": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/glob-to-regexp/download/glob-to-regexp-0.3.0.tgz",
- "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
- "dev": true
- },
- "globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npm.taobao.org/globals/download/globals-11.12.0.tgz",
- "integrity": "sha1-q4eVM4hooLq9hSV1gBjCp+uVxC4=",
- "dev": true
- },
- "globby": {
- "version": "9.2.0",
- "resolved": "https://registry.npm.taobao.org/globby/download/globby-9.2.0.tgz?cache=0&sync_timestamp=1591083812416&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglobby%2Fdownload%2Fglobby-9.2.0.tgz",
- "integrity": "sha1-/QKacGxwPSm90XD0tts6P3p8tj0=",
- "dev": true,
- "requires": {
- "@types/glob": "^7.1.1",
- "array-union": "^1.0.2",
- "dir-glob": "^2.2.2",
- "fast-glob": "^2.2.6",
- "glob": "^7.1.3",
- "ignore": "^4.0.3",
- "pify": "^4.0.1",
- "slash": "^2.0.0"
- }
- },
- "graceful-fs": {
- "version": "4.2.4",
- "resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz?cache=0&sync_timestamp=1589682809142&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fgraceful-fs%2Fdownload%2Fgraceful-fs-4.2.4.tgz",
- "integrity": "sha1-Ila94U02MpWMRl68ltxGfKB6Kfs=",
- "dev": true
- },
- "gzip-size": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/gzip-size/download/gzip-size-5.1.1.tgz",
- "integrity": "sha1-y5vuaS+HwGErIyhAqHOQTkwTUnQ=",
- "dev": true,
- "requires": {
- "duplexer": "^0.1.1",
- "pify": "^4.0.1"
- }
- },
- "handle-thing": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/handle-thing/download/handle-thing-2.0.1.tgz",
- "integrity": "sha1-hX95zjWVgMNA1DCBzGSJcNC7I04=",
- "dev": true
- },
- "har-schema": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/har-schema/download/har-schema-2.0.0.tgz",
- "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
- "dev": true
- },
- "har-validator": {
- "version": "5.1.5",
- "resolved": "https://registry.npm.taobao.org/har-validator/download/har-validator-5.1.5.tgz?cache=0&sync_timestamp=1596082584903&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhar-validator%2Fdownload%2Fhar-validator-5.1.5.tgz",
- "integrity": "sha1-HwgDufjLIMD6E4It8ezds2veHv0=",
- "dev": true,
- "requires": {
- "ajv": "^6.12.3",
- "har-schema": "^2.0.0"
- }
- },
- "has": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/has/download/has-1.0.3.tgz",
- "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.1"
- }
- },
- "has-ansi": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/has-ansi/download/has-ansi-2.0.0.tgz",
- "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- }
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "has-symbols": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/has-symbols/download/has-symbols-1.0.1.tgz",
- "integrity": "sha1-n1IUdYpEGWxAbZvXbOv4HsLdMeg=",
- "dev": true
- },
- "has-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/has-value/download/has-value-1.0.0.tgz",
- "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
- "dev": true,
- "requires": {
- "get-value": "^2.0.6",
- "has-values": "^1.0.0",
- "isobject": "^3.0.0"
- }
- },
- "has-values": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/has-values/download/has-values-1.0.0.tgz",
- "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
- "dev": true,
- "requires": {
- "is-number": "^3.0.0",
- "kind-of": "^4.0.0"
- },
- "dependencies": {
- "kind-of": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-4.0.0.tgz",
- "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "hash-base": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/hash-base/download/hash-base-3.1.0.tgz",
- "integrity": "sha1-VcOB2eBuHSmXqIO0o/3f5/DTrzM=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.4",
- "readable-stream": "^3.6.0",
- "safe-buffer": "^5.2.0"
- },
- "dependencies": {
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.6.0.tgz?cache=0&sync_timestamp=1589682741447&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-3.6.0.tgz",
- "integrity": "sha1-M3u9o63AcGvT4CRCaihtS0sskZg=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- },
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.2.1.tgz?cache=0&sync_timestamp=1589682795646&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafe-buffer%2Fdownload%2Fsafe-buffer-5.2.1.tgz",
- "integrity": "sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY=",
- "dev": true
- }
- }
- },
- "hash-sum": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/hash-sum/download/hash-sum-2.0.0.tgz",
- "integrity": "sha1-gdAbtd6OpKIUrV1urRtSNGCwtFo=",
- "dev": true
- },
- "hash.js": {
- "version": "1.1.7",
- "resolved": "https://registry.npm.taobao.org/hash.js/download/hash.js-1.1.7.tgz",
- "integrity": "sha1-C6vKU46NTuSg+JiNaIZlN6ADz0I=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "minimalistic-assert": "^1.0.1"
- }
- },
- "he": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/he/download/he-1.2.0.tgz?cache=0&sync_timestamp=1589682765156&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhe%2Fdownload%2Fhe-1.2.0.tgz",
- "integrity": "sha1-hK5l+n6vsWX922FWauFLrwVmTw8=",
- "dev": true
- },
- "hex-color-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/hex-color-regex/download/hex-color-regex-1.1.0.tgz",
- "integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4=",
- "dev": true
- },
- "highlight.js": {
- "version": "9.18.3",
- "resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-9.18.3.tgz",
- "integrity": "sha1-oaCiAo1eMUniOA+Khl7oUWcD1jQ=",
- "dev": true
- },
- "hmac-drbg": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/hmac-drbg/download/hmac-drbg-1.0.1.tgz",
- "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
- "dev": true,
- "requires": {
- "hash.js": "^1.0.3",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.1"
- }
- },
- "hoopy": {
- "version": "0.1.4",
- "resolved": "https://registry.npm.taobao.org/hoopy/download/hoopy-0.1.4.tgz",
- "integrity": "sha1-YJIH1mEQADOpqUAq096mdzgcGx0=",
- "dev": true
- },
- "hosted-git-info": {
- "version": "2.8.8",
- "resolved": "https://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.8.8.tgz?cache=0&sync_timestamp=1594427993800&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhosted-git-info%2Fdownload%2Fhosted-git-info-2.8.8.tgz",
- "integrity": "sha1-dTm9S8Hg4KiVgVouAmJCCxKFhIg=",
- "dev": true
- },
- "hpack.js": {
- "version": "2.1.6",
- "resolved": "https://registry.npm.taobao.org/hpack.js/download/hpack.js-2.1.6.tgz",
- "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "obuf": "^1.0.0",
- "readable-stream": "^2.0.1",
- "wbuf": "^1.1.0"
- }
- },
- "hsl-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/hsl-regex/download/hsl-regex-1.0.0.tgz",
- "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=",
- "dev": true
- },
- "hsla-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/hsla-regex/download/hsla-regex-1.0.0.tgz",
- "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
- "dev": true
- },
- "html-comment-regex": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/html-comment-regex/download/html-comment-regex-1.1.2.tgz",
- "integrity": "sha1-l9RoiutcgYhqNk+qDK0d2hTUM6c=",
- "dev": true
- },
- "html-entities": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/html-entities/download/html-entities-1.3.1.tgz",
- "integrity": "sha1-+5oaS1sUxdq6gtPjTGrk/nAaDkQ=",
- "dev": true
- },
- "html-minifier": {
- "version": "3.5.21",
- "resolved": "https://registry.npm.taobao.org/html-minifier/download/html-minifier-3.5.21.tgz",
- "integrity": "sha1-0AQOBUcw41TbAIRjWTGUAVIS0gw=",
- "dev": true,
- "requires": {
- "camel-case": "3.0.x",
- "clean-css": "4.2.x",
- "commander": "2.17.x",
- "he": "1.2.x",
- "param-case": "2.1.x",
- "relateurl": "0.2.x",
- "uglify-js": "3.4.x"
- },
- "dependencies": {
- "commander": {
- "version": "2.17.1",
- "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.17.1.tgz",
- "integrity": "sha1-vXerfebelCBc6sxy8XFtKfIKd78=",
- "dev": true
- }
- }
- },
- "html-tags": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/html-tags/download/html-tags-3.1.0.tgz",
- "integrity": "sha1-e15vfmZen7QfMAB+2eDUHpf7IUA=",
- "dev": true
- },
- "html-webpack-plugin": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/html-webpack-plugin/download/html-webpack-plugin-3.2.0.tgz",
- "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
- "dev": true,
- "requires": {
- "html-minifier": "^3.2.3",
- "loader-utils": "^0.2.16",
- "lodash": "^4.17.3",
- "pretty-error": "^2.0.2",
- "tapable": "^1.0.0",
- "toposort": "^1.0.0",
- "util.promisify": "1.0.0"
- },
- "dependencies": {
- "big.js": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/big.js/download/big.js-3.2.0.tgz",
- "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=",
- "dev": true
- },
- "emojis-list": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/emojis-list/download/emojis-list-2.1.0.tgz",
- "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
- "dev": true
- },
- "json5": {
- "version": "0.5.1",
- "resolved": "https://registry.npm.taobao.org/json5/download/json5-0.5.1.tgz",
- "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
- "dev": true
- },
- "loader-utils": {
- "version": "0.2.17",
- "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-0.2.17.tgz",
- "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
- "dev": true,
- "requires": {
- "big.js": "^3.1.3",
- "emojis-list": "^2.0.0",
- "json5": "^0.5.0",
- "object-assign": "^4.0.1"
- }
- },
- "util.promisify": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/util.promisify/download/util.promisify-1.0.0.tgz?cache=0&sync_timestamp=1589682767473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Futil.promisify%2Fdownload%2Futil.promisify-1.0.0.tgz",
- "integrity": "sha1-RA9xZaRZyaFtwUXrjnLzVocJcDA=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.2",
- "object.getownpropertydescriptors": "^2.0.3"
- }
- }
- }
- },
- "htmlparser2": {
- "version": "3.10.1",
- "resolved": "https://registry.npm.taobao.org/htmlparser2/download/htmlparser2-3.10.1.tgz",
- "integrity": "sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=",
- "dev": true,
- "requires": {
- "domelementtype": "^1.3.1",
- "domhandler": "^2.3.0",
- "domutils": "^1.5.1",
- "entities": "^1.1.1",
- "inherits": "^2.0.1",
- "readable-stream": "^3.1.1"
- },
- "dependencies": {
- "entities": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/entities/download/entities-1.1.2.tgz",
- "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=",
- "dev": true
- },
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.6.0.tgz?cache=0&sync_timestamp=1589682741447&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-3.6.0.tgz",
- "integrity": "sha1-M3u9o63AcGvT4CRCaihtS0sskZg=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- }
- }
- },
- "http-deceiver": {
- "version": "1.2.7",
- "resolved": "https://registry.npm.taobao.org/http-deceiver/download/http-deceiver-1.2.7.tgz",
- "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
- "dev": true
- },
- "http-errors": {
- "version": "1.7.2",
- "resolved": "https://registry.npm.taobao.org/http-errors/download/http-errors-1.7.2.tgz?cache=0&sync_timestamp=1593407858306&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhttp-errors%2Fdownload%2Fhttp-errors-1.7.2.tgz",
- "integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=",
- "dev": true,
- "requires": {
- "depd": "~1.1.2",
- "inherits": "2.0.3",
- "setprototypeof": "1.1.1",
- "statuses": ">= 1.5.0 < 2",
- "toidentifier": "1.0.0"
- },
- "dependencies": {
- "inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
- }
- }
- },
- "http-proxy": {
- "version": "1.18.1",
- "resolved": "https://registry.npm.taobao.org/http-proxy/download/http-proxy-1.18.1.tgz",
- "integrity": "sha1-QBVB8FNIhLv5UmAzTnL4juOXZUk=",
- "dev": true,
- "requires": {
- "eventemitter3": "^4.0.0",
- "follow-redirects": "^1.0.0",
- "requires-port": "^1.0.0"
- }
- },
- "http-proxy-middleware": {
- "version": "0.19.1",
- "resolved": "https://registry.npm.taobao.org/http-proxy-middleware/download/http-proxy-middleware-0.19.1.tgz",
- "integrity": "sha1-GDx9xKoUeRUDBkmMIQza+WCApDo=",
- "dev": true,
- "requires": {
- "http-proxy": "^1.17.0",
- "is-glob": "^4.0.0",
- "lodash": "^4.17.11",
- "micromatch": "^3.1.10"
- }
- },
- "http-signature": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/http-signature/download/http-signature-1.2.0.tgz?cache=0&sync_timestamp=1589682811784&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhttp-signature%2Fdownload%2Fhttp-signature-1.2.0.tgz",
- "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
- "dev": true,
- "requires": {
- "assert-plus": "^1.0.0",
- "jsprim": "^1.2.2",
- "sshpk": "^1.7.0"
- }
- },
- "https-browserify": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/https-browserify/download/https-browserify-1.0.0.tgz",
- "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
- "dev": true
- },
- "human-signals": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/human-signals/download/human-signals-1.1.1.tgz",
- "integrity": "sha1-xbHNFPUK6uCatsWf5jujOV/k36M=",
- "dev": true
- },
- "iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.24.tgz?cache=0&sync_timestamp=1594184264130&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ficonv-lite%2Fdownload%2Ficonv-lite-0.4.24.tgz",
- "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=",
- "dev": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "icss-replace-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/icss-replace-symbols/download/icss-replace-symbols-1.1.0.tgz",
- "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
- "dev": true
- },
- "icss-utils": {
- "version": "4.1.1",
- "resolved": "https://registry.npm.taobao.org/icss-utils/download/icss-utils-4.1.1.tgz",
- "integrity": "sha1-IRcLU3ie4nRHwvR91oMIFAP5pGc=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.14"
- }
- },
- "ieee754": {
- "version": "1.1.13",
- "resolved": "https://registry.npm.taobao.org/ieee754/download/ieee754-1.1.13.tgz",
- "integrity": "sha1-7BaFWOlaoYH9h9N/VcMrvLZwi4Q=",
- "dev": true
- },
- "iferr": {
- "version": "0.1.5",
- "resolved": "https://registry.npm.taobao.org/iferr/download/iferr-0.1.5.tgz",
- "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
- "dev": true
- },
- "ignore": {
- "version": "4.0.6",
- "resolved": "https://registry.npm.taobao.org/ignore/download/ignore-4.0.6.tgz",
- "integrity": "sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw=",
- "dev": true
- },
- "import-cwd": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/import-cwd/download/import-cwd-2.1.0.tgz",
- "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
- "dev": true,
- "requires": {
- "import-from": "^2.1.0"
- }
- },
- "import-fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/import-fresh/download/import-fresh-2.0.0.tgz?cache=0&sync_timestamp=1589682760620&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fimport-fresh%2Fdownload%2Fimport-fresh-2.0.0.tgz",
- "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
- "dev": true,
- "requires": {
- "caller-path": "^2.0.0",
- "resolve-from": "^3.0.0"
- }
- },
- "import-from": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/import-from/download/import-from-2.1.0.tgz",
- "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
- "dev": true,
- "requires": {
- "resolve-from": "^3.0.0"
- }
- },
- "import-local": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/import-local/download/import-local-2.0.0.tgz",
- "integrity": "sha1-VQcL44pZk88Y72236WH1vuXFoJ0=",
- "dev": true,
- "requires": {
- "pkg-dir": "^3.0.0",
- "resolve-cwd": "^2.0.0"
- }
- },
- "imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npm.taobao.org/imurmurhash/download/imurmurhash-0.1.4.tgz",
- "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
- "dev": true
- },
- "indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/indent-string/download/indent-string-4.0.0.tgz",
- "integrity": "sha1-Yk+PRJfWGbLZdoUx1Y9BIoVNclE=",
- "dev": true
- },
- "indexes-of": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/indexes-of/download/indexes-of-1.0.1.tgz",
- "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
- "dev": true
- },
- "infer-owner": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/infer-owner/download/infer-owner-1.0.4.tgz",
- "integrity": "sha1-xM78qo5RBRwqQLos6KPScpWvlGc=",
- "dev": true
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz",
- "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=",
- "dev": true
- },
- "inquirer": {
- "version": "7.3.3",
- "resolved": "https://registry.npm.taobao.org/inquirer/download/inquirer-7.3.3.tgz",
- "integrity": "sha1-BNF2sq8Er8FXqD/XwQDpjuCq0AM=",
- "dev": true,
- "requires": {
- "ansi-escapes": "^4.2.1",
- "chalk": "^4.1.0",
- "cli-cursor": "^3.1.0",
- "cli-width": "^3.0.0",
- "external-editor": "^3.0.3",
- "figures": "^3.0.0",
- "lodash": "^4.17.19",
- "mute-stream": "0.0.8",
- "run-async": "^2.4.0",
- "rxjs": "^6.6.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0",
- "through": "^2.3.6"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.2.1.tgz",
- "integrity": "sha1-kK51xCTQCNJiTFvynq0xd+v881k=",
- "dev": true,
- "requires": {
- "@types/color-name": "^1.1.1",
- "color-convert": "^2.0.1"
- }
- },
- "chalk": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz",
- "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/cli-cursor/download/cli-cursor-3.1.0.tgz",
- "integrity": "sha1-JkMFp65JDR0Dvwybp8kl0XU68wc=",
- "dev": true,
- "requires": {
- "restore-cursor": "^3.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
- "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
- "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
- "dev": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
- "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
- "dev": true
- },
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/mimic-fn/download/mimic-fn-2.1.0.tgz?cache=0&sync_timestamp=1596095644798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmimic-fn%2Fdownload%2Fmimic-fn-2.1.0.tgz",
- "integrity": "sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=",
- "dev": true
- },
- "onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npm.taobao.org/onetime/download/onetime-5.1.2.tgz?cache=0&sync_timestamp=1597005345612&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fonetime%2Fdownload%2Fonetime-5.1.2.tgz",
- "integrity": "sha1-0Oluu1awdHbfHdnEgG5SN5hcpF4=",
- "dev": true,
- "requires": {
- "mimic-fn": "^2.1.0"
- }
- },
- "restore-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/restore-cursor/download/restore-cursor-3.1.0.tgz",
- "integrity": "sha1-OfZ8VLOnpYzqUjbZXPADQjljH34=",
- "dev": true,
- "requires": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- }
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.1.0.tgz",
- "integrity": "sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
- "internal-ip": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/internal-ip/download/internal-ip-4.3.0.tgz?cache=0&sync_timestamp=1596563037835&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finternal-ip%2Fdownload%2Finternal-ip-4.3.0.tgz",
- "integrity": "sha1-hFRSuq2dLKO2nGNaE3rLmg2tCQc=",
- "dev": true,
- "requires": {
- "default-gateway": "^4.2.0",
- "ipaddr.js": "^1.9.0"
- },
- "dependencies": {
- "default-gateway": {
- "version": "4.2.0",
- "resolved": "https://registry.npm.taobao.org/default-gateway/download/default-gateway-4.2.0.tgz",
- "integrity": "sha1-FnEEx1AMIRX23WmwpTa7jtcgVSs=",
- "dev": true,
- "requires": {
- "execa": "^1.0.0",
- "ip-regex": "^2.1.0"
- }
- }
- }
- },
- "interpret": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/interpret/download/interpret-1.4.0.tgz",
- "integrity": "sha1-Zlq4vE2iendKQFhOgS4+D6RbGh4=",
- "dev": true
- },
- "invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npm.taobao.org/invariant/download/invariant-2.2.4.tgz",
- "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=",
- "dev": true,
- "requires": {
- "loose-envify": "^1.0.0"
- }
- },
- "ip": {
- "version": "1.1.5",
- "resolved": "https://registry.npm.taobao.org/ip/download/ip-1.1.5.tgz",
- "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
- "dev": true
- },
- "ip-regex": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/ip-regex/download/ip-regex-2.1.0.tgz",
- "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
- "dev": true
- },
- "ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npm.taobao.org/ipaddr.js/download/ipaddr.js-1.9.1.tgz",
- "integrity": "sha1-v/OFQ+64mEglB5/zoqjmy9RngbM=",
- "dev": true
- },
- "is-absolute-url": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/is-absolute-url/download/is-absolute-url-2.1.0.tgz",
- "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=",
- "dev": true
- },
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
- "dev": true,
- "requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "is-arguments": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/is-arguments/download/is-arguments-1.0.4.tgz",
- "integrity": "sha1-P6+WbHy6D/Q3+zH2JQCC/PBEjPM=",
- "dev": true
- },
- "is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npm.taobao.org/is-arrayish/download/is-arrayish-0.2.1.tgz",
- "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
- "dev": true
- },
- "is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-2.1.0.tgz",
- "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=",
- "dev": true,
- "optional": true,
- "requires": {
- "binary-extensions": "^2.0.0"
- }
- },
- "is-buffer": {
- "version": "1.1.6",
- "resolved": "https://registry.npm.taobao.org/is-buffer/download/is-buffer-1.1.6.tgz",
- "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=",
- "dev": true
- },
- "is-callable": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/is-callable/download/is-callable-1.2.0.tgz",
- "integrity": "sha1-gzNlYLVKOONeOi33r9BFTWkUaLs=",
- "dev": true
- },
- "is-ci": {
- "version": "1.2.1",
- "resolved": "https://registry.npm.taobao.org/is-ci/download/is-ci-1.2.1.tgz?cache=0&sync_timestamp=1589682764432&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-ci%2Fdownload%2Fis-ci-1.2.1.tgz",
- "integrity": "sha1-43ecjuF/zPQoSI9uKBGH8uYyhBw=",
- "dev": true,
- "requires": {
- "ci-info": "^1.5.0"
- }
- },
- "is-color-stop": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/is-color-stop/download/is-color-stop-1.1.0.tgz",
- "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=",
- "dev": true,
- "requires": {
- "css-color-names": "^0.0.4",
- "hex-color-regex": "^1.1.0",
- "hsl-regex": "^1.0.0",
- "hsla-regex": "^1.0.0",
- "rgb-regex": "^1.0.1",
- "rgba-regex": "^1.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
- "dev": true,
- "requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "is-date-object": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-date-object/download/is-date-object-1.0.2.tgz",
- "integrity": "sha1-vac28s2P0G0yhE53Q7+nSUw7/X4=",
- "dev": true
- },
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npm.taobao.org/is-descriptor/download/is-descriptor-0.1.6.tgz",
- "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^0.1.6",
- "is-data-descriptor": "^0.1.4",
- "kind-of": "^5.0.0"
- },
- "dependencies": {
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-5.1.0.tgz",
- "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=",
- "dev": true
- }
- }
- },
- "is-directory": {
- "version": "0.3.1",
- "resolved": "https://registry.npm.taobao.org/is-directory/download/is-directory-0.3.1.tgz",
- "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
- "dev": true
- },
- "is-docker": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/is-docker/download/is-docker-2.1.1.tgz",
- "integrity": "sha1-QSWojkTkUNOE4JBH7eca3C0UQVY=",
- "dev": true
- },
- "is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/is-extendable/download/is-extendable-0.1.1.tgz",
- "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
- "dev": true
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/is-extglob/download/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-4.0.1.tgz",
- "integrity": "sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/is-number/download/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "is-obj": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-obj/download/is-obj-2.0.0.tgz",
- "integrity": "sha1-Rz+wXZc3BeP9liBUUBjKjiLvSYI=",
- "dev": true
- },
- "is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/is-path-cwd/download/is-path-cwd-2.2.0.tgz",
- "integrity": "sha1-Z9Q7gmZKe1GR/ZEZEn6zAASKn9s=",
- "dev": true
- },
- "is-path-in-cwd": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/is-path-in-cwd/download/is-path-in-cwd-2.1.0.tgz",
- "integrity": "sha1-v+Lcomxp85cmWkAJljYCk1oFOss=",
- "dev": true,
- "requires": {
- "is-path-inside": "^2.1.0"
- }
- },
- "is-path-inside": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/is-path-inside/download/is-path-inside-2.1.0.tgz",
- "integrity": "sha1-fJgQWH1lmkDSe8201WFuqwWUlLI=",
- "dev": true,
- "requires": {
- "path-is-inside": "^1.0.2"
- }
- },
- "is-plain-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/is-plain-obj/download/is-plain-obj-1.1.0.tgz",
- "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
- "dev": true
- },
- "is-plain-object": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/is-plain-object/download/is-plain-object-2.0.4.tgz",
- "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=",
- "dev": true,
- "requires": {
- "isobject": "^3.0.1"
- }
- },
- "is-regex": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/is-regex/download/is-regex-1.1.1.tgz?cache=0&sync_timestamp=1596555640677&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-regex%2Fdownload%2Fis-regex-1.1.1.tgz",
- "integrity": "sha1-xvmKrMVG9s7FRooHt7FTq1ZKV7k=",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.1"
- }
- },
- "is-resolvable": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/is-resolvable/download/is-resolvable-1.1.0.tgz",
- "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=",
- "dev": true
- },
- "is-stream": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/is-stream/download/is-stream-1.1.0.tgz",
- "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
- "dev": true
- },
- "is-svg": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/is-svg/download/is-svg-3.0.0.tgz",
- "integrity": "sha1-kyHb0pwhLlypnE+peUxxS8r6L3U=",
- "dev": true,
- "requires": {
- "html-comment-regex": "^1.1.0"
- }
- },
- "is-symbol": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/is-symbol/download/is-symbol-1.0.3.tgz",
- "integrity": "sha1-OOEBS55jKb4N6dJKQU/XRB7GGTc=",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.1"
- }
- },
- "is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-typedarray/download/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
- "dev": true
- },
- "is-windows": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-windows/download/is-windows-1.0.2.tgz",
- "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=",
- "dev": true
- },
- "is-wsl": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/is-wsl/download/is-wsl-1.1.0.tgz",
- "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
- "dev": true
- },
- "isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
- },
- "isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/isobject/download/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "isstream": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/isstream/download/isstream-0.1.2.tgz",
- "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
- "dev": true
- },
- "javascript-stringify": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/javascript-stringify/download/javascript-stringify-2.0.1.tgz",
- "integrity": "sha1-bvNYA1MQ411mfGde1j0+t8GqGeU=",
- "dev": true
- },
- "jest-worker": {
- "version": "25.5.0",
- "resolved": "https://registry.npm.taobao.org/jest-worker/download/jest-worker-25.5.0.tgz?cache=0&sync_timestamp=1597057499649&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjest-worker%2Fdownload%2Fjest-worker-25.5.0.tgz",
- "integrity": "sha1-JhHQcbec6g9D7lej0RhZOsFUfbE=",
- "dev": true,
- "requires": {
- "merge-stream": "^2.0.0",
- "supports-color": "^7.0.0"
- },
- "dependencies": {
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
- "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
- "dev": true
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.1.0.tgz",
- "integrity": "sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
- "js-message": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/js-message/download/js-message-1.0.5.tgz",
- "integrity": "sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=",
- "dev": true
- },
- "js-queue": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/js-queue/download/js-queue-2.0.0.tgz",
- "integrity": "sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=",
- "dev": true,
- "requires": {
- "easy-stack": "^1.0.0"
- }
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/js-tokens/download/js-tokens-4.0.0.tgz",
- "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=",
- "dev": true
- },
- "js-yaml": {
- "version": "3.14.0",
- "resolved": "https://registry.npm.taobao.org/js-yaml/download/js-yaml-3.14.0.tgz",
- "integrity": "sha1-p6NBcPJqIbsWJCTYray0ETpp5II=",
- "dev": true,
- "requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- }
- },
- "jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/jsbn/download/jsbn-0.1.1.tgz?cache=0&sync_timestamp=1589682745609&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjsbn%2Fdownload%2Fjsbn-0.1.1.tgz",
- "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
- "dev": true
- },
- "jsesc": {
- "version": "2.5.2",
- "resolved": "https://registry.npm.taobao.org/jsesc/download/jsesc-2.5.2.tgz",
- "integrity": "sha1-gFZNLkg9rPbo7yCWUKZ98/DCg6Q=",
- "dev": true
- },
- "json-parse-better-errors": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/json-parse-better-errors/download/json-parse-better-errors-1.0.2.tgz",
- "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=",
- "dev": true
- },
- "json-schema": {
- "version": "0.2.3",
- "resolved": "https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.3.tgz",
- "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
- "dev": true
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=",
- "dev": true
- },
- "json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/json-stable-stringify-without-jsonify/download/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
- "dev": true
- },
- "json-stringify-safe": {
- "version": "5.0.1",
- "resolved": "https://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz?cache=0&sync_timestamp=1589682771374&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjson-stringify-safe%2Fdownload%2Fjson-stringify-safe-5.0.1.tgz",
- "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
- "dev": true
- },
- "json3": {
- "version": "3.3.3",
- "resolved": "https://registry.npm.taobao.org/json3/download/json3-3.3.3.tgz",
- "integrity": "sha1-f8EON1/FrkLEcFpcwKpvYr4wW4E=",
- "dev": true
- },
- "json5": {
- "version": "2.1.3",
- "resolved": "https://registry.npm.taobao.org/json5/download/json5-2.1.3.tgz",
- "integrity": "sha1-ybD3+pIzv+WAf+ZvzzpWF+1ZfUM=",
- "dev": true,
- "requires": {
- "minimist": "^1.2.5"
- }
- },
- "jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/jsonfile/download/jsonfile-4.0.0.tgz",
- "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.6"
- }
- },
- "jsprim": {
- "version": "1.4.1",
- "resolved": "https://registry.npm.taobao.org/jsprim/download/jsprim-1.4.1.tgz",
- "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0",
- "extsprintf": "1.3.0",
- "json-schema": "0.2.3",
- "verror": "1.10.0"
- }
- },
- "killable": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz",
- "integrity": "sha1-TIzkQRh6Bhx0dPuHygjipjgZSJI=",
- "dev": true
- },
- "kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-6.0.3.tgz",
- "integrity": "sha1-B8BQNKbDSfoG4k+jWqdttFgM5N0=",
- "dev": true
- },
- "launch-editor": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/launch-editor/download/launch-editor-2.2.1.tgz",
- "integrity": "sha1-hxtaPuOdZoD8wm03kwtu7aidsMo=",
- "dev": true,
- "requires": {
- "chalk": "^2.3.0",
- "shell-quote": "^1.6.1"
- }
- },
- "launch-editor-middleware": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/launch-editor-middleware/download/launch-editor-middleware-2.2.1.tgz",
- "integrity": "sha1-4UsH5scVSwpLhqD9NFeE5FgEwVc=",
- "dev": true,
- "requires": {
- "launch-editor": "^2.2.1"
- }
- },
- "leven": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/leven/download/leven-3.1.0.tgz",
- "integrity": "sha1-d4kd6DQGTMy6gq54QrtrFKE+1/I=",
- "dev": true
- },
- "levenary": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/levenary/download/levenary-1.1.1.tgz",
- "integrity": "sha1-hCqe6Y0gdap/ru2+MmeekgX0b3c=",
- "dev": true,
- "requires": {
- "leven": "^3.1.0"
- }
- },
- "levn": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/levn/download/levn-0.3.0.tgz",
- "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
- "dev": true,
- "requires": {
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2"
- }
- },
- "lines-and-columns": {
- "version": "1.1.6",
- "resolved": "https://registry.npm.taobao.org/lines-and-columns/download/lines-and-columns-1.1.6.tgz",
- "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
- "dev": true
- },
- "loader-fs-cache": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/loader-fs-cache/download/loader-fs-cache-1.0.3.tgz",
- "integrity": "sha1-8IZXZG1gcHi+LwoDL4vWndbyd9k=",
- "dev": true,
- "requires": {
- "find-cache-dir": "^0.1.1",
- "mkdirp": "^0.5.1"
- },
- "dependencies": {
- "find-cache-dir": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-0.1.1.tgz",
- "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
- "dev": true,
- "requires": {
- "commondir": "^1.0.1",
- "mkdirp": "^0.5.1",
- "pkg-dir": "^1.0.0"
- }
- },
- "find-up": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-1.1.2.tgz?cache=0&sync_timestamp=1597169842138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-up%2Fdownload%2Ffind-up-1.1.2.tgz",
- "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
- "dev": true,
- "requires": {
- "path-exists": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- }
- },
- "path-exists": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-2.1.0.tgz",
- "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
- "dev": true,
- "requires": {
- "pinkie-promise": "^2.0.0"
- }
- },
- "pkg-dir": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-1.0.0.tgz",
- "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
- "dev": true,
- "requires": {
- "find-up": "^1.0.0"
- }
- }
- }
- },
- "loader-runner": {
- "version": "2.4.0",
- "resolved": "https://registry.npm.taobao.org/loader-runner/download/loader-runner-2.4.0.tgz?cache=0&sync_timestamp=1593786221739&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Floader-runner%2Fdownload%2Floader-runner-2.4.0.tgz",
- "integrity": "sha1-7UcGa/5TTX6ExMe5mYwqdWB9k1c=",
- "dev": true
- },
- "loader-utils": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-1.4.0.tgz",
- "integrity": "sha1-xXm140yzSxp07cbB+za/o3HVphM=",
- "dev": true,
- "requires": {
- "big.js": "^5.2.2",
- "emojis-list": "^3.0.0",
- "json5": "^1.0.1"
- },
- "dependencies": {
- "json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/json5/download/json5-1.0.1.tgz",
- "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=",
- "dev": true,
- "requires": {
- "minimist": "^1.2.0"
- }
- }
- }
- },
- "locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-3.0.0.tgz",
- "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=",
- "dev": true,
- "requires": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- }
- },
- "lodash": {
- "version": "4.17.20",
- "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz",
- "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI="
- },
- "lodash.camelcase": {
- "version": "4.3.0",
- "resolved": "https://registry.npm.taobao.org/lodash.camelcase/download/lodash.camelcase-4.3.0.tgz",
- "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
- "dev": true
- },
- "lodash.defaultsdeep": {
- "version": "4.6.1",
- "resolved": "https://registry.npm.taobao.org/lodash.defaultsdeep/download/lodash.defaultsdeep-4.6.1.tgz",
- "integrity": "sha1-US6b1yHSctlOPTpjZT+hdRZ0HKY=",
- "dev": true
- },
- "lodash.kebabcase": {
- "version": "4.1.1",
- "resolved": "https://registry.npm.taobao.org/lodash.kebabcase/download/lodash.kebabcase-4.1.1.tgz",
- "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=",
- "dev": true
- },
- "lodash.mapvalues": {
- "version": "4.6.0",
- "resolved": "https://registry.npm.taobao.org/lodash.mapvalues/download/lodash.mapvalues-4.6.0.tgz",
- "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=",
- "dev": true
- },
- "lodash.memoize": {
- "version": "4.1.2",
- "resolved": "https://registry.npm.taobao.org/lodash.memoize/download/lodash.memoize-4.1.2.tgz?cache=0&sync_timestamp=1589682725270&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash.memoize%2Fdownload%2Flodash.memoize-4.1.2.tgz",
- "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
- "dev": true
- },
- "lodash.transform": {
- "version": "4.6.0",
- "resolved": "https://registry.npm.taobao.org/lodash.transform/download/lodash.transform-4.6.0.tgz",
- "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=",
- "dev": true
- },
- "lodash.uniq": {
- "version": "4.5.0",
- "resolved": "https://registry.npm.taobao.org/lodash.uniq/download/lodash.uniq-4.5.0.tgz?cache=0&sync_timestamp=1589682817275&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash.uniq%2Fdownload%2Flodash.uniq-4.5.0.tgz",
- "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
- "dev": true
- },
- "log-symbols": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/log-symbols/download/log-symbols-2.2.0.tgz",
- "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=",
- "dev": true,
- "requires": {
- "chalk": "^2.0.1"
- }
- },
- "loglevel": {
- "version": "1.6.8",
- "resolved": "https://registry.npm.taobao.org/loglevel/download/loglevel-1.6.8.tgz",
- "integrity": "sha1-iiX7ddCSIw7NRFcnDYC1TigBEXE=",
- "dev": true
- },
- "loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/loose-envify/download/loose-envify-1.4.0.tgz",
- "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=",
- "dev": true,
- "requires": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- }
- },
- "lower-case": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/lower-case/download/lower-case-1.1.4.tgz",
- "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
- "dev": true
- },
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-5.1.1.tgz?cache=0&sync_timestamp=1594427582110&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-5.1.1.tgz",
- "integrity": "sha1-HaJ+ZxAnGUdpXa9oSOhH8B2EuSA=",
- "dev": true,
- "requires": {
- "yallist": "^3.0.2"
- }
- },
- "magic-string": {
- "version": "0.25.7",
- "resolved": "https://registry.npm.taobao.org/magic-string/download/magic-string-0.25.7.tgz",
- "integrity": "sha1-P0l9b9NMZpxnmNy4IfLvMfVEUFE=",
- "dev": true,
- "requires": {
- "sourcemap-codec": "^1.4.4"
- }
- },
- "make-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/make-dir/download/make-dir-2.1.0.tgz",
- "integrity": "sha1-XwMQ4YuL6JjMBwCSlaMK5B6R5vU=",
- "dev": true,
- "requires": {
- "pify": "^4.0.1",
- "semver": "^5.6.0"
- }
- },
- "map-cache": {
- "version": "0.2.2",
- "resolved": "https://registry.npm.taobao.org/map-cache/download/map-cache-0.2.2.tgz",
- "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
- "dev": true
- },
- "map-visit": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/map-visit/download/map-visit-1.0.0.tgz",
- "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
- "dev": true,
- "requires": {
- "object-visit": "^1.0.0"
- }
- },
- "md5.js": {
- "version": "1.3.5",
- "resolved": "https://registry.npm.taobao.org/md5.js/download/md5.js-1.3.5.tgz",
- "integrity": "sha1-tdB7jjIW4+J81yjXL3DR5qNCAF8=",
- "dev": true,
- "requires": {
- "hash-base": "^3.0.0",
- "inherits": "^2.0.1",
- "safe-buffer": "^5.1.2"
- }
- },
- "mdn-data": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/mdn-data/download/mdn-data-2.0.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmdn-data%2Fdownload%2Fmdn-data-2.0.4.tgz",
- "integrity": "sha1-aZs8OKxvHXKAkaZGULZdOIUC/Vs=",
- "dev": true
- },
- "media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "dev": true
- },
- "memory-fs": {
- "version": "0.4.1",
- "resolved": "https://registry.npm.taobao.org/memory-fs/download/memory-fs-0.4.1.tgz",
- "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
- "dev": true,
- "requires": {
- "errno": "^0.1.3",
- "readable-stream": "^2.0.1"
- }
- },
- "merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/merge-descriptors/download/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
- "dev": true
- },
- "merge-source-map": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/merge-source-map/download/merge-source-map-1.1.0.tgz",
- "integrity": "sha1-L93n5gIJOfcJBqaPLXrmheTIxkY=",
- "dev": true,
- "requires": {
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/merge-stream/download/merge-stream-2.0.0.tgz?cache=0&sync_timestamp=1589682763068&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmerge-stream%2Fdownload%2Fmerge-stream-2.0.0.tgz",
- "integrity": "sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=",
- "dev": true
- },
- "merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npm.taobao.org/merge2/download/merge2-1.4.1.tgz",
- "integrity": "sha1-Q2iJL4hekHRVpv19xVwMnUBJkK4=",
- "dev": true
- },
- "methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
- "dev": true
- },
- "micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npm.taobao.org/micromatch/download/micromatch-3.1.10.tgz",
- "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=",
- "dev": true,
- "requires": {
- "arr-diff": "^4.0.0",
- "array-unique": "^0.3.2",
- "braces": "^2.3.1",
- "define-property": "^2.0.2",
- "extend-shallow": "^3.0.2",
- "extglob": "^2.0.4",
- "fragment-cache": "^0.2.1",
- "kind-of": "^6.0.2",
- "nanomatch": "^1.2.9",
- "object.pick": "^1.3.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.2"
- }
- },
- "miller-rabin": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/miller-rabin/download/miller-rabin-4.0.1.tgz",
- "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=",
- "dev": true,
- "requires": {
- "bn.js": "^4.0.0",
- "brorand": "^1.0.1"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "mime": {
- "version": "2.4.6",
- "resolved": "https://registry.npm.taobao.org/mime/download/mime-2.4.6.tgz?cache=0&sync_timestamp=1590635592890&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.4.6.tgz",
- "integrity": "sha1-5bQHyQ20QvK+tbFiNz0Htpr/pNE=",
- "dev": true
- },
- "mime-db": {
- "version": "1.44.0",
- "resolved": "https://registry.npm.taobao.org/mime-db/download/mime-db-1.44.0.tgz",
- "integrity": "sha1-+hHF6wrKEzS0Izy01S8QxaYnL5I=",
- "dev": true
- },
- "mime-types": {
- "version": "2.1.27",
- "resolved": "https://registry.npm.taobao.org/mime-types/download/mime-types-2.1.27.tgz?cache=0&sync_timestamp=1589682770020&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime-types%2Fdownload%2Fmime-types-2.1.27.tgz",
- "integrity": "sha1-R5SfmOJ56lMRn1ci4PNOUpvsAJ8=",
- "dev": true,
- "requires": {
- "mime-db": "1.44.0"
- }
- },
- "mimic-fn": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/mimic-fn/download/mimic-fn-1.2.0.tgz?cache=0&sync_timestamp=1596095644798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmimic-fn%2Fdownload%2Fmimic-fn-1.2.0.tgz",
- "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=",
- "dev": true
- },
- "mini-css-extract-plugin": {
- "version": "0.9.0",
- "resolved": "https://registry.npm.taobao.org/mini-css-extract-plugin/download/mini-css-extract-plugin-0.9.0.tgz?cache=0&sync_timestamp=1597072282658&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmini-css-extract-plugin%2Fdownload%2Fmini-css-extract-plugin-0.9.0.tgz",
- "integrity": "sha1-R/LPB6oWWrNXM7H8l9TEbAVkM54=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0",
- "normalize-url": "1.9.1",
- "schema-utils": "^1.0.0",
- "webpack-sources": "^1.1.0"
- },
- "dependencies": {
- "normalize-url": {
- "version": "1.9.1",
- "resolved": "https://registry.npm.taobao.org/normalize-url/download/normalize-url-1.9.1.tgz?cache=0&sync_timestamp=1596373165623&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnormalize-url%2Fdownload%2Fnormalize-url-1.9.1.tgz",
- "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
- "dev": true,
- "requires": {
- "object-assign": "^4.0.1",
- "prepend-http": "^1.0.0",
- "query-string": "^4.1.0",
- "sort-keys": "^1.0.0"
- }
- },
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- }
- }
- },
- "minimalistic-assert": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/minimalistic-assert/download/minimalistic-assert-1.0.1.tgz",
- "integrity": "sha1-LhlN4ERibUoQ5/f7wAznPoPk1cc=",
- "dev": true
- },
- "minimalistic-crypto-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/minimalistic-crypto-utils/download/minimalistic-crypto-utils-1.0.1.tgz",
- "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
- "dev": true
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz",
- "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz?cache=0&sync_timestamp=1589682820731&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fminimist%2Fdownload%2Fminimist-1.2.5.tgz",
- "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=",
- "dev": true
- },
- "minipass": {
- "version": "3.1.3",
- "resolved": "https://registry.npm.taobao.org/minipass/download/minipass-3.1.3.tgz?cache=0&sync_timestamp=1589683712023&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fminipass%2Fdownload%2Fminipass-3.1.3.tgz",
- "integrity": "sha1-fUL/HzljVILhX5zbUxhN7r1YFf0=",
- "dev": true,
- "requires": {
- "yallist": "^4.0.0"
- },
- "dependencies": {
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-4.0.0.tgz",
- "integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=",
- "dev": true
- }
- }
- },
- "minipass-collect": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/minipass-collect/download/minipass-collect-1.0.2.tgz",
- "integrity": "sha1-IrgTv3Rdxu26JXa5QAIq1u3Ixhc=",
- "dev": true,
- "requires": {
- "minipass": "^3.0.0"
- }
- },
- "minipass-flush": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/minipass-flush/download/minipass-flush-1.0.5.tgz",
- "integrity": "sha1-gucTXX6JpQ/+ZGEKeHlTxMTLs3M=",
- "dev": true,
- "requires": {
- "minipass": "^3.0.0"
- }
- },
- "minipass-pipeline": {
- "version": "1.2.4",
- "resolved": "https://registry.npm.taobao.org/minipass-pipeline/download/minipass-pipeline-1.2.4.tgz?cache=0&sync_timestamp=1595998621838&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fminipass-pipeline%2Fdownload%2Fminipass-pipeline-1.2.4.tgz",
- "integrity": "sha1-aEcveXEcCEZXwGfFxq2Tzd6oIUw=",
- "dev": true,
- "requires": {
- "minipass": "^3.0.0"
- }
- },
- "mississippi": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/mississippi/download/mississippi-3.0.0.tgz",
- "integrity": "sha1-6goykfl+C16HdrNj1fChLZTGcCI=",
- "dev": true,
- "requires": {
- "concat-stream": "^1.5.0",
- "duplexify": "^3.4.2",
- "end-of-stream": "^1.1.0",
- "flush-write-stream": "^1.0.0",
- "from2": "^2.1.0",
- "parallel-transform": "^1.1.0",
- "pump": "^3.0.0",
- "pumpify": "^1.3.3",
- "stream-each": "^1.1.0",
- "through2": "^2.0.0"
- }
- },
- "mixin-deep": {
- "version": "1.3.2",
- "resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz",
- "integrity": "sha1-ESC0PcNZp4Xc5ltVuC4lfM9HlWY=",
- "dev": true,
- "requires": {
- "for-in": "^1.0.2",
- "is-extendable": "^1.0.1"
- },
- "dependencies": {
- "is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/is-extendable/download/is-extendable-1.0.1.tgz",
- "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=",
- "dev": true,
- "requires": {
- "is-plain-object": "^2.0.4"
- }
- }
- }
- },
- "mkdirp": {
- "version": "0.5.5",
- "resolved": "https://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.5.tgz?cache=0&sync_timestamp=1589682820707&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmkdirp%2Fdownload%2Fmkdirp-0.5.5.tgz",
- "integrity": "sha1-2Rzv1i0UNsoPQWIOJRKI1CAJne8=",
- "dev": true,
- "requires": {
- "minimist": "^1.2.5"
- }
- },
- "move-concurrently": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
- "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
- "dev": true,
- "requires": {
- "aproba": "^1.1.1",
- "copy-concurrently": "^1.0.0",
- "fs-write-stream-atomic": "^1.0.8",
- "mkdirp": "^0.5.1",
- "rimraf": "^2.5.4",
- "run-queue": "^1.0.3"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz",
- "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=",
- "dev": true
- },
- "multicast-dns": {
- "version": "6.2.3",
- "resolved": "https://registry.npm.taobao.org/multicast-dns/download/multicast-dns-6.2.3.tgz",
- "integrity": "sha1-oOx72QVcQoL3kMPIL04o2zsxsik=",
- "dev": true,
- "requires": {
- "dns-packet": "^1.3.1",
- "thunky": "^1.0.2"
- }
- },
- "multicast-dns-service-types": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/multicast-dns-service-types/download/multicast-dns-service-types-1.1.0.tgz",
- "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
- "dev": true
- },
- "mute-stream": {
- "version": "0.0.8",
- "resolved": "https://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.8.tgz",
- "integrity": "sha1-FjDEKyJR/4HiooPelqVJfqkuXg0=",
- "dev": true
- },
- "mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npm.taobao.org/mz/download/mz-2.7.0.tgz",
- "integrity": "sha1-lQCAV6Vsr63CvGPd5/n/aVWUjjI=",
- "dev": true,
- "requires": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
- "nan": {
- "version": "2.14.1",
- "resolved": "https://registry.npm.taobao.org/nan/download/nan-2.14.1.tgz?cache=0&sync_timestamp=1589682780413&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnan%2Fdownload%2Fnan-2.14.1.tgz",
- "integrity": "sha1-174036MQW5FJTDFHCJMV7/iHSwE=",
- "dev": true,
- "optional": true
- },
- "nanomatch": {
- "version": "1.2.13",
- "resolved": "https://registry.npm.taobao.org/nanomatch/download/nanomatch-1.2.13.tgz",
- "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=",
- "dev": true,
- "requires": {
- "arr-diff": "^4.0.0",
- "array-unique": "^0.3.2",
- "define-property": "^2.0.2",
- "extend-shallow": "^3.0.2",
- "fragment-cache": "^0.2.1",
- "is-windows": "^1.0.2",
- "kind-of": "^6.0.2",
- "object.pick": "^1.3.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- }
- },
- "natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/natural-compare/download/natural-compare-1.4.0.tgz",
- "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
- "dev": true
- },
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz?cache=0&sync_timestamp=1589682752355&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnegotiator%2Fdownload%2Fnegotiator-0.6.2.tgz",
- "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=",
- "dev": true
- },
- "neo-async": {
- "version": "2.6.2",
- "resolved": "https://registry.npm.taobao.org/neo-async/download/neo-async-2.6.2.tgz?cache=0&sync_timestamp=1594317434347&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fneo-async%2Fdownload%2Fneo-async-2.6.2.tgz",
- "integrity": "sha1-tKr7k+OustgXTKU88WOrfXMIMF8=",
- "dev": true
- },
- "nice-try": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/nice-try/download/nice-try-1.0.5.tgz",
- "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=",
- "dev": true
- },
- "no-case": {
- "version": "2.3.2",
- "resolved": "https://registry.npm.taobao.org/no-case/download/no-case-2.3.2.tgz",
- "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=",
- "dev": true,
- "requires": {
- "lower-case": "^1.1.1"
- }
- },
- "node-forge": {
- "version": "0.9.0",
- "resolved": "https://registry.npm.taobao.org/node-forge/download/node-forge-0.9.0.tgz",
- "integrity": "sha1-1iQFDtu0SHStyhK7mlLsY8t4JXk=",
- "dev": true
- },
- "node-ipc": {
- "version": "9.1.1",
- "resolved": "https://registry.npm.taobao.org/node-ipc/download/node-ipc-9.1.1.tgz",
- "integrity": "sha1-TiRe1pOOZRAOWV68XcNLFujdXWk=",
- "dev": true,
- "requires": {
- "event-pubsub": "4.3.0",
- "js-message": "1.0.5",
- "js-queue": "2.0.0"
- }
- },
- "node-libs-browser": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/node-libs-browser/download/node-libs-browser-2.2.1.tgz",
- "integrity": "sha1-tk9RPRgzhiX5A0bSew0jXmMfZCU=",
- "dev": true,
- "requires": {
- "assert": "^1.1.1",
- "browserify-zlib": "^0.2.0",
- "buffer": "^4.3.0",
- "console-browserify": "^1.1.0",
- "constants-browserify": "^1.0.0",
- "crypto-browserify": "^3.11.0",
- "domain-browser": "^1.1.1",
- "events": "^3.0.0",
- "https-browserify": "^1.0.0",
- "os-browserify": "^0.3.0",
- "path-browserify": "0.0.1",
- "process": "^0.11.10",
- "punycode": "^1.2.4",
- "querystring-es3": "^0.2.0",
- "readable-stream": "^2.3.3",
- "stream-browserify": "^2.0.1",
- "stream-http": "^2.7.2",
- "string_decoder": "^1.0.0",
- "timers-browserify": "^2.0.4",
- "tty-browserify": "0.0.0",
- "url": "^0.11.0",
- "util": "^0.11.0",
- "vm-browserify": "^1.0.1"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npm.taobao.org/punycode/download/punycode-1.4.1.tgz?cache=0&sync_timestamp=1589682803838&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpunycode%2Fdownload%2Fpunycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
- }
- }
- },
- "node-releases": {
- "version": "1.1.60",
- "resolved": "https://registry.npm.taobao.org/node-releases/download/node-releases-1.1.60.tgz?cache=0&sync_timestamp=1595485377499&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnode-releases%2Fdownload%2Fnode-releases-1.1.60.tgz",
- "integrity": "sha1-aUi9/OgobwtdDlqI6DhOlU3+cIQ=",
- "dev": true
- },
- "normalize-package-data": {
- "version": "2.5.0",
- "resolved": "https://registry.npm.taobao.org/normalize-package-data/download/normalize-package-data-2.5.0.tgz",
- "integrity": "sha1-5m2xg4sgDB38IzIl0SyzZSDiNKg=",
- "dev": true,
- "requires": {
- "hosted-git-info": "^2.1.4",
- "resolve": "^1.10.0",
- "semver": "2 || 3 || 4 || 5",
- "validate-npm-package-license": "^3.0.1"
- }
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz",
- "integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=",
- "dev": true
- },
- "normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/normalize-range/download/normalize-range-0.1.2.tgz",
- "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
- "dev": true
- },
- "normalize-url": {
- "version": "3.3.0",
- "resolved": "https://registry.npm.taobao.org/normalize-url/download/normalize-url-3.3.0.tgz?cache=0&sync_timestamp=1596373165623&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnormalize-url%2Fdownload%2Fnormalize-url-3.3.0.tgz",
- "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=",
- "dev": true
- },
- "npm-run-path": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz",
- "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
- "dev": true,
- "requires": {
- "path-key": "^2.0.0"
- }
- },
- "nth-check": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz",
- "integrity": "sha1-sr0pXDfj3VijvwcAN2Zjuk2c8Fw=",
- "dev": true,
- "requires": {
- "boolbase": "~1.0.0"
- }
- },
- "null-loader": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/null-loader/download/null-loader-3.0.0.tgz",
- "integrity": "sha1-PitsZjxb2oxzpUNX2PoHCNxhskU=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.2.3",
- "schema-utils": "^1.0.0"
- },
- "dependencies": {
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- }
- }
- },
- "num2fraction": {
- "version": "1.2.2",
- "resolved": "https://registry.npm.taobao.org/num2fraction/download/num2fraction-1.2.2.tgz",
- "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
- "dev": true
- },
- "oauth-sign": {
- "version": "0.9.0",
- "resolved": "https://registry.npm.taobao.org/oauth-sign/download/oauth-sign-0.9.0.tgz?cache=0&sync_timestamp=1589682811909&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Foauth-sign%2Fdownload%2Foauth-sign-0.9.0.tgz",
- "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=",
- "dev": true
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
- },
- "object-copy": {
- "version": "0.1.0",
- "resolved": "https://registry.npm.taobao.org/object-copy/download/object-copy-0.1.0.tgz",
- "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
- "dev": true,
- "requires": {
- "copy-descriptor": "^0.1.0",
- "define-property": "^0.2.5",
- "kind-of": "^3.0.3"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- },
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "object-hash": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/object-hash/download/object-hash-1.3.1.tgz",
- "integrity": "sha1-/eRSCYqVHLFF8Dm7fUVUSd3BJt8=",
- "dev": true
- },
- "object-inspect": {
- "version": "1.8.0",
- "resolved": "https://registry.npm.taobao.org/object-inspect/download/object-inspect-1.8.0.tgz?cache=0&sync_timestamp=1592545231350&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fobject-inspect%2Fdownload%2Fobject-inspect-1.8.0.tgz",
- "integrity": "sha1-34B+Xs9TpgnMa/6T6sPMe+WzqdA=",
- "dev": true
- },
- "object-is": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/object-is/download/object-is-1.1.2.tgz",
- "integrity": "sha1-xdLof/nhGfeLegiEQVGeLuwVc7Y=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.5"
- }
- },
- "object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/object-keys/download/object-keys-1.1.1.tgz",
- "integrity": "sha1-HEfyct8nfzsdrwYWd9nILiMixg4=",
- "dev": true
- },
- "object-visit": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/object-visit/download/object-visit-1.0.1.tgz",
- "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
- "dev": true,
- "requires": {
- "isobject": "^3.0.0"
- }
- },
- "object.assign": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/object.assign/download/object.assign-4.1.0.tgz",
- "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.2",
- "function-bind": "^1.1.1",
- "has-symbols": "^1.0.0",
- "object-keys": "^1.0.11"
- }
- },
- "object.getownpropertydescriptors": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/object.getownpropertydescriptors/download/object.getownpropertydescriptors-2.1.0.tgz",
- "integrity": "sha1-Npvx+VktiridcS3O1cuBx8U1Jkk=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.0-next.1"
- }
- },
- "object.pick": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/object.pick/download/object.pick-1.3.0.tgz",
- "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
- "dev": true,
- "requires": {
- "isobject": "^3.0.1"
- }
- },
- "object.values": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/object.values/download/object.values-1.1.1.tgz",
- "integrity": "sha1-aKmezeNWt+kpWjxeDOMdyMlT3l4=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.0-next.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3"
- }
- },
- "obuf": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/obuf/download/obuf-1.1.2.tgz",
- "integrity": "sha1-Cb6jND1BhZ69RGKS0RydTbYZCE4=",
- "dev": true
- },
- "on-finished": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz",
- "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dev": true,
- "requires": {
- "ee-first": "1.1.1"
- }
- },
- "on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/on-headers/download/on-headers-1.0.2.tgz",
- "integrity": "sha1-dysK5qqlJcOZ5Imt+tkMQD6zwo8=",
- "dev": true
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/once/download/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "onetime": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/onetime/download/onetime-2.0.1.tgz?cache=0&sync_timestamp=1597005345612&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fonetime%2Fdownload%2Fonetime-2.0.1.tgz",
- "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
- "dev": true,
- "requires": {
- "mimic-fn": "^1.0.0"
- }
- },
- "open": {
- "version": "6.4.0",
- "resolved": "https://registry.npm.taobao.org/open/download/open-6.4.0.tgz?cache=0&sync_timestamp=1595208443014&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fopen%2Fdownload%2Fopen-6.4.0.tgz",
- "integrity": "sha1-XBPpbQ3IlGhhZPGJZez+iJ7PyKk=",
- "dev": true,
- "requires": {
- "is-wsl": "^1.1.0"
- }
- },
- "opener": {
- "version": "1.5.1",
- "resolved": "https://registry.npm.taobao.org/opener/download/opener-1.5.1.tgz?cache=0&sync_timestamp=1589682813674&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fopener%2Fdownload%2Fopener-1.5.1.tgz",
- "integrity": "sha1-bS8Od/GgrwAyrKcWwsH7uOfoq+0=",
- "dev": true
- },
- "opn": {
- "version": "5.5.0",
- "resolved": "https://registry.npm.taobao.org/opn/download/opn-5.5.0.tgz",
- "integrity": "sha1-/HFk+rVtI1kExRw7J9pnWMo7m/w=",
- "dev": true,
- "requires": {
- "is-wsl": "^1.1.0"
- }
- },
- "optionator": {
- "version": "0.8.3",
- "resolved": "https://registry.npm.taobao.org/optionator/download/optionator-0.8.3.tgz",
- "integrity": "sha1-hPodA2/p08fiHZmIS2ARZ+yPtJU=",
- "dev": true,
- "requires": {
- "deep-is": "~0.1.3",
- "fast-levenshtein": "~2.0.6",
- "levn": "~0.3.0",
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2",
- "word-wrap": "~1.2.3"
- }
- },
- "ora": {
- "version": "3.4.0",
- "resolved": "https://registry.npm.taobao.org/ora/download/ora-3.4.0.tgz?cache=0&sync_timestamp=1596812605371&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fora%2Fdownload%2Fora-3.4.0.tgz",
- "integrity": "sha1-vwdSSRBZo+8+1MhQl1Md6f280xg=",
- "dev": true,
- "requires": {
- "chalk": "^2.4.2",
- "cli-cursor": "^2.1.0",
- "cli-spinners": "^2.0.0",
- "log-symbols": "^2.2.0",
- "strip-ansi": "^5.2.0",
- "wcwidth": "^1.0.1"
- },
- "dependencies": {
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "original": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/original/download/original-1.0.2.tgz",
- "integrity": "sha1-5EKmHP/hxf0gpl8yYcJmY7MD8l8=",
- "dev": true,
- "requires": {
- "url-parse": "^1.4.3"
- }
- },
- "os-browserify": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/os-browserify/download/os-browserify-0.3.0.tgz",
- "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
- "dev": true
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/os-tmpdir/download/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
- },
- "p-finally": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/p-finally/download/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
- },
- "p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/p-limit/download/p-limit-2.3.0.tgz?cache=0&sync_timestamp=1594559734248&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fp-limit%2Fdownload%2Fp-limit-2.3.0.tgz",
- "integrity": "sha1-PdM8ZHohT9//2DWTPrCG2g3CHbE=",
- "dev": true,
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/p-locate/download/p-locate-3.0.0.tgz",
- "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=",
- "dev": true,
- "requires": {
- "p-limit": "^2.0.0"
- }
- },
- "p-map": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/p-map/download/p-map-3.0.0.tgz",
- "integrity": "sha1-1wTZr4orpoTiYA2aIVmD1BQal50=",
- "dev": true,
- "requires": {
- "aggregate-error": "^3.0.0"
- }
- },
- "p-retry": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/p-retry/download/p-retry-3.0.1.tgz",
- "integrity": "sha1-MWtMiJPiyNwc+okfQGxLQivr8yg=",
- "dev": true,
- "requires": {
- "retry": "^0.12.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/p-try/download/p-try-2.2.0.tgz",
- "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=",
- "dev": true
- },
- "pako": {
- "version": "1.0.11",
- "resolved": "https://registry.npm.taobao.org/pako/download/pako-1.0.11.tgz",
- "integrity": "sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8=",
- "dev": true
- },
- "parallel-transform": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/parallel-transform/download/parallel-transform-1.2.0.tgz",
- "integrity": "sha1-kEnKN9bLIYLDsdLHIL6U0UpYFPw=",
- "dev": true,
- "requires": {
- "cyclist": "^1.0.1",
- "inherits": "^2.0.3",
- "readable-stream": "^2.1.5"
- }
- },
- "param-case": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/param-case/download/param-case-2.1.1.tgz",
- "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
- "dev": true,
- "requires": {
- "no-case": "^2.2.0"
- }
- },
- "parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/parent-module/download/parent-module-1.0.1.tgz",
- "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=",
- "dev": true,
- "requires": {
- "callsites": "^3.0.0"
- },
- "dependencies": {
- "callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/callsites/download/callsites-3.1.0.tgz",
- "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=",
- "dev": true
- }
- }
- },
- "parse-asn1": {
- "version": "5.1.6",
- "resolved": "https://registry.npm.taobao.org/parse-asn1/download/parse-asn1-5.1.6.tgz?cache=0&sync_timestamp=1597167309380&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fparse-asn1%2Fdownload%2Fparse-asn1-5.1.6.tgz",
- "integrity": "sha1-OFCAo+wTy2KmLTlAnLPoiETNrtQ=",
- "dev": true,
- "requires": {
- "asn1.js": "^5.2.0",
- "browserify-aes": "^1.0.0",
- "evp_bytestokey": "^1.0.0",
- "pbkdf2": "^3.0.3",
- "safe-buffer": "^5.1.1"
- }
- },
- "parse-json": {
- "version": "5.0.1",
- "resolved": "https://registry.npm.taobao.org/parse-json/download/parse-json-5.0.1.tgz",
- "integrity": "sha1-fP41wczWQbzjmBRn5sLs5hs7OHg=",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-better-errors": "^1.0.1",
- "lines-and-columns": "^1.1.6"
- }
- },
- "parse5": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/parse5/download/parse5-5.1.1.tgz?cache=0&sync_timestamp=1595849263958&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fparse5%2Fdownload%2Fparse5-5.1.1.tgz",
- "integrity": "sha1-9o5OW6GFKsLK3AD0VV//bCq7YXg=",
- "dev": true
- },
- "parse5-htmlparser2-tree-adapter": {
- "version": "5.1.1",
- "resolved": "https://registry.npm.taobao.org/parse5-htmlparser2-tree-adapter/download/parse5-htmlparser2-tree-adapter-5.1.1.tgz?cache=0&sync_timestamp=1596089818598&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fparse5-htmlparser2-tree-adapter%2Fdownload%2Fparse5-htmlparser2-tree-adapter-5.1.1.tgz",
- "integrity": "sha1-6MdD1OkhlNUpPs3isIvjHmdGHLw=",
- "dev": true,
- "requires": {
- "parse5": "^5.1.1"
- }
- },
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz",
- "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=",
- "dev": true
- },
- "pascalcase": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/pascalcase/download/pascalcase-0.1.1.tgz",
- "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
- "dev": true
- },
- "path-browserify": {
- "version": "0.0.1",
- "resolved": "https://registry.npm.taobao.org/path-browserify/download/path-browserify-0.0.1.tgz",
- "integrity": "sha1-5sTd1+06onxoogzE5Q4aTug7vEo=",
- "dev": true
- },
- "path-dirname": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/path-dirname/download/path-dirname-1.0.2.tgz",
- "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
- "dev": true
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "path-is-inside": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/path-is-inside/download/path-is-inside-1.0.2.tgz",
- "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
- "dev": true
- },
- "path-key": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/path-key/download/path-key-2.0.1.tgz",
- "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
- "dev": true
- },
- "path-parse": {
- "version": "1.0.6",
- "resolved": "https://registry.npm.taobao.org/path-parse/download/path-parse-1.0.6.tgz",
- "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=",
- "dev": true
- },
- "path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
- "dev": true
- },
- "path-type": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz",
- "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=",
- "dev": true,
- "requires": {
- "pify": "^3.0.0"
- },
- "dependencies": {
- "pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
- }
- }
- },
- "pbkdf2": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/pbkdf2/download/pbkdf2-3.1.1.tgz",
- "integrity": "sha1-y4cksPramEWWhW0abrr9NYRlS5Q=",
- "dev": true,
- "requires": {
- "create-hash": "^1.1.2",
- "create-hmac": "^1.1.4",
- "ripemd160": "^2.0.1",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
- }
- },
- "performance-now": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/performance-now/download/performance-now-2.1.0.tgz",
- "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
- "dev": true
- },
- "picomatch": {
- "version": "2.2.2",
- "resolved": "https://registry.npm.taobao.org/picomatch/download/picomatch-2.2.2.tgz",
- "integrity": "sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=",
- "dev": true,
- "optional": true
- },
- "pify": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/pify/download/pify-4.0.1.tgz",
- "integrity": "sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=",
- "dev": true
- },
- "pinkie": {
- "version": "2.0.4",
- "resolved": "https://registry.npm.taobao.org/pinkie/download/pinkie-2.0.4.tgz",
- "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
- "dev": true
- },
- "pinkie-promise": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/pinkie-promise/download/pinkie-promise-2.0.1.tgz?cache=0&sync_timestamp=1589682729560&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpinkie-promise%2Fdownload%2Fpinkie-promise-2.0.1.tgz",
- "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
- "dev": true,
- "requires": {
- "pinkie": "^2.0.0"
- }
- },
- "pkg-dir": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-3.0.0.tgz",
- "integrity": "sha1-J0kCDyOe2ZCIGx9xIQ1R62UjvqM=",
- "dev": true,
- "requires": {
- "find-up": "^3.0.0"
- }
- },
- "pnp-webpack-plugin": {
- "version": "1.6.4",
- "resolved": "https://registry.npm.taobao.org/pnp-webpack-plugin/download/pnp-webpack-plugin-1.6.4.tgz?cache=0&sync_timestamp=1589684269502&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpnp-webpack-plugin%2Fdownload%2Fpnp-webpack-plugin-1.6.4.tgz",
- "integrity": "sha1-yXEaxNxIpoXauvyG+Lbdn434QUk=",
- "dev": true,
- "requires": {
- "ts-pnp": "^1.1.6"
- }
- },
- "portfinder": {
- "version": "1.0.28",
- "resolved": "https://registry.npm.taobao.org/portfinder/download/portfinder-1.0.28.tgz?cache=0&sync_timestamp=1596018176291&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fportfinder%2Fdownload%2Fportfinder-1.0.28.tgz",
- "integrity": "sha1-Z8RiKFK9U3TdHdkA93n1NGL6x3g=",
- "dev": true,
- "requires": {
- "async": "^2.6.2",
- "debug": "^3.1.1",
- "mkdirp": "^0.5.5"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-3.2.6.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-3.2.6.tgz",
- "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- }
- }
- },
- "posix-character-classes": {
- "version": "0.1.1",
- "resolved": "https://registry.npm.taobao.org/posix-character-classes/download/posix-character-classes-0.1.1.tgz",
- "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
- "dev": true
- },
- "postcss": {
- "version": "7.0.32",
- "resolved": "https://registry.npm.taobao.org/postcss/download/postcss-7.0.32.tgz",
- "integrity": "sha1-QxDW7jRwU9o0M9sr5JKIPWLOxZ0=",
- "dev": true,
- "requires": {
- "chalk": "^2.4.2",
- "source-map": "^0.6.1",
- "supports-color": "^6.1.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- },
- "supports-color": {
- "version": "6.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-6.1.0.tgz",
- "integrity": "sha1-B2Srxpxj1ayELdSGfo0CXogN+PM=",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
- }
- },
- "postcss-calc": {
- "version": "7.0.3",
- "resolved": "https://registry.npm.taobao.org/postcss-calc/download/postcss-calc-7.0.3.tgz?cache=0&sync_timestamp=1596805654356&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-calc%2Fdownload%2Fpostcss-calc-7.0.3.tgz",
- "integrity": "sha1-1lzKkqPFK/J603pfcy4Fh7dPFiM=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.27",
- "postcss-selector-parser": "^6.0.2",
- "postcss-value-parser": "^4.0.2"
- }
- },
- "postcss-colormin": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/postcss-colormin/download/postcss-colormin-4.0.3.tgz?cache=0&sync_timestamp=1597682964214&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-colormin%2Fdownload%2Fpostcss-colormin-4.0.3.tgz",
- "integrity": "sha1-rgYLzpPteUrHEmTwgTLVUJVr04E=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "color": "^3.0.0",
- "has": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-convert-values": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-convert-values/download/postcss-convert-values-4.0.1.tgz?cache=0&sync_timestamp=1597682964333&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-convert-values%2Fdownload%2Fpostcss-convert-values-4.0.1.tgz",
- "integrity": "sha1-yjgT7U2g+BL51DcDWE5Enr4Ymn8=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-discard-comments": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-discard-comments/download/postcss-discard-comments-4.0.2.tgz?cache=0&sync_timestamp=1597682964431&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-discard-comments%2Fdownload%2Fpostcss-discard-comments-4.0.2.tgz",
- "integrity": "sha1-H7q9LCRr/2qq15l7KwkY9NevQDM=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "postcss-discard-duplicates": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-discard-duplicates/download/postcss-discard-duplicates-4.0.2.tgz?cache=0&sync_timestamp=1597682964524&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-discard-duplicates%2Fdownload%2Fpostcss-discard-duplicates-4.0.2.tgz",
- "integrity": "sha1-P+EzzTyCKC5VD8myORdqkge3hOs=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "postcss-discard-empty": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-discard-empty/download/postcss-discard-empty-4.0.1.tgz?cache=0&sync_timestamp=1597682964629&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-discard-empty%2Fdownload%2Fpostcss-discard-empty-4.0.1.tgz",
- "integrity": "sha1-yMlR6fc+2UKAGUWERKAq2Qu592U=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "postcss-discard-overridden": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-discard-overridden/download/postcss-discard-overridden-4.0.1.tgz?cache=0&sync_timestamp=1597682964720&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-discard-overridden%2Fdownload%2Fpostcss-discard-overridden-4.0.1.tgz",
- "integrity": "sha1-ZSrvipZybwKfXj4AFG7npOdV/1c=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "postcss-load-config": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/postcss-load-config/download/postcss-load-config-2.1.0.tgz",
- "integrity": "sha1-yE1pK3u3tB3c7ZTuYuirMbQXsAM=",
- "dev": true,
- "requires": {
- "cosmiconfig": "^5.0.0",
- "import-cwd": "^2.0.0"
- }
- },
- "postcss-loader": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/postcss-loader/download/postcss-loader-3.0.0.tgz",
- "integrity": "sha1-a5eUPkfHLYRfqeA/Jzdz1OjdbC0=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0",
- "postcss": "^7.0.0",
- "postcss-load-config": "^2.0.0",
- "schema-utils": "^1.0.0"
- },
- "dependencies": {
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- }
- }
- },
- "postcss-merge-longhand": {
- "version": "4.0.11",
- "resolved": "https://registry.npm.taobao.org/postcss-merge-longhand/download/postcss-merge-longhand-4.0.11.tgz?cache=0&sync_timestamp=1597684735557&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-merge-longhand%2Fdownload%2Fpostcss-merge-longhand-4.0.11.tgz",
- "integrity": "sha1-YvSaE+Sg7gTnuY9CuxYGLKJUniQ=",
- "dev": true,
- "requires": {
- "css-color-names": "0.0.4",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0",
- "stylehacks": "^4.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-merge-rules": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/postcss-merge-rules/download/postcss-merge-rules-4.0.3.tgz?cache=0&sync_timestamp=1597684597276&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-merge-rules%2Fdownload%2Fpostcss-merge-rules-4.0.3.tgz",
- "integrity": "sha1-NivqT/Wh+Y5AdacTxsslrv75plA=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "caniuse-api": "^3.0.0",
- "cssnano-util-same-parent": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-selector-parser": "^3.0.0",
- "vendors": "^1.0.0"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.2",
- "resolved": "https://registry.npm.taobao.org/postcss-selector-parser/download/postcss-selector-parser-3.1.2.tgz",
- "integrity": "sha1-sxD1xMD9r3b5SQK7qjDbaqhPUnA=",
- "dev": true,
- "requires": {
- "dot-prop": "^5.2.0",
- "indexes-of": "^1.0.1",
- "uniq": "^1.0.1"
- }
- }
- }
- },
- "postcss-minify-font-values": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-minify-font-values/download/postcss-minify-font-values-4.0.2.tgz?cache=0&sync_timestamp=1597684597341&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-minify-font-values%2Fdownload%2Fpostcss-minify-font-values-4.0.2.tgz",
- "integrity": "sha1-zUw0TM5HQ0P6xdgiBqssvLiv1aY=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-minify-gradients": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-minify-gradients/download/postcss-minify-gradients-4.0.2.tgz?cache=0&sync_timestamp=1597684736463&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-minify-gradients%2Fdownload%2Fpostcss-minify-gradients-4.0.2.tgz",
- "integrity": "sha1-k7KcL/UJnFNe7NpWxKpuZlpmNHE=",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "^4.0.0",
- "is-color-stop": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-minify-params": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-minify-params/download/postcss-minify-params-4.0.2.tgz?cache=0&sync_timestamp=1597684736706&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-minify-params%2Fdownload%2Fpostcss-minify-params-4.0.2.tgz",
- "integrity": "sha1-a5zvAwwR41Jh+V9hjJADbWgNuHQ=",
- "dev": true,
- "requires": {
- "alphanum-sort": "^1.0.0",
- "browserslist": "^4.0.0",
- "cssnano-util-get-arguments": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0",
- "uniqs": "^2.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-minify-selectors": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-minify-selectors/download/postcss-minify-selectors-4.0.2.tgz?cache=0&sync_timestamp=1597684737076&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-minify-selectors%2Fdownload%2Fpostcss-minify-selectors-4.0.2.tgz",
- "integrity": "sha1-4uXrQL/uUA0M2SQ1APX46kJi+9g=",
- "dev": true,
- "requires": {
- "alphanum-sort": "^1.0.0",
- "has": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-selector-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.2",
- "resolved": "https://registry.npm.taobao.org/postcss-selector-parser/download/postcss-selector-parser-3.1.2.tgz",
- "integrity": "sha1-sxD1xMD9r3b5SQK7qjDbaqhPUnA=",
- "dev": true,
- "requires": {
- "dot-prop": "^5.2.0",
- "indexes-of": "^1.0.1",
- "uniq": "^1.0.1"
- }
- }
- }
- },
- "postcss-modules": {
- "version": "3.2.0",
- "resolved": "https://registry.npm.taobao.org/postcss-modules/download/postcss-modules-3.2.0.tgz?cache=0&sync_timestamp=1593589775045&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-modules%2Fdownload%2Fpostcss-modules-3.2.0.tgz",
- "integrity": "sha1-HKhw0ZfNCaeWQlPhLeKqyQbJQlY=",
- "dev": true,
- "requires": {
- "generic-names": "^2.0.1",
- "icss-replace-symbols": "^1.1.0",
- "lodash.camelcase": "^4.3.0",
- "postcss": "^7.0.32",
- "postcss-modules-extract-imports": "^2.0.0",
- "postcss-modules-local-by-default": "^3.0.2",
- "postcss-modules-scope": "^2.2.0",
- "postcss-modules-values": "^3.0.0",
- "string-hash": "^1.1.1"
- }
- },
- "postcss-modules-extract-imports": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/postcss-modules-extract-imports/download/postcss-modules-extract-imports-2.0.0.tgz",
- "integrity": "sha1-gYcZoa4doyX5gyRGsBE27rSTzX4=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.5"
- }
- },
- "postcss-modules-local-by-default": {
- "version": "3.0.3",
- "resolved": "https://registry.npm.taobao.org/postcss-modules-local-by-default/download/postcss-modules-local-by-default-3.0.3.tgz?cache=0&sync_timestamp=1595733620602&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-modules-local-by-default%2Fdownload%2Fpostcss-modules-local-by-default-3.0.3.tgz",
- "integrity": "sha1-uxTgzHgnnVBNvcv9fgyiiZP/u7A=",
- "dev": true,
- "requires": {
- "icss-utils": "^4.1.1",
- "postcss": "^7.0.32",
- "postcss-selector-parser": "^6.0.2",
- "postcss-value-parser": "^4.1.0"
- }
- },
- "postcss-modules-scope": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/postcss-modules-scope/download/postcss-modules-scope-2.2.0.tgz",
- "integrity": "sha1-OFyuATzHdD9afXYC0Qc6iequYu4=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.6",
- "postcss-selector-parser": "^6.0.0"
- }
- },
- "postcss-modules-values": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/postcss-modules-values/download/postcss-modules-values-3.0.0.tgz",
- "integrity": "sha1-W1AA1uuuKbQlUwG0o6VFdEI+fxA=",
- "dev": true,
- "requires": {
- "icss-utils": "^4.0.0",
- "postcss": "^7.0.6"
- }
- },
- "postcss-normalize-charset": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-charset/download/postcss-normalize-charset-4.0.1.tgz?cache=0&sync_timestamp=1597684737335&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-charset%2Fdownload%2Fpostcss-normalize-charset-4.0.1.tgz",
- "integrity": "sha1-izWt067oOhNrBHHg1ZvlilAoXdQ=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0"
- }
- },
- "postcss-normalize-display-values": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-display-values/download/postcss-normalize-display-values-4.0.2.tgz?cache=0&sync_timestamp=1597683104006&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-display-values%2Fdownload%2Fpostcss-normalize-display-values-4.0.2.tgz",
- "integrity": "sha1-Db4EpM6QY9RmftK+R2u4MMglk1o=",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-positions": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-positions/download/postcss-normalize-positions-4.0.2.tgz?cache=0&sync_timestamp=1597683104141&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-positions%2Fdownload%2Fpostcss-normalize-positions-4.0.2.tgz",
- "integrity": "sha1-BfdX+E8mBDc3g2ipH4ky1LECkX8=",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "^4.0.0",
- "has": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-repeat-style": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-repeat-style/download/postcss-normalize-repeat-style-4.0.2.tgz?cache=0&sync_timestamp=1597683104230&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-repeat-style%2Fdownload%2Fpostcss-normalize-repeat-style-4.0.2.tgz",
- "integrity": "sha1-xOu8KJ85kaAo1EdRy90RkYsXkQw=",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "^4.0.0",
- "cssnano-util-get-match": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-string": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-string/download/postcss-normalize-string-4.0.2.tgz?cache=0&sync_timestamp=1597683104306&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-string%2Fdownload%2Fpostcss-normalize-string-4.0.2.tgz",
- "integrity": "sha1-zUTECrB6DHo23F6Zqs4eyk7CaQw=",
- "dev": true,
- "requires": {
- "has": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-timing-functions": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-timing-functions/download/postcss-normalize-timing-functions-4.0.2.tgz?cache=0&sync_timestamp=1597683104399&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-timing-functions%2Fdownload%2Fpostcss-normalize-timing-functions-4.0.2.tgz",
- "integrity": "sha1-jgCcoqOUnNr4rSPmtquZy159KNk=",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-unicode": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-unicode/download/postcss-normalize-unicode-4.0.1.tgz?cache=0&sync_timestamp=1597683104480&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-unicode%2Fdownload%2Fpostcss-normalize-unicode-4.0.1.tgz",
- "integrity": "sha1-hBvUj9zzAZrUuqdJOj02O1KuHPs=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-url": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-url/download/postcss-normalize-url-4.0.1.tgz?cache=0&sync_timestamp=1597682956775&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-normalize-url%2Fdownload%2Fpostcss-normalize-url-4.0.1.tgz",
- "integrity": "sha1-EOQ3+GvHx+WPe5ZS7YeNqqlfquE=",
- "dev": true,
- "requires": {
- "is-absolute-url": "^2.0.0",
- "normalize-url": "^3.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-normalize-whitespace": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-normalize-whitespace/download/postcss-normalize-whitespace-4.0.2.tgz",
- "integrity": "sha1-vx1AcP5Pzqh9E0joJdjMDF+qfYI=",
- "dev": true,
- "requires": {
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-ordered-values": {
- "version": "4.1.2",
- "resolved": "https://registry.npm.taobao.org/postcss-ordered-values/download/postcss-ordered-values-4.1.2.tgz?cache=0&sync_timestamp=1597682957024&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-ordered-values%2Fdownload%2Fpostcss-ordered-values-4.1.2.tgz",
- "integrity": "sha1-DPdcgg7H1cTSgBiVWeC1ceusDu4=",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-reduce-initial": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/postcss-reduce-initial/download/postcss-reduce-initial-4.0.3.tgz?cache=0&sync_timestamp=1597682957212&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-reduce-initial%2Fdownload%2Fpostcss-reduce-initial-4.0.3.tgz",
- "integrity": "sha1-f9QuvqXpyBRgljniwuhK4nC6SN8=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "caniuse-api": "^3.0.0",
- "has": "^1.0.0",
- "postcss": "^7.0.0"
- }
- },
- "postcss-reduce-transforms": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-reduce-transforms/download/postcss-reduce-transforms-4.0.2.tgz?cache=0&sync_timestamp=1597682957300&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-reduce-transforms%2Fdownload%2Fpostcss-reduce-transforms-4.0.2.tgz",
- "integrity": "sha1-F++kBerMbge+NBSlyi0QdGgdTik=",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "^4.0.0",
- "has": "^1.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-selector-parser": {
- "version": "6.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-selector-parser/download/postcss-selector-parser-6.0.2.tgz",
- "integrity": "sha1-k0z3mdAWyDQRhZ4J3Oyt4BKG7Fw=",
- "dev": true,
- "requires": {
- "cssesc": "^3.0.0",
- "indexes-of": "^1.0.1",
- "uniq": "^1.0.1"
- }
- },
- "postcss-svgo": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/postcss-svgo/download/postcss-svgo-4.0.2.tgz?cache=0&sync_timestamp=1597682957408&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-svgo%2Fdownload%2Fpostcss-svgo-4.0.2.tgz",
- "integrity": "sha1-F7mXvHEbMzurFDqu07jT1uPTglg=",
- "dev": true,
- "requires": {
- "is-svg": "^3.0.0",
- "postcss": "^7.0.0",
- "postcss-value-parser": "^3.0.0",
- "svgo": "^1.0.0"
- },
- "dependencies": {
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=",
- "dev": true
- }
- }
- },
- "postcss-unique-selectors": {
- "version": "4.0.1",
- "resolved": "https://registry.npm.taobao.org/postcss-unique-selectors/download/postcss-unique-selectors-4.0.1.tgz?cache=0&sync_timestamp=1597682957541&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-unique-selectors%2Fdownload%2Fpostcss-unique-selectors-4.0.1.tgz",
- "integrity": "sha1-lEaRHzKJv9ZMbWgPBzwDsfnuS6w=",
- "dev": true,
- "requires": {
- "alphanum-sort": "^1.0.0",
- "postcss": "^7.0.0",
- "uniqs": "^2.0.0"
- }
- },
- "postcss-value-parser": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-4.1.0.tgz",
- "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=",
- "dev": true
- },
- "prelude-ls": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz",
- "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
- "dev": true
- },
- "prepend-http": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/prepend-http/download/prepend-http-1.0.4.tgz",
- "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
- "dev": true
- },
- "prettier": {
- "version": "1.19.1",
- "resolved": "https://registry.npm.taobao.org/prettier/download/prettier-1.19.1.tgz?cache=0&sync_timestamp=1589682761987&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fprettier%2Fdownload%2Fprettier-1.19.1.tgz",
- "integrity": "sha1-99f1/4qc2HKnvkyhQglZVqYHl8s=",
- "dev": true,
- "optional": true
- },
- "pretty-error": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/pretty-error/download/pretty-error-2.1.1.tgz",
- "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=",
- "dev": true,
- "requires": {
- "renderkid": "^2.0.1",
- "utila": "~0.4"
- }
- },
- "process": {
- "version": "0.11.10",
- "resolved": "https://registry.npm.taobao.org/process/download/process-0.11.10.tgz",
- "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
- "dev": true
- },
- "process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.1.tgz",
- "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=",
- "dev": true
- },
- "progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/progress/download/progress-2.0.3.tgz",
- "integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=",
- "dev": true
- },
- "promise-inflight": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/promise-inflight/download/promise-inflight-1.0.1.tgz",
- "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
- "dev": true
- },
- "proxy-addr": {
- "version": "2.0.6",
- "resolved": "https://registry.npm.taobao.org/proxy-addr/download/proxy-addr-2.0.6.tgz",
- "integrity": "sha1-/cIzZQVEfT8vLGOO0nLK9hS7sr8=",
- "dev": true,
- "requires": {
- "forwarded": "~0.1.2",
- "ipaddr.js": "1.9.1"
- }
- },
- "prr": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/prr/download/prr-1.0.1.tgz",
- "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
- "dev": true
- },
- "pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz",
- "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
- "dev": true
- },
- "psl": {
- "version": "1.8.0",
- "resolved": "https://registry.npm.taobao.org/psl/download/psl-1.8.0.tgz",
- "integrity": "sha1-kyb4vPsBOtzABf3/BWrM4CDlHCQ=",
- "dev": true
- },
- "public-encrypt": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/public-encrypt/download/public-encrypt-4.0.3.tgz",
- "integrity": "sha1-T8ydd6B+SLp1J+fL4N4z0HATMeA=",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "browserify-rsa": "^4.0.0",
- "create-hash": "^1.1.0",
- "parse-asn1": "^5.0.0",
- "randombytes": "^2.0.1",
- "safe-buffer": "^5.1.2"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npm.taobao.org/bn.js/download/bn.js-4.11.9.tgz",
- "integrity": "sha1-JtVWgpRY+dHoH8SJUkk9C6NQeCg=",
- "dev": true
- }
- }
- },
- "pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/pump/download/pump-3.0.0.tgz",
- "integrity": "sha1-tKIRaBW94vTh6mAjVOjHVWUQemQ=",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "pumpify": {
- "version": "1.5.1",
- "resolved": "https://registry.npm.taobao.org/pumpify/download/pumpify-1.5.1.tgz",
- "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=",
- "dev": true,
- "requires": {
- "duplexify": "^3.6.0",
- "inherits": "^2.0.3",
- "pump": "^2.0.0"
- },
- "dependencies": {
- "pump": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/pump/download/pump-2.0.1.tgz",
- "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- }
- }
- },
- "punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/punycode/download/punycode-2.1.1.tgz?cache=0&sync_timestamp=1589682803838&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpunycode%2Fdownload%2Fpunycode-2.1.1.tgz",
- "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=",
- "dev": true
- },
- "q": {
- "version": "1.5.1",
- "resolved": "https://registry.npm.taobao.org/q/download/q-1.5.1.tgz?cache=0&sync_timestamp=1589682817412&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fq%2Fdownload%2Fq-1.5.1.tgz",
- "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
- "dev": true
- },
- "qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.5.2.tgz",
- "integrity": "sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=",
- "dev": true
- },
- "query-string": {
- "version": "4.3.4",
- "resolved": "https://registry.npm.taobao.org/query-string/download/query-string-4.3.4.tgz",
- "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
- "dev": true,
- "requires": {
- "object-assign": "^4.1.0",
- "strict-uri-encode": "^1.0.0"
- }
- },
- "querystring": {
- "version": "0.2.0",
- "resolved": "https://registry.npm.taobao.org/querystring/download/querystring-0.2.0.tgz?cache=0&sync_timestamp=1589682791876&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fquerystring%2Fdownload%2Fquerystring-0.2.0.tgz",
- "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
- "dev": true
- },
- "querystring-es3": {
- "version": "0.2.1",
- "resolved": "https://registry.npm.taobao.org/querystring-es3/download/querystring-es3-0.2.1.tgz",
- "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
- "dev": true
- },
- "querystringify": {
- "version": "2.2.0",
- "resolved": "https://registry.npm.taobao.org/querystringify/download/querystringify-2.2.0.tgz?cache=0&sync_timestamp=1597686771604&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fquerystringify%2Fdownload%2Fquerystringify-2.2.0.tgz",
- "integrity": "sha1-M0WUG0FTy50ILY7uTNogFqmu9/Y=",
- "dev": true
- },
- "randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/randombytes/download/randombytes-2.1.0.tgz",
- "integrity": "sha1-32+ENy8CcNxlzfYpE0mrekc9Tyo=",
- "dev": true,
- "requires": {
- "safe-buffer": "^5.1.0"
- }
- },
- "randomfill": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/randomfill/download/randomfill-1.0.4.tgz",
- "integrity": "sha1-ySGW/IarQr6YPxvzF3giSTHWFFg=",
- "dev": true,
- "requires": {
- "randombytes": "^2.0.5",
- "safe-buffer": "^5.1.0"
- }
- },
- "range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npm.taobao.org/range-parser/download/range-parser-1.2.1.tgz",
- "integrity": "sha1-PPNwI9GZ4cJNGlW4SADC8+ZGgDE=",
- "dev": true
- },
- "raw-body": {
- "version": "2.4.0",
- "resolved": "https://registry.npm.taobao.org/raw-body/download/raw-body-2.4.0.tgz",
- "integrity": "sha1-oc5vucm8NWylLoklarWQWeE9AzI=",
- "dev": true,
- "requires": {
- "bytes": "3.1.0",
- "http-errors": "1.7.2",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- }
- },
- "read-pkg": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/read-pkg/download/read-pkg-5.2.0.tgz?cache=0&sync_timestamp=1589682810106&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fread-pkg%2Fdownload%2Fread-pkg-5.2.0.tgz",
- "integrity": "sha1-e/KVQ4yloz5WzTDgU7NO5yUMk8w=",
- "dev": true,
- "requires": {
- "@types/normalize-package-data": "^2.4.0",
- "normalize-package-data": "^2.5.0",
- "parse-json": "^5.0.0",
- "type-fest": "^0.6.0"
- }
- },
- "readable-stream": {
- "version": "2.3.7",
- "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.7.tgz?cache=0&sync_timestamp=1589682741447&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-2.3.7.tgz",
- "integrity": "sha1-Hsoc9xGu+BTAT2IlKjamL2yyO1c=",
- "dev": true,
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "readdirp": {
- "version": "3.4.0",
- "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-3.4.0.tgz",
- "integrity": "sha1-n9zN+ekVWAVEkiGsZF6DA6tbmto=",
- "dev": true,
- "optional": true,
- "requires": {
- "picomatch": "^2.2.1"
- }
- },
- "rechoir": {
- "version": "0.6.2",
- "resolved": "https://registry.npm.taobao.org/rechoir/download/rechoir-0.6.2.tgz",
- "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
- "dev": true,
- "requires": {
- "resolve": "^1.1.6"
- }
- },
- "regenerate": {
- "version": "1.4.1",
- "resolved": "https://registry.npm.taobao.org/regenerate/download/regenerate-1.4.1.tgz",
- "integrity": "sha1-ytkq2Oa1kXc0hfvgWkhcr09Ffm8=",
- "dev": true
- },
- "regenerate-unicode-properties": {
- "version": "8.2.0",
- "resolved": "https://registry.npm.taobao.org/regenerate-unicode-properties/download/regenerate-unicode-properties-8.2.0.tgz",
- "integrity": "sha1-5d5xEdZV57pgwFfb6f83yH5lzew=",
- "dev": true,
- "requires": {
- "regenerate": "^1.4.0"
- }
- },
- "regenerator-runtime": {
- "version": "0.13.7",
- "resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.13.7.tgz?cache=0&sync_timestamp=1595456311465&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.13.7.tgz",
- "integrity": "sha1-ysLazIoepnX+qrrriugziYrkb1U=",
- "dev": true
- },
- "regenerator-transform": {
- "version": "0.14.5",
- "resolved": "https://registry.npm.taobao.org/regenerator-transform/download/regenerator-transform-0.14.5.tgz",
- "integrity": "sha1-yY2hVGg2ccnE3LFuznNlF+G3/rQ=",
- "dev": true,
- "requires": {
- "@babel/runtime": "^7.8.4"
- }
- },
- "regex-not": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/regex-not/download/regex-not-1.0.2.tgz",
- "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=",
- "dev": true,
- "requires": {
- "extend-shallow": "^3.0.2",
- "safe-regex": "^1.1.0"
- }
- },
- "regexp.prototype.flags": {
- "version": "1.3.0",
- "resolved": "https://registry.npm.taobao.org/regexp.prototype.flags/download/regexp.prototype.flags-1.3.0.tgz",
- "integrity": "sha1-erqJs8E6ZFCdq888qNn7ub31y3U=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.0-next.1"
- }
- },
- "regexpp": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/regexpp/download/regexpp-2.0.1.tgz",
- "integrity": "sha1-jRnTHPYySCtYkEn4KB+T28uk0H8=",
- "dev": true
- },
- "regexpu-core": {
- "version": "4.7.0",
- "resolved": "https://registry.npm.taobao.org/regexpu-core/download/regexpu-core-4.7.0.tgz",
- "integrity": "sha1-/L9FjFBDGwu3tF1pZ7gZLZHz2Tg=",
- "dev": true,
- "requires": {
- "regenerate": "^1.4.0",
- "regenerate-unicode-properties": "^8.2.0",
- "regjsgen": "^0.5.1",
- "regjsparser": "^0.6.4",
- "unicode-match-property-ecmascript": "^1.0.4",
- "unicode-match-property-value-ecmascript": "^1.2.0"
- }
- },
- "regjsgen": {
- "version": "0.5.2",
- "resolved": "https://registry.npm.taobao.org/regjsgen/download/regjsgen-0.5.2.tgz",
- "integrity": "sha1-kv8pX7He7L9uzaslQ9IH6RqjNzM=",
- "dev": true
- },
- "regjsparser": {
- "version": "0.6.4",
- "resolved": "https://registry.npm.taobao.org/regjsparser/download/regjsparser-0.6.4.tgz",
- "integrity": "sha1-p2n4aEMIQBpm6bUp0kNv9NBmYnI=",
- "dev": true,
- "requires": {
- "jsesc": "~0.5.0"
- },
- "dependencies": {
- "jsesc": {
- "version": "0.5.0",
- "resolved": "https://registry.npm.taobao.org/jsesc/download/jsesc-0.5.0.tgz",
- "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
- "dev": true
- }
- }
- },
- "relateurl": {
- "version": "0.2.7",
- "resolved": "https://registry.npm.taobao.org/relateurl/download/relateurl-0.2.7.tgz",
- "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
- "dev": true
- },
- "remove-trailing-separator": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/remove-trailing-separator/download/remove-trailing-separator-1.1.0.tgz",
- "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
- "dev": true
- },
- "renderkid": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/renderkid/download/renderkid-2.0.3.tgz",
- "integrity": "sha1-OAF5wv9a4TZcUivy/Pz/AcW3QUk=",
- "dev": true,
- "requires": {
- "css-select": "^1.1.0",
- "dom-converter": "^0.2",
- "htmlparser2": "^3.3.0",
- "strip-ansi": "^3.0.0",
- "utila": "^0.4.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "css-select": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/css-select/download/css-select-1.2.0.tgz",
- "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
- "dev": true,
- "requires": {
- "boolbase": "~1.0.0",
- "css-what": "2.1",
- "domutils": "1.5.1",
- "nth-check": "~1.0.1"
- }
- },
- "css-what": {
- "version": "2.1.3",
- "resolved": "https://registry.npm.taobao.org/css-what/download/css-what-2.1.3.tgz",
- "integrity": "sha1-ptdgRXM2X+dGhsPzEcVlE9iChfI=",
- "dev": true
- },
- "domutils": {
- "version": "1.5.1",
- "resolved": "https://registry.npm.taobao.org/domutils/download/domutils-1.5.1.tgz?cache=0&sync_timestamp=1597680509643&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomutils%2Fdownload%2Fdomutils-1.5.1.tgz",
- "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
- "dev": true,
- "requires": {
- "dom-serializer": "0",
- "domelementtype": "1"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- }
- }
- },
- "repeat-element": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/repeat-element/download/repeat-element-1.1.3.tgz",
- "integrity": "sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4=",
- "dev": true
- },
- "repeat-string": {
- "version": "1.6.1",
- "resolved": "https://registry.npm.taobao.org/repeat-string/download/repeat-string-1.6.1.tgz",
- "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
- "dev": true
- },
- "request": {
- "version": "2.88.2",
- "resolved": "https://registry.npm.taobao.org/request/download/request-2.88.2.tgz?cache=0&sync_timestamp=1589682741998&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Frequest%2Fdownload%2Frequest-2.88.2.tgz",
- "integrity": "sha1-1zyRhzHLWofaBH4gcjQUb2ZNErM=",
- "dev": true,
- "requires": {
- "aws-sign2": "~0.7.0",
- "aws4": "^1.8.0",
- "caseless": "~0.12.0",
- "combined-stream": "~1.0.6",
- "extend": "~3.0.2",
- "forever-agent": "~0.6.1",
- "form-data": "~2.3.2",
- "har-validator": "~5.1.3",
- "http-signature": "~1.2.0",
- "is-typedarray": "~1.0.0",
- "isstream": "~0.1.2",
- "json-stringify-safe": "~5.0.1",
- "mime-types": "~2.1.19",
- "oauth-sign": "~0.9.0",
- "performance-now": "^2.1.0",
- "qs": "~6.5.2",
- "safe-buffer": "^5.1.2",
- "tough-cookie": "~2.5.0",
- "tunnel-agent": "^0.6.0",
- "uuid": "^3.3.2"
- }
- },
- "require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
- },
- "require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/require-main-filename/download/require-main-filename-2.0.0.tgz",
- "integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=",
- "dev": true
- },
- "requires-port": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/requires-port/download/requires-port-1.0.0.tgz",
- "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
- "dev": true
- },
- "resolve": {
- "version": "1.17.0",
- "resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.17.0.tgz?cache=0&sync_timestamp=1589682751623&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fresolve%2Fdownload%2Fresolve-1.17.0.tgz",
- "integrity": "sha1-sllBtUloIxzC0bt2p5y38sC/hEQ=",
- "dev": true,
- "requires": {
- "path-parse": "^1.0.6"
- }
- },
- "resolve-cwd": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/resolve-cwd/download/resolve-cwd-2.0.0.tgz",
- "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
- "dev": true,
- "requires": {
- "resolve-from": "^3.0.0"
- }
- },
- "resolve-from": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/resolve-from/download/resolve-from-3.0.0.tgz",
- "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
- "dev": true
- },
- "resolve-url": {
- "version": "0.2.1",
- "resolved": "https://registry.npm.taobao.org/resolve-url/download/resolve-url-0.2.1.tgz",
- "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
- "dev": true
- },
- "restore-cursor": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/restore-cursor/download/restore-cursor-2.0.0.tgz",
- "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
- "dev": true,
- "requires": {
- "onetime": "^2.0.0",
- "signal-exit": "^3.0.2"
- }
- },
- "ret": {
- "version": "0.1.15",
- "resolved": "https://registry.npm.taobao.org/ret/download/ret-0.1.15.tgz",
- "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=",
- "dev": true
- },
- "retry": {
- "version": "0.12.0",
- "resolved": "https://registry.npm.taobao.org/retry/download/retry-0.12.0.tgz",
- "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
- "dev": true
- },
- "rgb-regex": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/rgb-regex/download/rgb-regex-1.0.1.tgz",
- "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=",
- "dev": true
- },
- "rgba-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/rgba-regex/download/rgba-regex-1.0.0.tgz",
- "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
- "dev": true
- },
- "rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npm.taobao.org/rimraf/download/rimraf-2.7.1.tgz?cache=0&sync_timestamp=1589682814592&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Frimraf%2Fdownload%2Frimraf-2.7.1.tgz",
- "integrity": "sha1-NXl/E6f9rcVmFCwp1PB8ytSD4+w=",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "ripemd160": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/ripemd160/download/ripemd160-2.0.2.tgz",
- "integrity": "sha1-ocGm9iR1FXe6XQeRTLyShQWFiQw=",
- "dev": true,
- "requires": {
- "hash-base": "^3.0.0",
- "inherits": "^2.0.1"
- }
- },
- "run-async": {
- "version": "2.4.1",
- "resolved": "https://registry.npm.taobao.org/run-async/download/run-async-2.4.1.tgz",
- "integrity": "sha1-hEDsz5nqPnC9QJ1JqriOEMGJpFU=",
- "dev": true
- },
- "run-queue": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/run-queue/download/run-queue-1.0.3.tgz",
- "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
- "dev": true,
- "requires": {
- "aproba": "^1.1.1"
- }
- },
- "rxjs": {
- "version": "6.6.2",
- "resolved": "https://registry.npm.taobao.org/rxjs/download/rxjs-6.6.2.tgz",
- "integrity": "sha1-gJanrAPyzE/lhg725XKBDZ4BwNI=",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz?cache=0&sync_timestamp=1589682795646&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafe-buffer%2Fdownload%2Fsafe-buffer-5.1.2.tgz",
- "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=",
- "dev": true
- },
- "safe-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/safe-regex/download/safe-regex-1.1.0.tgz?cache=0&sync_timestamp=1589682757445&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafe-regex%2Fdownload%2Fsafe-regex-1.1.0.tgz",
- "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
- "dev": true,
- "requires": {
- "ret": "~0.1.10"
- }
- },
- "safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/safer-buffer/download/safer-buffer-2.1.2.tgz?cache=0&sync_timestamp=1589682784154&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafer-buffer%2Fdownload%2Fsafer-buffer-2.1.2.tgz",
- "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=",
- "dev": true
- },
- "sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npm.taobao.org/sax/download/sax-1.2.4.tgz",
- "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
- "dev": true
- },
- "schema-utils": {
- "version": "2.7.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-2.7.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-2.7.0.tgz",
- "integrity": "sha1-FxUfdtjq5n+793lgwzxnatn078c=",
- "dev": true,
- "requires": {
- "@types/json-schema": "^7.0.4",
- "ajv": "^6.12.2",
- "ajv-keywords": "^3.4.1"
- }
- },
- "select-hose": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/select-hose/download/select-hose-2.0.0.tgz",
- "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
- "dev": true
- },
- "selfsigned": {
- "version": "1.10.7",
- "resolved": "https://registry.npm.taobao.org/selfsigned/download/selfsigned-1.10.7.tgz",
- "integrity": "sha1-2lgZ/QSdVXTyjoipvMbbxubzkGs=",
- "dev": true,
- "requires": {
- "node-forge": "0.9.0"
- }
- },
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz",
- "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=",
- "dev": true
- },
- "send": {
- "version": "0.17.1",
- "resolved": "https://registry.npm.taobao.org/send/download/send-0.17.1.tgz",
- "integrity": "sha1-wdiwWfeQD3Rm3Uk4vcROEd2zdsg=",
- "dev": true,
- "requires": {
- "debug": "2.6.9",
- "depd": "~1.1.2",
- "destroy": "~1.0.4",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "~1.7.2",
- "mime": "1.6.0",
- "ms": "2.1.1",
- "on-finished": "~2.3.0",
- "range-parser": "~1.2.1",
- "statuses": "~1.5.0"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- },
- "dependencies": {
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz?cache=0&sync_timestamp=1590635592890&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-1.6.0.tgz",
- "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=",
- "dev": true
- },
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.1.tgz",
- "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=",
- "dev": true
- }
- }
- },
- "serialize-javascript": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/serialize-javascript/download/serialize-javascript-4.0.0.tgz",
- "integrity": "sha1-tSXhI4SJpez8Qq+sw/6Z5mb0sao=",
- "dev": true,
- "requires": {
- "randombytes": "^2.1.0"
- }
- },
- "serve-index": {
- "version": "1.9.1",
- "resolved": "https://registry.npm.taobao.org/serve-index/download/serve-index-1.9.1.tgz",
- "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
- "dev": true,
- "requires": {
- "accepts": "~1.3.4",
- "batch": "0.6.1",
- "debug": "2.6.9",
- "escape-html": "~1.0.3",
- "http-errors": "~1.6.2",
- "mime-types": "~2.1.17",
- "parseurl": "~1.3.2"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npm.taobao.org/http-errors/download/http-errors-1.6.3.tgz?cache=0&sync_timestamp=1593407858306&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhttp-errors%2Fdownload%2Fhttp-errors-1.6.3.tgz",
- "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
- "dev": true,
- "requires": {
- "depd": "~1.1.2",
- "inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
- "setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.0.tgz",
- "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=",
- "dev": true
- }
- }
- },
- "serve-static": {
- "version": "1.14.1",
- "resolved": "https://registry.npm.taobao.org/serve-static/download/serve-static-1.14.1.tgz",
- "integrity": "sha1-Zm5jbcTwEPfvKZcKiKZ0MgiYsvk=",
- "dev": true,
- "requires": {
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.17.1"
- }
- },
- "set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "dev": true
- },
- "set-value": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/set-value/download/set-value-2.0.1.tgz",
- "integrity": "sha1-oY1AUw5vB95CKMfe/kInr4ytAFs=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.3",
- "split-string": "^3.0.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- }
- }
- },
- "setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npm.taobao.org/setimmediate/download/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
- "dev": true
- },
- "setprototypeof": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.1.tgz",
- "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM=",
- "dev": true
- },
- "sha.js": {
- "version": "2.4.11",
- "resolved": "https://registry.npm.taobao.org/sha.js/download/sha.js-2.4.11.tgz",
- "integrity": "sha1-N6XPC4HsvGlD3hCbopYNGyZYSuc=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
- "shebang-command": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/shebang-command/download/shebang-command-1.2.0.tgz",
- "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
- "dev": true,
- "requires": {
- "shebang-regex": "^1.0.0"
- }
- },
- "shebang-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz",
- "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
- "dev": true
- },
- "shell-quote": {
- "version": "1.7.2",
- "resolved": "https://registry.npm.taobao.org/shell-quote/download/shell-quote-1.7.2.tgz?cache=0&sync_timestamp=1589682755902&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fshell-quote%2Fdownload%2Fshell-quote-1.7.2.tgz",
- "integrity": "sha1-Z6fQLHbJ2iT5nSCAj8re0ODgS+I=",
- "dev": true
- },
- "shelljs": {
- "version": "0.8.4",
- "resolved": "https://registry.npm.taobao.org/shelljs/download/shelljs-0.8.4.tgz?cache=0&sync_timestamp=1589684087110&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fshelljs%2Fdownload%2Fshelljs-0.8.4.tgz",
- "integrity": "sha1-3naE/ut2f4cWsyYHiooAh1iQ48I=",
- "dev": true,
- "requires": {
- "glob": "^7.0.0",
- "interpret": "^1.0.0",
- "rechoir": "^0.6.2"
- }
- },
- "signal-exit": {
- "version": "3.0.3",
- "resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz?cache=0&sync_timestamp=1589682814780&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsignal-exit%2Fdownload%2Fsignal-exit-3.0.3.tgz",
- "integrity": "sha1-oUEMLt2PB3sItOJTyOrPyvBXRhw=",
- "dev": true
- },
- "simple-swizzle": {
- "version": "0.2.2",
- "resolved": "https://registry.npm.taobao.org/simple-swizzle/download/simple-swizzle-0.2.2.tgz",
- "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
- "dev": true,
- "requires": {
- "is-arrayish": "^0.3.1"
- },
- "dependencies": {
- "is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://registry.npm.taobao.org/is-arrayish/download/is-arrayish-0.3.2.tgz",
- "integrity": "sha1-RXSirlb3qyBolvtDHq7tBm/fjwM=",
- "dev": true
- }
- }
- },
- "slash": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/slash/download/slash-2.0.0.tgz?cache=0&sync_timestamp=1589682715547&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fslash%2Fdownload%2Fslash-2.0.0.tgz",
- "integrity": "sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=",
- "dev": true
- },
- "slice-ansi": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/slice-ansi/download/slice-ansi-2.1.0.tgz",
- "integrity": "sha1-ys12k0YaY3pXiNkqfdT7oGjoFjY=",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.0",
- "astral-regex": "^1.0.0",
- "is-fullwidth-code-point": "^2.0.0"
- },
- "dependencies": {
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- }
- }
- },
- "snapdragon": {
- "version": "0.8.2",
- "resolved": "https://registry.npm.taobao.org/snapdragon/download/snapdragon-0.8.2.tgz",
- "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=",
- "dev": true,
- "requires": {
- "base": "^0.11.1",
- "debug": "^2.2.0",
- "define-property": "^0.2.5",
- "extend-shallow": "^2.0.1",
- "map-cache": "^0.2.2",
- "source-map": "^0.5.6",
- "source-map-resolve": "^0.5.0",
- "use": "^3.1.0"
- },
- "dependencies": {
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-2.6.9.tgz",
- "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "snapdragon-node": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/snapdragon-node/download/snapdragon-node-2.1.1.tgz",
- "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=",
- "dev": true,
- "requires": {
- "define-property": "^1.0.0",
- "isobject": "^3.0.0",
- "snapdragon-util": "^3.0.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.0"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-accessor-descriptor/download/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/is-data-descriptor/download/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=",
- "dev": true,
- "requires": {
- "kind-of": "^6.0.0"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/is-descriptor/download/is-descriptor-1.0.2.tgz",
- "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
- }
- }
- }
- },
- "snapdragon-util": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/snapdragon-util/download/snapdragon-util-3.0.1.tgz",
- "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=",
- "dev": true,
- "requires": {
- "kind-of": "^3.2.0"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "sockjs": {
- "version": "0.3.20",
- "resolved": "https://registry.npm.taobao.org/sockjs/download/sockjs-0.3.20.tgz",
- "integrity": "sha1-smooPsVi74smh7RAM6Tuzqx12FU=",
- "dev": true,
- "requires": {
- "faye-websocket": "^0.10.0",
- "uuid": "^3.4.0",
- "websocket-driver": "0.6.5"
- }
- },
- "sockjs-client": {
- "version": "1.4.0",
- "resolved": "https://registry.npm.taobao.org/sockjs-client/download/sockjs-client-1.4.0.tgz?cache=0&sync_timestamp=1596410219305&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsockjs-client%2Fdownload%2Fsockjs-client-1.4.0.tgz",
- "integrity": "sha1-yfJWjhnI/YFztJl+o0IOC7MGx9U=",
- "dev": true,
- "requires": {
- "debug": "^3.2.5",
- "eventsource": "^1.0.7",
- "faye-websocket": "~0.11.1",
- "inherits": "^2.0.3",
- "json3": "^3.3.2",
- "url-parse": "^1.4.3"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npm.taobao.org/debug/download/debug-3.2.6.tgz?cache=0&sync_timestamp=1589891993007&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdebug%2Fdownload%2Fdebug-3.2.6.tgz",
- "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "faye-websocket": {
- "version": "0.11.3",
- "resolved": "https://registry.npm.taobao.org/faye-websocket/download/faye-websocket-0.11.3.tgz",
- "integrity": "sha1-XA6aiWjokSwoZjn96XeosgnyUI4=",
- "dev": true,
- "requires": {
- "websocket-driver": ">=0.5.1"
- }
- }
- }
- },
- "sort-keys": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/sort-keys/download/sort-keys-1.1.2.tgz",
- "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
- "dev": true,
- "requires": {
- "is-plain-obj": "^1.0.0"
- }
- },
- "source-list-map": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/source-list-map/download/source-list-map-2.0.1.tgz",
- "integrity": "sha1-OZO9hzv8SEecyp6jpUeDXHwVSzQ=",
- "dev": true
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.5.7.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- },
- "source-map-resolve": {
- "version": "0.5.3",
- "resolved": "https://registry.npm.taobao.org/source-map-resolve/download/source-map-resolve-0.5.3.tgz",
- "integrity": "sha1-GQhmvs51U+H48mei7oLGBrVQmho=",
- "dev": true,
- "requires": {
- "atob": "^2.1.2",
- "decode-uri-component": "^0.2.0",
- "resolve-url": "^0.2.1",
- "source-map-url": "^0.4.0",
- "urix": "^0.1.0"
- }
- },
- "source-map-support": {
- "version": "0.5.19",
- "resolved": "https://registry.npm.taobao.org/source-map-support/download/source-map-support-0.5.19.tgz?cache=0&sync_timestamp=1589682814927&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map-support%2Fdownload%2Fsource-map-support-0.5.19.tgz",
- "integrity": "sha1-qYti+G3K9PZzmWSMCFKRq56P7WE=",
- "dev": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "source-map-url": {
- "version": "0.4.0",
- "resolved": "https://registry.npm.taobao.org/source-map-url/download/source-map-url-0.4.0.tgz",
- "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
- "dev": true
- },
- "sourcemap-codec": {
- "version": "1.4.8",
- "resolved": "https://registry.npm.taobao.org/sourcemap-codec/download/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=",
- "dev": true
- },
- "spdx-correct": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/spdx-correct/download/spdx-correct-3.1.1.tgz",
- "integrity": "sha1-3s6BrJweZxPl99G28X1Gj6U9iak=",
- "dev": true,
- "requires": {
- "spdx-expression-parse": "^3.0.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-exceptions": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/spdx-exceptions/download/spdx-exceptions-2.3.0.tgz",
- "integrity": "sha1-PyjOGnegA3JoPq3kpDMYNSeiFj0=",
- "dev": true
- },
- "spdx-expression-parse": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/spdx-expression-parse/download/spdx-expression-parse-3.0.1.tgz?cache=0&sync_timestamp=1589682794533&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fspdx-expression-parse%2Fdownload%2Fspdx-expression-parse-3.0.1.tgz",
- "integrity": "sha1-z3D1BILu/cmOPOCmgz5KU87rpnk=",
- "dev": true,
- "requires": {
- "spdx-exceptions": "^2.1.0",
- "spdx-license-ids": "^3.0.0"
- }
- },
- "spdx-license-ids": {
- "version": "3.0.5",
- "resolved": "https://registry.npm.taobao.org/spdx-license-ids/download/spdx-license-ids-3.0.5.tgz",
- "integrity": "sha1-NpS1gEVnpFjTyARYQqY1hjL2JlQ=",
- "dev": true
- },
- "spdy": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/spdy/download/spdy-4.0.2.tgz",
- "integrity": "sha1-t09GYgOj7aRSwCSSuR+56EonZ3s=",
- "dev": true,
- "requires": {
- "debug": "^4.1.0",
- "handle-thing": "^2.0.0",
- "http-deceiver": "^1.2.7",
- "select-hose": "^2.0.0",
- "spdy-transport": "^3.0.0"
- }
- },
- "spdy-transport": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/spdy-transport/download/spdy-transport-3.0.0.tgz",
- "integrity": "sha1-ANSGOmQArXXfkzYaFghgXl3NzzE=",
- "dev": true,
- "requires": {
- "debug": "^4.1.0",
- "detect-node": "^2.0.4",
- "hpack.js": "^2.1.6",
- "obuf": "^1.1.2",
- "readable-stream": "^3.0.6",
- "wbuf": "^1.7.3"
- },
- "dependencies": {
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.6.0.tgz?cache=0&sync_timestamp=1589682741447&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-3.6.0.tgz",
- "integrity": "sha1-M3u9o63AcGvT4CRCaihtS0sskZg=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- }
- }
- },
- "split-string": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/split-string/download/split-string-3.1.0.tgz",
- "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=",
- "dev": true,
- "requires": {
- "extend-shallow": "^3.0.0"
- }
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
- "dev": true
- },
- "sshpk": {
- "version": "1.16.1",
- "resolved": "https://registry.npm.taobao.org/sshpk/download/sshpk-1.16.1.tgz",
- "integrity": "sha1-+2YcC+8ps520B2nuOfpwCT1vaHc=",
- "dev": true,
- "requires": {
- "asn1": "~0.2.3",
- "assert-plus": "^1.0.0",
- "bcrypt-pbkdf": "^1.0.0",
- "dashdash": "^1.12.0",
- "ecc-jsbn": "~0.1.1",
- "getpass": "^0.1.1",
- "jsbn": "~0.1.0",
- "safer-buffer": "^2.0.2",
- "tweetnacl": "~0.14.0"
- }
- },
- "ssri": {
- "version": "6.0.1",
- "resolved": "https://registry.npm.taobao.org/ssri/download/ssri-6.0.1.tgz",
- "integrity": "sha1-KjxBso3UW2K2Nnbst0ABJlrp7dg=",
- "dev": true,
- "requires": {
- "figgy-pudding": "^3.5.1"
- }
- },
- "stable": {
- "version": "0.1.8",
- "resolved": "https://registry.npm.taobao.org/stable/download/stable-0.1.8.tgz",
- "integrity": "sha1-g26zyDgv4pNv6vVEYxAXzn1Ho88=",
- "dev": true
- },
- "stackframe": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/stackframe/download/stackframe-1.2.0.tgz",
- "integrity": "sha1-UkKUktY8YuuYmATBFVLj0i53kwM=",
- "dev": true
- },
- "static-extend": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/static-extend/download/static-extend-0.1.2.tgz",
- "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
- "dev": true,
- "requires": {
- "define-property": "^0.2.5",
- "object-copy": "^0.1.0"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npm.taobao.org/define-property/download/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "^0.1.0"
- }
- }
- }
- },
- "statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz?cache=0&sync_timestamp=1589682739548&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstatuses%2Fdownload%2Fstatuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "dev": true
- },
- "stream-browserify": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/stream-browserify/download/stream-browserify-2.0.2.tgz",
- "integrity": "sha1-h1IdOKRKp+6RzhzSpH3wy0ndZgs=",
- "dev": true,
- "requires": {
- "inherits": "~2.0.1",
- "readable-stream": "^2.0.2"
- }
- },
- "stream-each": {
- "version": "1.2.3",
- "resolved": "https://registry.npm.taobao.org/stream-each/download/stream-each-1.2.3.tgz",
- "integrity": "sha1-6+J6DDibBPvMIzZClS4Qcxr6m64=",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "stream-shift": "^1.0.0"
- }
- },
- "stream-http": {
- "version": "2.8.3",
- "resolved": "https://registry.npm.taobao.org/stream-http/download/stream-http-2.8.3.tgz",
- "integrity": "sha1-stJCRpKIpaJ+xP6JM6z2I95lFPw=",
- "dev": true,
- "requires": {
- "builtin-status-codes": "^3.0.0",
- "inherits": "^2.0.1",
- "readable-stream": "^2.3.6",
- "to-arraybuffer": "^1.0.0",
- "xtend": "^4.0.0"
- }
- },
- "stream-shift": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/stream-shift/download/stream-shift-1.0.1.tgz",
- "integrity": "sha1-1wiCgVWasneEJCebCHfaPDktWj0=",
- "dev": true
- },
- "strict-uri-encode": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/strict-uri-encode/download/strict-uri-encode-1.1.0.tgz",
- "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
- "dev": true
- },
- "string-hash": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/string-hash/download/string-hash-1.1.3.tgz",
- "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
- "dev": true
- },
- "string-width": {
- "version": "4.2.0",
- "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-4.2.0.tgz",
- "integrity": "sha1-lSGCxGzHssMT0VluYjmSvRY7crU=",
- "dev": true,
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.0"
- }
- },
- "string.prototype.trimend": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/string.prototype.trimend/download/string.prototype.trimend-1.0.1.tgz",
- "integrity": "sha1-hYEqa4R6wAInD1gIFGBkyZX7aRM=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.5"
- }
- },
- "string.prototype.trimstart": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/string.prototype.trimstart/download/string.prototype.trimstart-1.0.1.tgz",
- "integrity": "sha1-FK9tnzSwU/fPyJty+PLuFLkDmlQ=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.5"
- }
- },
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz",
- "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
- "dev": true,
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- },
- "strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-6.0.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-6.0.0.tgz",
- "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-5.0.0.tgz",
- "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=",
- "dev": true
- }
- }
- },
- "strip-eof": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/strip-eof/download/strip-eof-1.0.0.tgz",
- "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
- "dev": true
- },
- "strip-final-newline": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/strip-final-newline/download/strip-final-newline-2.0.0.tgz",
- "integrity": "sha1-ibhS+y/L6Tb29LMYevsKEsGrWK0=",
- "dev": true
- },
- "strip-indent": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/strip-indent/download/strip-indent-2.0.0.tgz",
- "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=",
- "dev": true
- },
- "strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/strip-json-comments/download/strip-json-comments-3.1.1.tgz?cache=0&sync_timestamp=1594567532500&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-json-comments%2Fdownload%2Fstrip-json-comments-3.1.1.tgz",
- "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=",
- "dev": true
- },
- "stylehacks": {
- "version": "4.0.3",
- "resolved": "https://registry.npm.taobao.org/stylehacks/download/stylehacks-4.0.3.tgz",
- "integrity": "sha1-Zxj8r00eB9ihMYaQiB6NlnJqcdU=",
- "dev": true,
- "requires": {
- "browserslist": "^4.0.0",
- "postcss": "^7.0.0",
- "postcss-selector-parser": "^3.0.0"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.2",
- "resolved": "https://registry.npm.taobao.org/postcss-selector-parser/download/postcss-selector-parser-3.1.2.tgz",
- "integrity": "sha1-sxD1xMD9r3b5SQK7qjDbaqhPUnA=",
- "dev": true,
- "requires": {
- "dot-prop": "^5.2.0",
- "indexes-of": "^1.0.1",
- "uniq": "^1.0.1"
- }
- }
- }
- },
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-5.5.0.tgz",
- "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- },
- "svg-tags": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/svg-tags/download/svg-tags-1.0.0.tgz",
- "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
- "dev": true
- },
- "svgo": {
- "version": "1.3.2",
- "resolved": "https://registry.npm.taobao.org/svgo/download/svgo-1.3.2.tgz",
- "integrity": "sha1-ttxRHAYzRsnkFbgeQ0ARRbltQWc=",
- "dev": true,
- "requires": {
- "chalk": "^2.4.1",
- "coa": "^2.0.2",
- "css-select": "^2.0.0",
- "css-select-base-adapter": "^0.1.1",
- "css-tree": "1.0.0-alpha.37",
- "csso": "^4.0.2",
- "js-yaml": "^3.13.1",
- "mkdirp": "~0.5.1",
- "object.values": "^1.1.0",
- "sax": "~1.2.4",
- "stable": "^0.1.8",
- "unquote": "~1.1.1",
- "util.promisify": "~1.0.0"
- }
- },
- "table": {
- "version": "5.4.6",
- "resolved": "https://registry.npm.taobao.org/table/download/table-5.4.6.tgz",
- "integrity": "sha1-EpLRlQDOP4YFOwXw6Ofko7shB54=",
- "dev": true,
- "requires": {
- "ajv": "^6.10.2",
- "lodash": "^4.17.14",
- "slice-ansi": "^2.1.0",
- "string-width": "^3.0.0"
- },
- "dependencies": {
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-7.0.3.tgz",
- "integrity": "sha1-kzoEBShgyF6DwSJHnEdIqOTHIVY=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-3.1.0.tgz",
- "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "tapable": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/tapable/download/tapable-1.1.3.tgz",
- "integrity": "sha1-ofzMBrWNth/XpF2i2kT186Pme6I=",
- "dev": true
- },
- "terser": {
- "version": "4.8.0",
- "resolved": "https://registry.npm.taobao.org/terser/download/terser-4.8.0.tgz?cache=0&sync_timestamp=1597761636364&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser%2Fdownload%2Fterser-4.8.0.tgz",
- "integrity": "sha1-YwVjQ9fHC7KfOvZlhlpG/gOg3xc=",
- "dev": true,
- "requires": {
- "commander": "^2.20.0",
- "source-map": "~0.6.1",
- "source-map-support": "~0.5.12"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "terser-webpack-plugin": {
- "version": "1.4.5",
- "resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-1.4.5.tgz?cache=0&sync_timestamp=1597229593156&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-1.4.5.tgz",
- "integrity": "sha1-oheu+uozDnNP+sthIOwfoxLWBAs=",
- "dev": true,
- "requires": {
- "cacache": "^12.0.2",
- "find-cache-dir": "^2.1.0",
- "is-wsl": "^1.1.0",
- "schema-utils": "^1.0.0",
- "serialize-javascript": "^4.0.0",
- "source-map": "^0.6.1",
- "terser": "^4.1.2",
- "webpack-sources": "^1.4.0",
- "worker-farm": "^1.7.0"
- },
- "dependencies": {
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npm.taobao.org/text-table/download/text-table-0.2.0.tgz",
- "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
- "dev": true
- },
- "thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npm.taobao.org/thenify/download/thenify-3.3.1.tgz",
- "integrity": "sha1-iTLmhqQGYDigFt2eLKRq3Zg4qV8=",
- "dev": true,
- "requires": {
- "any-promise": "^1.0.0"
- }
- },
- "thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npm.taobao.org/thenify-all/download/thenify-all-1.6.0.tgz",
- "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
- "dev": true,
- "requires": {
- "thenify": ">= 3.1.0 < 4"
- }
- },
- "thread-loader": {
- "version": "2.1.3",
- "resolved": "https://registry.npm.taobao.org/thread-loader/download/thread-loader-2.1.3.tgz",
- "integrity": "sha1-y9LBOfwrLebp0o9iKGq3cMGsvdo=",
- "dev": true,
- "requires": {
- "loader-runner": "^2.3.1",
- "loader-utils": "^1.1.0",
- "neo-async": "^2.6.0"
- }
- },
- "through": {
- "version": "2.3.8",
- "resolved": "https://registry.npm.taobao.org/through/download/through-2.3.8.tgz",
- "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
- "dev": true
- },
- "through2": {
- "version": "2.0.5",
- "resolved": "https://registry.npm.taobao.org/through2/download/through2-2.0.5.tgz",
- "integrity": "sha1-AcHjnrMdB8t9A6lqcIIyYLIxMs0=",
- "dev": true,
- "requires": {
- "readable-stream": "~2.3.6",
- "xtend": "~4.0.1"
- }
- },
- "thunky": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/thunky/download/thunky-1.1.0.tgz",
- "integrity": "sha1-Wrr3FKlAXbBQRzK7zNLO3Z75U30=",
- "dev": true
- },
- "timers-browserify": {
- "version": "2.0.11",
- "resolved": "https://registry.npm.taobao.org/timers-browserify/download/timers-browserify-2.0.11.tgz",
- "integrity": "sha1-gAsfPu4nLlvFPuRloE0OgEwxIR8=",
- "dev": true,
- "requires": {
- "setimmediate": "^1.0.4"
- }
- },
- "timsort": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/timsort/download/timsort-0.3.0.tgz",
- "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
- "dev": true
- },
- "tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz?cache=0&sync_timestamp=1589684134816&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftmp%2Fdownload%2Ftmp-0.0.33.tgz",
- "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=",
- "dev": true,
- "requires": {
- "os-tmpdir": "~1.0.2"
- }
- },
- "to-arraybuffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/to-arraybuffer/download/to-arraybuffer-1.0.1.tgz",
- "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
- "dev": true
- },
- "to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-2.0.0.tgz",
- "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
- },
- "to-object-path": {
- "version": "0.3.0",
- "resolved": "https://registry.npm.taobao.org/to-object-path/download/to-object-path-0.3.0.tgz",
- "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
- "dev": true,
- "requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
- "to-regex": {
- "version": "3.0.2",
- "resolved": "https://registry.npm.taobao.org/to-regex/download/to-regex-3.0.2.tgz",
- "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=",
- "dev": true,
- "requires": {
- "define-property": "^2.0.2",
- "extend-shallow": "^3.0.2",
- "regex-not": "^1.0.2",
- "safe-regex": "^1.1.0"
- }
- },
- "to-regex-range": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-2.1.1.tgz",
- "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
- "dev": true,
- "requires": {
- "is-number": "^3.0.0",
- "repeat-string": "^1.6.1"
- }
- },
- "toidentifier": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz",
- "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM=",
- "dev": true
- },
- "toposort": {
- "version": "1.0.7",
- "resolved": "https://registry.npm.taobao.org/toposort/download/toposort-1.0.7.tgz",
- "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
- "dev": true
- },
- "tough-cookie": {
- "version": "2.5.0",
- "resolved": "https://registry.npm.taobao.org/tough-cookie/download/tough-cookie-2.5.0.tgz?cache=0&sync_timestamp=1589682815640&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftough-cookie%2Fdownload%2Ftough-cookie-2.5.0.tgz",
- "integrity": "sha1-zZ+yoKodWhK0c72fuW+j3P9lreI=",
- "dev": true,
- "requires": {
- "psl": "^1.1.28",
- "punycode": "^2.1.1"
- }
- },
- "tryer": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/tryer/download/tryer-1.0.1.tgz",
- "integrity": "sha1-8shUBoALmw90yfdGW4HqrSQSUvg=",
- "dev": true
- },
- "ts-pnp": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/ts-pnp/download/ts-pnp-1.2.0.tgz",
- "integrity": "sha1-pQCtCEsHmPHDBxrzkeZZEshrypI=",
- "dev": true
- },
- "tslib": {
- "version": "1.13.0",
- "resolved": "https://registry.npm.taobao.org/tslib/download/tslib-1.13.0.tgz?cache=0&sync_timestamp=1596753875166&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-1.13.0.tgz",
- "integrity": "sha1-yIHhPMcBWJTtkUhi0nZDb6mkcEM=",
- "dev": true
- },
- "tty-browserify": {
- "version": "0.0.0",
- "resolved": "https://registry.npm.taobao.org/tty-browserify/download/tty-browserify-0.0.0.tgz",
- "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
- "dev": true
- },
- "tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.6.0.tgz",
- "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
- "requires": {
- "safe-buffer": "^5.0.1"
- }
- },
- "tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npm.taobao.org/tweetnacl/download/tweetnacl-0.14.5.tgz?cache=0&sync_timestamp=1589682745749&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftweetnacl%2Fdownload%2Ftweetnacl-0.14.5.tgz",
- "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true
- },
- "type-check": {
- "version": "0.3.2",
- "resolved": "https://registry.npm.taobao.org/type-check/download/type-check-0.3.2.tgz",
- "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
- "dev": true,
- "requires": {
- "prelude-ls": "~1.1.2"
- }
- },
- "type-fest": {
- "version": "0.6.0",
- "resolved": "https://registry.npm.taobao.org/type-fest/download/type-fest-0.6.0.tgz",
- "integrity": "sha1-jSojcNPfiG61yQraHFv2GIrPg4s=",
- "dev": true
- },
- "type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz",
- "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=",
- "dev": true,
- "requires": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- }
- },
- "typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npm.taobao.org/typedarray/download/typedarray-0.0.6.tgz",
- "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
- "dev": true
- },
- "uglify-js": {
- "version": "3.4.10",
- "resolved": "https://registry.npm.taobao.org/uglify-js/download/uglify-js-3.4.10.tgz?cache=0&sync_timestamp=1596387201241&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fuglify-js%2Fdownload%2Fuglify-js-3.4.10.tgz",
- "integrity": "sha1-mtlWPY6zrN+404WX0q8dgV9qdV8=",
- "dev": true,
- "requires": {
- "commander": "~2.19.0",
- "source-map": "~0.6.1"
- },
- "dependencies": {
- "commander": {
- "version": "2.19.0",
- "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.19.0.tgz",
- "integrity": "sha1-9hmKqE5bg8RgVLlN3tv+1e6f8So=",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "unicode-canonical-property-names-ecmascript": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/unicode-canonical-property-names-ecmascript/download/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
- "integrity": "sha1-JhmADEyCWADv3YNDr33Zkzy+KBg=",
- "dev": true
- },
- "unicode-match-property-ecmascript": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/unicode-match-property-ecmascript/download/unicode-match-property-ecmascript-1.0.4.tgz",
- "integrity": "sha1-jtKjJWmWG86SJ9Cc0/+7j+1fAgw=",
- "dev": true,
- "requires": {
- "unicode-canonical-property-names-ecmascript": "^1.0.4",
- "unicode-property-aliases-ecmascript": "^1.0.4"
- }
- },
- "unicode-match-property-value-ecmascript": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/unicode-match-property-value-ecmascript/download/unicode-match-property-value-ecmascript-1.2.0.tgz",
- "integrity": "sha1-DZH2AO7rMJaqlisdb8iIduZOpTE=",
- "dev": true
- },
- "unicode-property-aliases-ecmascript": {
- "version": "1.1.0",
- "resolved": "https://registry.npm.taobao.org/unicode-property-aliases-ecmascript/download/unicode-property-aliases-ecmascript-1.1.0.tgz",
- "integrity": "sha1-3Vepn2IHvt/0Yoq++5TFDblByPQ=",
- "dev": true
- },
- "union-value": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/union-value/download/union-value-1.0.1.tgz",
- "integrity": "sha1-C2/nuDWuzaYcbqTU8CwUIh4QmEc=",
- "dev": true,
- "requires": {
- "arr-union": "^3.1.0",
- "get-value": "^2.0.6",
- "is-extendable": "^0.1.1",
- "set-value": "^2.0.1"
- }
- },
- "uniq": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/uniq/download/uniq-1.0.1.tgz",
- "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
- "dev": true
- },
- "uniqs": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/uniqs/download/uniqs-2.0.0.tgz",
- "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
- "dev": true
- },
- "unique-filename": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/unique-filename/download/unique-filename-1.1.1.tgz",
- "integrity": "sha1-HWl2k2mtoFgxA6HmrodoG1ZXMjA=",
- "dev": true,
- "requires": {
- "unique-slug": "^2.0.0"
- }
- },
- "unique-slug": {
- "version": "2.0.2",
- "resolved": "https://registry.npm.taobao.org/unique-slug/download/unique-slug-2.0.2.tgz",
- "integrity": "sha1-uqvOkQg/xk6UWw861hPiZPfNTmw=",
- "dev": true,
- "requires": {
- "imurmurhash": "^0.1.4"
- }
- },
- "universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npm.taobao.org/universalify/download/universalify-0.1.2.tgz",
- "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=",
- "dev": true
- },
- "unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz?cache=0&sync_timestamp=1589682745059&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Funpipe%2Fdownload%2Funpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
- "dev": true
- },
- "unquote": {
- "version": "1.1.1",
- "resolved": "https://registry.npm.taobao.org/unquote/download/unquote-1.1.1.tgz",
- "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
- "dev": true
- },
- "unset-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/unset-value/download/unset-value-1.0.0.tgz",
- "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
- "dev": true,
- "requires": {
- "has-value": "^0.3.1",
- "isobject": "^3.0.0"
- },
- "dependencies": {
- "has-value": {
- "version": "0.3.1",
- "resolved": "https://registry.npm.taobao.org/has-value/download/has-value-0.3.1.tgz",
- "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
- "dev": true,
- "requires": {
- "get-value": "^2.0.3",
- "has-values": "^0.1.4",
- "isobject": "^2.0.0"
- },
- "dependencies": {
- "isobject": {
- "version": "2.1.0",
- "resolved": "https://registry.npm.taobao.org/isobject/download/isobject-2.1.0.tgz",
- "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
- "dev": true,
- "requires": {
- "isarray": "1.0.0"
- }
- }
- }
- },
- "has-values": {
- "version": "0.1.4",
- "resolved": "https://registry.npm.taobao.org/has-values/download/has-values-0.1.4.tgz",
- "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
- "dev": true
- }
- }
- },
- "upath": {
- "version": "1.2.0",
- "resolved": "https://registry.npm.taobao.org/upath/download/upath-1.2.0.tgz",
- "integrity": "sha1-j2bbzVWog6za5ECK+LA1pQRMGJQ=",
- "dev": true
- },
- "upper-case": {
- "version": "1.1.3",
- "resolved": "https://registry.npm.taobao.org/upper-case/download/upper-case-1.1.3.tgz",
- "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
- "dev": true
- },
- "uri-js": {
- "version": "4.2.2",
- "resolved": "https://registry.npm.taobao.org/uri-js/download/uri-js-4.2.2.tgz",
- "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=",
- "dev": true,
- "requires": {
- "punycode": "^2.1.0"
- }
- },
- "urix": {
- "version": "0.1.0",
- "resolved": "https://registry.npm.taobao.org/urix/download/urix-0.1.0.tgz",
- "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
- "dev": true
- },
- "url": {
- "version": "0.11.0",
- "resolved": "https://registry.npm.taobao.org/url/download/url-0.11.0.tgz",
- "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
- "dev": true,
- "requires": {
- "punycode": "1.3.2",
- "querystring": "0.2.0"
- },
- "dependencies": {
- "punycode": {
- "version": "1.3.2",
- "resolved": "https://registry.npm.taobao.org/punycode/download/punycode-1.3.2.tgz?cache=0&sync_timestamp=1589682803838&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpunycode%2Fdownload%2Fpunycode-1.3.2.tgz",
- "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
- "dev": true
- }
- }
- },
- "url-loader": {
- "version": "2.3.0",
- "resolved": "https://registry.npm.taobao.org/url-loader/download/url-loader-2.3.0.tgz",
- "integrity": "sha1-4OLvZY8APvuMpBsPP/v3a6uIZYs=",
- "dev": true,
- "requires": {
- "loader-utils": "^1.2.3",
- "mime": "^2.4.4",
- "schema-utils": "^2.5.0"
- }
- },
- "url-parse": {
- "version": "1.4.7",
- "resolved": "https://registry.npm.taobao.org/url-parse/download/url-parse-1.4.7.tgz",
- "integrity": "sha1-qKg1NejACjFuQDpdtKwbm4U64ng=",
- "dev": true,
- "requires": {
- "querystringify": "^2.1.1",
- "requires-port": "^1.0.0"
- }
- },
- "use": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/use/download/use-3.1.1.tgz",
- "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=",
- "dev": true
- },
- "util": {
- "version": "0.11.1",
- "resolved": "https://registry.npm.taobao.org/util/download/util-0.11.1.tgz",
- "integrity": "sha1-MjZzNyDsZLsn9uJvQhqqLhtYjWE=",
- "dev": true,
- "requires": {
- "inherits": "2.0.3"
- },
- "dependencies": {
- "inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
- }
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
- },
- "util.promisify": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/util.promisify/download/util.promisify-1.0.1.tgz?cache=0&sync_timestamp=1589682767473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Futil.promisify%2Fdownload%2Futil.promisify-1.0.1.tgz",
- "integrity": "sha1-a693dLgO6w91INi4HQeYKlmruu4=",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.2",
- "has-symbols": "^1.0.1",
- "object.getownpropertydescriptors": "^2.1.0"
- }
- },
- "utila": {
- "version": "0.4.0",
- "resolved": "https://registry.npm.taobao.org/utila/download/utila-0.4.0.tgz",
- "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=",
- "dev": true
- },
- "utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/utils-merge/download/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
- "dev": true
- },
- "uuid": {
- "version": "3.4.0",
- "resolved": "https://registry.npm.taobao.org/uuid/download/uuid-3.4.0.tgz?cache=0&sync_timestamp=1595885088251&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fuuid%2Fdownload%2Fuuid-3.4.0.tgz",
- "integrity": "sha1-sj5DWK+oogL+ehAK8fX4g/AgB+4=",
- "dev": true
- },
- "v8-compile-cache": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/v8-compile-cache/download/v8-compile-cache-2.1.1.tgz",
- "integrity": "sha1-VLw83UMxe8qR413K8wWxpyN950U=",
- "dev": true
- },
- "validate-npm-package-license": {
- "version": "3.0.4",
- "resolved": "https://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.4.tgz",
- "integrity": "sha1-/JH2uce6FchX9MssXe/uw51PQQo=",
- "dev": true,
- "requires": {
- "spdx-correct": "^3.0.0",
- "spdx-expression-parse": "^3.0.0"
- }
- },
- "vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz",
- "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "dev": true
- },
- "vendors": {
- "version": "1.0.4",
- "resolved": "https://registry.npm.taobao.org/vendors/download/vendors-1.0.4.tgz",
- "integrity": "sha1-4rgApT56Kbk1BsPPQRANFsTErY4=",
- "dev": true
- },
- "verror": {
- "version": "1.10.0",
- "resolved": "https://registry.npm.taobao.org/verror/download/verror-1.10.0.tgz",
- "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
- "dev": true,
- "requires": {
- "assert-plus": "^1.0.0",
- "core-util-is": "1.0.2",
- "extsprintf": "^1.2.0"
- }
- },
- "vm-browserify": {
- "version": "1.1.2",
- "resolved": "https://registry.npm.taobao.org/vm-browserify/download/vm-browserify-1.1.2.tgz?cache=0&sync_timestamp=1589682787766&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvm-browserify%2Fdownload%2Fvm-browserify-1.1.2.tgz",
- "integrity": "sha1-eGQcSIuObKkadfUR56OzKobl3aA=",
- "dev": true
- },
- "vue": {
- "version": "3.0.0-rc.5",
- "resolved": "https://registry.npm.taobao.org/vue/download/vue-3.0.0-rc.5.tgz?cache=0&sync_timestamp=1595983987853&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue%2Fdownload%2Fvue-3.0.0-rc.5.tgz",
- "integrity": "sha1-lzF11FqJKzvSPvXef6pK3ZxmJ18=",
- "requires": {
- "@vue/compiler-dom": "3.0.0-rc.5",
- "@vue/runtime-dom": "3.0.0-rc.5",
- "@vue/shared": "3.0.0-rc.5"
- }
- },
- "vue-cli-plugin-vuetify": {
- "version": "2.0.7",
- "resolved": "https://registry.npm.taobao.org/vue-cli-plugin-vuetify/download/vue-cli-plugin-vuetify-2.0.7.tgz",
- "integrity": "sha1-/LTxZV58kZnuQNy/ZGXiNV/QdNU=",
- "dev": true,
- "requires": {
- "null-loader": "^3.0.0",
- "semver": "^7.1.2",
- "shelljs": "^0.8.3"
- },
- "dependencies": {
- "semver": {
- "version": "7.3.2",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-7.3.2.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-7.3.2.tgz",
- "integrity": "sha1-YElisFK4HtB4aq6EOJ/7pw/9OTg=",
- "dev": true
- }
- }
- },
- "vue-eslint-parser": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/vue-eslint-parser/download/vue-eslint-parser-7.1.0.tgz?cache=0&sync_timestamp=1589684321779&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-eslint-parser%2Fdownload%2Fvue-eslint-parser-7.1.0.tgz",
- "integrity": "sha1-nNvMgj5lawh1B6GRFzK4Z6wQHoM=",
- "dev": true,
- "requires": {
- "debug": "^4.1.1",
- "eslint-scope": "^5.0.0",
- "eslint-visitor-keys": "^1.1.0",
- "espree": "^6.2.1",
- "esquery": "^1.0.1",
- "lodash": "^4.17.15"
- },
- "dependencies": {
- "eslint-scope": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/eslint-scope/download/eslint-scope-5.1.0.tgz?cache=0&sync_timestamp=1591269986906&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Feslint-scope%2Fdownload%2Feslint-scope-5.1.0.tgz",
- "integrity": "sha1-0Plx3+WcaeDK2mhLI9Sdv4JgDOU=",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- }
- }
- },
- "vue-hot-reload-api": {
- "version": "2.3.4",
- "resolved": "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.4.tgz?cache=0&sync_timestamp=1589682714858&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-hot-reload-api%2Fdownload%2Fvue-hot-reload-api-2.3.4.tgz",
- "integrity": "sha1-UylVzB6yCKPZkLOp+acFdGV+CPI=",
- "dev": true
- },
- "vue-loader": {
- "version": "15.9.3",
- "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-15.9.3.tgz",
- "integrity": "sha1-DeNdnlVdPtU5aVFsrFziVTEpndo=",
- "dev": true,
- "requires": {
- "@vue/component-compiler-utils": "^3.1.0",
- "hash-sum": "^1.0.2",
- "loader-utils": "^1.1.0",
- "vue-hot-reload-api": "^2.3.0",
- "vue-style-loader": "^4.1.0"
- },
- "dependencies": {
- "hash-sum": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/hash-sum/download/hash-sum-1.0.2.tgz",
- "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
- "dev": true
- }
- }
- },
- "vue-loader-v16": {
- "version": "npm:vue-loader@16.0.0-beta.5",
- "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.0.0-beta.5.tgz",
- "integrity": "sha1-BO3IiUkrA6RF56xm6SJqcBdcqKA=",
- "dev": true,
- "optional": true,
- "requires": {
- "@types/mini-css-extract-plugin": "^0.9.1",
- "chalk": "^3.0.0",
- "hash-sum": "^2.0.0",
- "loader-utils": "^1.2.3",
- "merge-source-map": "^1.1.0",
- "source-map": "^0.6.1"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.2.1.tgz",
- "integrity": "sha1-kK51xCTQCNJiTFvynq0xd+v881k=",
- "dev": true,
- "optional": true,
- "requires": {
- "@types/color-name": "^1.1.1",
- "color-convert": "^2.0.1"
- }
- },
- "chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-3.0.0.tgz",
- "integrity": "sha1-P3PCv1JlkfV0zEksUeJFY0n4ROQ=",
- "dev": true,
- "optional": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
- "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
- "dev": true,
- "optional": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
- "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
- "dev": true,
- "optional": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
- "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
- "dev": true,
- "optional": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true,
- "optional": true
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.1.0.tgz",
- "integrity": "sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=",
- "dev": true,
- "optional": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
- "vue-style-loader": {
- "version": "4.1.2",
- "resolved": "https://registry.npm.taobao.org/vue-style-loader/download/vue-style-loader-4.1.2.tgz",
- "integrity": "sha1-3t80mAbyXOtOZPOtfApE+6c1/Pg=",
- "dev": true,
- "requires": {
- "hash-sum": "^1.0.2",
- "loader-utils": "^1.0.2"
- },
- "dependencies": {
- "hash-sum": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/hash-sum/download/hash-sum-1.0.2.tgz",
- "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
- "dev": true
- }
- }
- },
- "vue-template-es2015-compiler": {
- "version": "1.9.1",
- "resolved": "https://registry.npm.taobao.org/vue-template-es2015-compiler/download/vue-template-es2015-compiler-1.9.1.tgz",
- "integrity": "sha1-HuO8mhbsv1EYvjNLsV+cRvgvWCU=",
- "dev": true
- },
- "watchpack": {
- "version": "1.7.4",
- "resolved": "https://registry.npm.taobao.org/watchpack/download/watchpack-1.7.4.tgz?cache=0&sync_timestamp=1597081659128&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwatchpack%2Fdownload%2Fwatchpack-1.7.4.tgz",
- "integrity": "sha1-bp2lOzyAuy1lCBiPWyAEEIZs0ws=",
- "dev": true,
- "requires": {
- "chokidar": "^3.4.1",
- "graceful-fs": "^4.1.2",
- "neo-async": "^2.5.0",
- "watchpack-chokidar2": "^2.0.0"
- }
- },
- "watchpack-chokidar2": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/watchpack-chokidar2/download/watchpack-chokidar2-2.0.0.tgz",
- "integrity": "sha1-mUihhmy71suCTeoTp+1pH2yN3/A=",
- "dev": true,
- "optional": true,
- "requires": {
- "chokidar": "^2.1.8"
- },
- "dependencies": {
- "anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-2.0.0.tgz",
- "integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=",
- "dev": true,
- "optional": true,
- "requires": {
- "micromatch": "^3.1.4",
- "normalize-path": "^2.1.1"
- },
- "dependencies": {
- "normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-2.1.1.tgz",
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
- "dev": true,
- "optional": true,
- "requires": {
- "remove-trailing-separator": "^1.0.1"
- }
- }
- }
- },
- "binary-extensions": {
- "version": "1.13.1",
- "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-1.13.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-1.13.1.tgz",
- "integrity": "sha1-WYr+VHVbKGilMw0q/51Ou1Mgm2U=",
- "dev": true,
- "optional": true
- },
- "chokidar": {
- "version": "2.1.8",
- "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-2.1.8.tgz",
- "integrity": "sha1-gEs6e2qZNYw8XGHnHYco8EHP+Rc=",
- "dev": true,
- "optional": true,
- "requires": {
- "anymatch": "^2.0.0",
- "async-each": "^1.0.1",
- "braces": "^2.3.2",
- "fsevents": "^1.2.7",
- "glob-parent": "^3.1.0",
- "inherits": "^2.0.3",
- "is-binary-path": "^1.0.0",
- "is-glob": "^4.0.0",
- "normalize-path": "^3.0.0",
- "path-is-absolute": "^1.0.0",
- "readdirp": "^2.2.1",
- "upath": "^1.1.1"
- }
- },
- "fsevents": {
- "version": "1.2.13",
- "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-1.2.13.tgz",
- "integrity": "sha1-8yXLBFVZJCi88Rs4M3DvcOO/zDg=",
- "dev": true,
- "optional": true,
- "requires": {
- "bindings": "^1.5.0",
- "nan": "^2.12.1"
- }
- },
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "optional": true,
- "requires": {
- "is-glob": "^3.1.0",
- "path-dirname": "^1.0.0"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "optional": true,
- "requires": {
- "is-extglob": "^2.1.0"
- }
- }
- }
- },
- "is-binary-path": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-1.0.1.tgz",
- "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
- "dev": true,
- "optional": true,
- "requires": {
- "binary-extensions": "^1.0.0"
- }
- },
- "readdirp": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-2.2.1.tgz",
- "integrity": "sha1-DodiKjMlqjPokihcr4tOhGUppSU=",
- "dev": true,
- "optional": true,
- "requires": {
- "graceful-fs": "^4.1.11",
- "micromatch": "^3.1.10",
- "readable-stream": "^2.0.2"
- }
- }
- }
- },
- "wbuf": {
- "version": "1.7.3",
- "resolved": "https://registry.npm.taobao.org/wbuf/download/wbuf-1.7.3.tgz",
- "integrity": "sha1-wdjRSTFtPqhShIiVy2oL/oh7h98=",
- "dev": true,
- "requires": {
- "minimalistic-assert": "^1.0.0"
- }
- },
- "wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/wcwidth/download/wcwidth-1.0.1.tgz",
- "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
- "dev": true,
- "requires": {
- "defaults": "^1.0.3"
- }
- },
- "webpack": {
- "version": "4.44.1",
- "resolved": "https://registry.npm.taobao.org/webpack/download/webpack-4.44.1.tgz?cache=0&sync_timestamp=1597430610874&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwebpack%2Fdownload%2Fwebpack-4.44.1.tgz",
- "integrity": "sha1-F+af/58yG48RfR/acU7fwLk5zCE=",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.9.0",
- "@webassemblyjs/helper-module-context": "1.9.0",
- "@webassemblyjs/wasm-edit": "1.9.0",
- "@webassemblyjs/wasm-parser": "1.9.0",
- "acorn": "^6.4.1",
- "ajv": "^6.10.2",
- "ajv-keywords": "^3.4.1",
- "chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^4.3.0",
- "eslint-scope": "^4.0.3",
- "json-parse-better-errors": "^1.0.2",
- "loader-runner": "^2.4.0",
- "loader-utils": "^1.2.3",
- "memory-fs": "^0.4.1",
- "micromatch": "^3.1.10",
- "mkdirp": "^0.5.3",
- "neo-async": "^2.6.1",
- "node-libs-browser": "^2.2.1",
- "schema-utils": "^1.0.0",
- "tapable": "^1.1.3",
- "terser-webpack-plugin": "^1.4.3",
- "watchpack": "^1.7.4",
- "webpack-sources": "^1.4.1"
- },
- "dependencies": {
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- }
- }
- },
- "webpack-bundle-analyzer": {
- "version": "3.8.0",
- "resolved": "https://registry.npm.taobao.org/webpack-bundle-analyzer/download/webpack-bundle-analyzer-3.8.0.tgz",
- "integrity": "sha1-zms/kI2vBp/R9yZvaSy7O97ZuhY=",
- "dev": true,
- "requires": {
- "acorn": "^7.1.1",
- "acorn-walk": "^7.1.1",
- "bfj": "^6.1.1",
- "chalk": "^2.4.1",
- "commander": "^2.18.0",
- "ejs": "^2.6.1",
- "express": "^4.16.3",
- "filesize": "^3.6.1",
- "gzip-size": "^5.0.0",
- "lodash": "^4.17.15",
- "mkdirp": "^0.5.1",
- "opener": "^1.5.1",
- "ws": "^6.0.0"
- },
- "dependencies": {
- "acorn": {
- "version": "7.4.0",
- "resolved": "https://registry.npm.taobao.org/acorn/download/acorn-7.4.0.tgz?cache=0&sync_timestamp=1597235774928&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Facorn%2Fdownload%2Facorn-7.4.0.tgz",
- "integrity": "sha1-4a1IbmxUUBY0xsOXxcEh2qODYHw=",
- "dev": true
- }
- }
- },
- "webpack-chain": {
- "version": "6.5.1",
- "resolved": "https://registry.npm.taobao.org/webpack-chain/download/webpack-chain-6.5.1.tgz?cache=0&sync_timestamp=1595814928534&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwebpack-chain%2Fdownload%2Fwebpack-chain-6.5.1.tgz",
- "integrity": "sha1-TycoTLu2N+PI+970Pu9YjU2GEgY=",
- "dev": true,
- "requires": {
- "deepmerge": "^1.5.2",
- "javascript-stringify": "^2.0.1"
- }
- },
- "webpack-dev-middleware": {
- "version": "3.7.2",
- "resolved": "https://registry.npm.taobao.org/webpack-dev-middleware/download/webpack-dev-middleware-3.7.2.tgz?cache=0&sync_timestamp=1594744455919&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwebpack-dev-middleware%2Fdownload%2Fwebpack-dev-middleware-3.7.2.tgz",
- "integrity": "sha1-ABnD23FuP6XOy/ZPKriKdLqzMfM=",
- "dev": true,
- "requires": {
- "memory-fs": "^0.4.1",
- "mime": "^2.4.4",
- "mkdirp": "^0.5.1",
- "range-parser": "^1.2.1",
- "webpack-log": "^2.0.0"
- }
- },
- "webpack-dev-server": {
- "version": "3.11.0",
- "resolved": "https://registry.npm.taobao.org/webpack-dev-server/download/webpack-dev-server-3.11.0.tgz",
- "integrity": "sha1-jxVKO84bz9HMYY705wMniFXn/4w=",
- "dev": true,
- "requires": {
- "ansi-html": "0.0.7",
- "bonjour": "^3.5.0",
- "chokidar": "^2.1.8",
- "compression": "^1.7.4",
- "connect-history-api-fallback": "^1.6.0",
- "debug": "^4.1.1",
- "del": "^4.1.1",
- "express": "^4.17.1",
- "html-entities": "^1.3.1",
- "http-proxy-middleware": "0.19.1",
- "import-local": "^2.0.0",
- "internal-ip": "^4.3.0",
- "ip": "^1.1.5",
- "is-absolute-url": "^3.0.3",
- "killable": "^1.0.1",
- "loglevel": "^1.6.8",
- "opn": "^5.5.0",
- "p-retry": "^3.0.1",
- "portfinder": "^1.0.26",
- "schema-utils": "^1.0.0",
- "selfsigned": "^1.10.7",
- "semver": "^6.3.0",
- "serve-index": "^1.9.1",
- "sockjs": "0.3.20",
- "sockjs-client": "1.4.0",
- "spdy": "^4.0.2",
- "strip-ansi": "^3.0.1",
- "supports-color": "^6.1.0",
- "url": "^0.11.0",
- "webpack-dev-middleware": "^3.7.2",
- "webpack-log": "^2.0.0",
- "ws": "^6.2.1",
- "yargs": "^13.3.2"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-2.0.0.tgz",
- "integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=",
- "dev": true,
- "requires": {
- "micromatch": "^3.1.4",
- "normalize-path": "^2.1.1"
- },
- "dependencies": {
- "normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-2.1.1.tgz",
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
- "dev": true,
- "requires": {
- "remove-trailing-separator": "^1.0.1"
- }
- }
- }
- },
- "binary-extensions": {
- "version": "1.13.1",
- "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-1.13.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-1.13.1.tgz",
- "integrity": "sha1-WYr+VHVbKGilMw0q/51Ou1Mgm2U=",
- "dev": true
- },
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-5.3.1.tgz",
- "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=",
- "dev": true
- },
- "chokidar": {
- "version": "2.1.8",
- "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-2.1.8.tgz",
- "integrity": "sha1-gEs6e2qZNYw8XGHnHYco8EHP+Rc=",
- "dev": true,
- "requires": {
- "anymatch": "^2.0.0",
- "async-each": "^1.0.1",
- "braces": "^2.3.2",
- "fsevents": "^1.2.7",
- "glob-parent": "^3.1.0",
- "inherits": "^2.0.3",
- "is-binary-path": "^1.0.0",
- "is-glob": "^4.0.0",
- "normalize-path": "^3.0.0",
- "path-is-absolute": "^1.0.0",
- "readdirp": "^2.2.1",
- "upath": "^1.1.1"
- }
- },
- "cliui": {
- "version": "5.0.0",
- "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-5.0.0.tgz?cache=0&sync_timestamp=1597608006561&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcliui%2Fdownload%2Fcliui-5.0.0.tgz",
- "integrity": "sha1-3u/P2y6AB4SqNPRvoI4GhRx7u8U=",
- "dev": true,
- "requires": {
- "string-width": "^3.1.0",
- "strip-ansi": "^5.2.0",
- "wrap-ansi": "^5.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz",
- "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=",
- "dev": true
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-7.0.3.tgz",
- "integrity": "sha1-kzoEBShgyF6DwSJHnEdIqOTHIVY=",
- "dev": true
- },
- "fsevents": {
- "version": "1.2.13",
- "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-1.2.13.tgz",
- "integrity": "sha1-8yXLBFVZJCi88Rs4M3DvcOO/zDg=",
- "dev": true,
- "optional": true,
- "requires": {
- "bindings": "^1.5.0",
- "nan": "^2.12.1"
- }
- },
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "requires": {
- "is-glob": "^3.1.0",
- "path-dirname": "^1.0.0"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.0"
- }
- }
- }
- },
- "is-absolute-url": {
- "version": "3.0.3",
- "resolved": "https://registry.npm.taobao.org/is-absolute-url/download/is-absolute-url-3.0.3.tgz",
- "integrity": "sha1-lsaiK2ojkpsR6gr7GDbDatSl1pg=",
- "dev": true
- },
- "is-binary-path": {
- "version": "1.0.1",
- "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-1.0.1.tgz",
- "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
- "dev": true,
- "requires": {
- "binary-extensions": "^1.0.0"
- }
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "readdirp": {
- "version": "2.2.1",
- "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-2.2.1.tgz",
- "integrity": "sha1-DodiKjMlqjPokihcr4tOhGUppSU=",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.1.11",
- "micromatch": "^3.1.10",
- "readable-stream": "^2.0.2"
- }
- },
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/schema-utils/download/schema-utils-1.0.0.tgz?cache=0&sync_timestamp=1590789322916&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fschema-utils%2Fdownload%2Fschema-utils-1.0.0.tgz",
- "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=",
- "dev": true,
- "requires": {
- "ajv": "^6.1.0",
- "ajv-errors": "^1.0.0",
- "ajv-keywords": "^3.1.0"
- }
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&sync_timestamp=1589682805026&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
- "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-3.1.0.tgz",
- "integrity": "sha1-InZ74htirxCBV0MG9prFG2IgOWE=",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz",
- "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=",
- "dev": true
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "supports-color": {
- "version": "6.1.0",
- "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-6.1.0.tgz",
- "integrity": "sha1-B2Srxpxj1ayELdSGfo0CXogN+PM=",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- },
- "wrap-ansi": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-5.1.0.tgz",
- "integrity": "sha1-H9H2cjXVttD+54EFYAG/tpTAOwk=",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.0",
- "string-width": "^3.0.0",
- "strip-ansi": "^5.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz",
- "integrity": "sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=",
- "dev": true
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz?cache=0&sync_timestamp=1589682795383&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstrip-ansi%2Fdownload%2Fstrip-ansi-5.2.0.tgz",
- "integrity": "sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- }
- }
- }
- },
- "yargs": {
- "version": "13.3.2",
- "resolved": "https://registry.npm.taobao.org/yargs/download/yargs-13.3.2.tgz?cache=0&sync_timestamp=1597809611661&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-13.3.2.tgz",
- "integrity": "sha1-rX/+/sGqWVZayRX4Lcyzipwxot0=",
- "dev": true,
- "requires": {
- "cliui": "^5.0.0",
- "find-up": "^3.0.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^3.0.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^13.1.2"
- }
- },
- "yargs-parser": {
- "version": "13.1.2",
- "resolved": "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-13.1.2.tgz?cache=0&sync_timestamp=1596945681098&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-13.1.2.tgz",
- "integrity": "sha1-Ew8JcC667vJlDVTObj5XBvek+zg=",
- "dev": true,
- "requires": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- }
- }
- }
- },
- "webpack-log": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/webpack-log/download/webpack-log-2.0.0.tgz",
- "integrity": "sha1-W3ko4GN1k/EZ0y9iJ8HgrDHhtH8=",
- "dev": true,
- "requires": {
- "ansi-colors": "^3.0.0",
- "uuid": "^3.3.2"
- }
- },
- "webpack-merge": {
- "version": "4.2.2",
- "resolved": "https://registry.npm.taobao.org/webpack-merge/download/webpack-merge-4.2.2.tgz?cache=0&sync_timestamp=1597767024911&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwebpack-merge%2Fdownload%2Fwebpack-merge-4.2.2.tgz",
- "integrity": "sha1-onxS6ng9E5iv0gh/VH17nS9DY00=",
- "dev": true,
- "requires": {
- "lodash": "^4.17.15"
- }
- },
- "webpack-sources": {
- "version": "1.4.3",
- "resolved": "https://registry.npm.taobao.org/webpack-sources/download/webpack-sources-1.4.3.tgz",
- "integrity": "sha1-7t2OwLko+/HL/plOItLYkPMwqTM=",
- "dev": true,
- "requires": {
- "source-list-map": "^2.0.0",
- "source-map": "~0.6.1"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
- "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
- "dev": true
- }
- }
- },
- "websocket-driver": {
- "version": "0.6.5",
- "resolved": "https://registry.npm.taobao.org/websocket-driver/download/websocket-driver-0.6.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwebsocket-driver%2Fdownload%2Fwebsocket-driver-0.6.5.tgz",
- "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
- "dev": true,
- "requires": {
- "websocket-extensions": ">=0.1.1"
- }
- },
- "websocket-extensions": {
- "version": "0.1.4",
- "resolved": "https://registry.npm.taobao.org/websocket-extensions/download/websocket-extensions-0.1.4.tgz",
- "integrity": "sha1-f4RzvIOd/YdgituV1+sHUhFXikI=",
- "dev": true
- },
- "which": {
- "version": "1.3.1",
- "resolved": "https://registry.npm.taobao.org/which/download/which-1.3.1.tgz?cache=0&sync_timestamp=1589682812246&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwhich%2Fdownload%2Fwhich-1.3.1.tgz",
- "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=",
- "dev": true,
- "requires": {
- "isexe": "^2.0.0"
- }
- },
- "which-module": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/which-module/download/which-module-2.0.0.tgz",
- "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
- "dev": true
- },
- "word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npm.taobao.org/word-wrap/download/word-wrap-1.2.3.tgz?cache=0&sync_timestamp=1589683603678&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fword-wrap%2Fdownload%2Fword-wrap-1.2.3.tgz",
- "integrity": "sha1-YQY29rH3A4kb00dxzLF/uTtHB5w=",
- "dev": true
- },
- "worker-farm": {
- "version": "1.7.0",
- "resolved": "https://registry.npm.taobao.org/worker-farm/download/worker-farm-1.7.0.tgz",
- "integrity": "sha1-JqlMU5G7ypJhUgAvabhKS/dy5ag=",
- "dev": true,
- "requires": {
- "errno": "~0.1.7"
- }
- },
- "wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-6.2.0.tgz",
- "integrity": "sha1-6Tk7oHEC5skaOyIUePAlfNKFblM=",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.2.1.tgz",
- "integrity": "sha1-kK51xCTQCNJiTFvynq0xd+v881k=",
- "dev": true,
- "requires": {
- "@types/color-name": "^1.1.1",
- "color-convert": "^2.0.1"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
- "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
- "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
- "dev": true
- }
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
- },
- "write": {
- "version": "1.0.3",
- "resolved": "https://registry.npm.taobao.org/write/download/write-1.0.3.tgz",
- "integrity": "sha1-CADhRSO5I6OH5BUSPIZWFqrg9cM=",
- "dev": true,
- "requires": {
- "mkdirp": "^0.5.1"
- }
- },
- "ws": {
- "version": "6.2.1",
- "resolved": "https://registry.npm.taobao.org/ws/download/ws-6.2.1.tgz?cache=0&sync_timestamp=1593925481882&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fws%2Fdownload%2Fws-6.2.1.tgz",
- "integrity": "sha1-RC/fCkftZPWbal2P8TD0dI7VJPs=",
- "dev": true,
- "requires": {
- "async-limiter": "~1.0.0"
- }
- },
- "xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npm.taobao.org/xtend/download/xtend-4.0.2.tgz?cache=0&sync_timestamp=1589682817913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxtend%2Fdownload%2Fxtend-4.0.2.tgz",
- "integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=",
- "dev": true
- },
- "y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/y18n/download/y18n-4.0.0.tgz",
- "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms=",
- "dev": true
- },
- "yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-3.1.1.tgz",
- "integrity": "sha1-27fa+b/YusmrRev2ArjLrQ1dCP0=",
- "dev": true
- },
- "yargs": {
- "version": "15.4.1",
- "resolved": "https://registry.npm.taobao.org/yargs/download/yargs-15.4.1.tgz?cache=0&sync_timestamp=1597809611661&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-15.4.1.tgz",
- "integrity": "sha1-DYehbeAa7p2L7Cv7909nhRcw9Pg=",
- "dev": true,
- "requires": {
- "cliui": "^6.0.0",
- "decamelize": "^1.2.0",
- "find-up": "^4.1.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^4.2.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^18.1.2"
- },
- "dependencies": {
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-4.1.0.tgz?cache=0&sync_timestamp=1597169842138&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-up%2Fdownload%2Ffind-up-4.1.0.tgz",
- "integrity": "sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=",
- "dev": true,
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz",
- "integrity": "sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=",
- "dev": true,
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npm.taobao.org/p-locate/download/p-locate-4.1.0.tgz",
- "integrity": "sha1-o0KLtwiLOmApL2aRkni3wpetTwc=",
- "dev": true,
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-4.0.0.tgz",
- "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=",
- "dev": true
- }
- }
- },
- "yargs-parser": {
- "version": "18.1.3",
- "resolved": "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-18.1.3.tgz?cache=0&sync_timestamp=1596945681098&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-18.1.3.tgz",
- "integrity": "sha1-vmjEl1xrKr9GkjawyHA2L6sJp7A=",
- "dev": true,
- "requires": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- },
- "dependencies": {
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz?cache=0&sync_timestamp=1589682790492&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcamelcase%2Fdownload%2Fcamelcase-5.3.1.tgz",
- "integrity": "sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=",
- "dev": true
- }
- }
- },
- "yorkie": {
- "version": "2.0.0",
- "resolved": "https://registry.npm.taobao.org/yorkie/download/yorkie-2.0.0.tgz",
- "integrity": "sha1-kkEZEtQ1IU4SxRwq4Qk+VLa7g9k=",
- "dev": true,
- "requires": {
- "execa": "^0.8.0",
- "is-ci": "^1.0.10",
- "normalize-path": "^1.0.0",
- "strip-indent": "^2.0.0"
- },
- "dependencies": {
- "cross-spawn": {
- "version": "5.1.0",
- "resolved": "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz",
- "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
- "dev": true,
- "requires": {
- "lru-cache": "^4.0.1",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
- "execa": {
- "version": "0.8.0",
- "resolved": "https://registry.npm.taobao.org/execa/download/execa-0.8.0.tgz",
- "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=",
- "dev": true,
- "requires": {
- "cross-spawn": "^5.0.1",
- "get-stream": "^3.0.0",
- "is-stream": "^1.1.0",
- "npm-run-path": "^2.0.0",
- "p-finally": "^1.0.0",
- "signal-exit": "^3.0.0",
- "strip-eof": "^1.0.0"
- }
- },
- "get-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npm.taobao.org/get-stream/download/get-stream-3.0.0.tgz?cache=0&sync_timestamp=1597056502934&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fget-stream%2Fdownload%2Fget-stream-3.0.0.tgz",
- "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
- "dev": true
- },
- "lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.5.tgz?cache=0&sync_timestamp=1594427582110&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-4.1.5.tgz",
- "integrity": "sha1-i75Q6oW+1ZvJ4z3KuCNe6bz0Q80=",
- "dev": true,
- "requires": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
- }
- },
- "normalize-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-1.0.0.tgz",
- "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=",
- "dev": true
- },
- "yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
- }
- }
- }
- }
-}
diff --git a/sub-store-web/package.json b/sub-store-web/package.json
deleted file mode 100644
index d7e10b5c85..0000000000
--- a/sub-store-web/package.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "name": "sub-store-web",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "serve": "vue-cli-service serve",
- "build": "vue-cli-service build",
- "lint": "vue-cli-service lint"
- },
- "dependencies": {
- "core-js": "^3.6.5",
- "vue": "^3.0.0-0"
- },
- "devDependencies": {
- "@vue/cli-plugin-babel": "~4.5.0",
- "@vue/cli-plugin-eslint": "~4.5.0",
- "@vue/cli-service": "~4.5.0",
- "@vue/compiler-sfc": "^3.0.0-0",
- "babel-eslint": "^10.1.0",
- "eslint": "^6.7.2",
- "eslint-plugin-vue": "^7.0.0-0",
- "vue-cli-plugin-vuetify": "~2.0.7"
- },
- "eslintConfig": {
- "root": true,
- "env": {
- "node": true
- },
- "extends": [
- "plugin:vue/vue3-essential",
- "eslint:recommended"
- ],
- "parserOptions": {
- "parser": "babel-eslint"
- },
- "rules": {}
- },
- "browserslist": [
- "> 1%",
- "last 2 versions",
- "not dead"
- ]
-}
diff --git a/sub-store-web/public/favicon.ico b/sub-store-web/public/favicon.ico
deleted file mode 100644
index df36fcfb72..0000000000
Binary files a/sub-store-web/public/favicon.ico and /dev/null differ
diff --git a/sub-store-web/public/index.html b/sub-store-web/public/index.html
deleted file mode 100644
index 4123528656..0000000000
--- a/sub-store-web/public/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
- <%= htmlWebpackPlugin.options.title %>
-
-
-
- We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
-
-
-
-
-
diff --git a/sub-store-web/src/App.vue b/sub-store-web/src/App.vue
deleted file mode 100644
index 55df315325..0000000000
--- a/sub-store-web/src/App.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/sub-store-web/src/assets/logo.png b/sub-store-web/src/assets/logo.png
deleted file mode 100644
index f3d2503fc2..0000000000
Binary files a/sub-store-web/src/assets/logo.png and /dev/null differ
diff --git a/sub-store-web/src/components/HelloWorld.vue b/sub-store-web/src/components/HelloWorld.vue
deleted file mode 100644
index 879051a297..0000000000
--- a/sub-store-web/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
{{ msg }}
-
- For a guide and recipes on how to configure / customize this project,
- check out the
- vue-cli documentation .
-
-
Installed CLI Plugins
-
-
Essential Links
-
-
Ecosystem
-
-
-
-
-
-
-
-
diff --git a/sub-store-web/src/main.js b/sub-store-web/src/main.js
deleted file mode 100644
index 01433bca2a..0000000000
--- a/sub-store-web/src/main.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-
-createApp(App).mount('#app')
diff --git a/sub-store.js b/sub-store.js
deleted file mode 100644
index cbd9691b97..0000000000
--- a/sub-store.js
+++ /dev/null
@@ -1,2420 +0,0 @@
-/**
- * Sub-Store v0.1 (Backend only)
- * @Author: Peng-YM
- * @Description:
- * 适用于QX,Loon,Surge的订阅管理工具。
- * - 功能
- * 1. 订阅转换,支持SS, SSR, V2RayN, QX, Loon, Surge格式的互相转换。
- * 2. 节点过滤,重命名,排序等。
- * 3. 订阅拆分,组合。
- */
-
-const $ = API("sub-store");
-
-// Constants
-const SUBS_KEY = "subs";
-const COLLECTIONS_KEY = "collections";
-
-// SOME INITIALIZATIONS
-if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
-if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
-
-// BACKEND API
-const $app = express();
-
-// subscriptions
-$app.get("/download/:name", downloadSub)
-
-$app.route("/sub/:name")
- .get(getSub)
- .patch(updateSub)
- .delete(deleteSub);
-
-$app.route("/sub")
- .get(getAllSubs)
- .post(newSub)
- .delete(deleteAllSubs);
-
-// collections
-$app.get("/download/collection/:name", downloadCollection);
-$app.route("/collection/:name")
- .get(getCollection)
- .patch(updateCollection)
- .delete(deleteCollection);
-$app.route("/collection")
- .get(getAllCollections)
- .post(newCollection)
- .delete(deleteAllCollections);
-
-$app.all("/", (req, res) => {
- res.send("Hello from Sub-Store! Made with ❤️ by Peng-YM.")
-});
-
-$app.start();
-
-// SOME CONSTANTS
-const FALL_BACK_TARGET = "Raw";
-const DEFAULT_SUPPORTED_PLATFORMS = {
- QX: true,
- Loon: true,
- Surge: true,
- Raw: true
-}
-const AVAILABLE_FILTERS = {
- "Keyword Filter": KeywordFilter,
- "Discard Keyword Filter": DiscardKeywordFilter,
- "Useless Filter": UselessFilter,
- "Region Filter": RegionFilter,
- "Regex Filter": RegexFilter,
- "Discard Regex Filter": DiscardRegexFilter,
- "Type Filter": TypeFilter,
- "Script Filter": ScriptFilter
-}
-
-const AVAILABLE_OPERATORS = {
- "Set Property Operator": SetPropertyOperator,
- "Flag Operator": FlagOperator,
- "Sort Operator": SortOperator,
- "Keyword Sort Operator": KeywordSortOperator,
- "Keyword Rename Operator": KeywordRenameOperator,
- "Keyword Delete Operator": KeywordDeleteOperator,
- "Regex Rename Operator": RegexRenameOperator,
- "Regex Delete Operator": RegexDeleteOperator,
- "Script Operator": ScriptOperator
-}
-
-/**************************** API -- Subscriptions ***************************************/
-// download subscription, for APP only
-async function downloadSub(req, res) {
- const {name} = req.params;
- const platform = getPlatformFromHeaders(req.headers);
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[name]) {
- const sub = allSubs[name];
- try {
- const output = await parseSub(sub, platform);
- res.send(output);
- } catch (err) {
- $.notify('[Sub-Store]', '❌ 无法获取订阅!', `错误信息:${err}`)
- res.status(500).json({
- status: "failed",
- message: err
- });
- }
- } else {
- res.status(404).json({
- status: "failed",
- message: `订阅${name}不存在!`
- });
- }
-}
-
-async function parseSub(sub, platform) {
- // download from url
- const raw = await $.http.get(sub.url).then(resp => resp.body).catch(err => {
- throw new Error(err);
- });
- console.log("=======================================================================");
- console.log(`Processing subscription: ${sub.name}, target platform ==> ${platform}.`);
- const $parser = ProxyParser(platform);
- let proxies = $parser.parse(raw);
-
- // filters
- const $filter = ProxyFilter();
- // operators
- const $operator = ProxyOperator();
-
- for (const item of sub.process || []) {
- if (item.type.indexOf("Script") !== -1) {
- if (item.args && item.args[0].indexOf("http") !== -1) {
- // if this is remote script
- item.args[0] = await $.http.get(item.args[0]).then(resp => resp.body).catch(err => {
- throw new Error(`Error when downloading remote script: ${item.args[0]}.\n Reason: ${err}`);
- });
- }
- }
- if (item.type.indexOf("Filter") !== -1) {
- const filter = AVAILABLE_FILTERS[item.type];
- if (filter) {
- $filter.addFilters(filter(...(item.args || [])));
- proxies = $filter.process(proxies);
- console.log(`Applying filter "${item.type}" with arguments:\n >>> ${item.args || "None"}`);
- }
- } else if (item.type.indexOf("Operator") !== -1) {
- const operator = AVAILABLE_OPERATORS[item.type];
- if (operator) {
- $operator.addOperators(operator(...(item.args || [])));
- proxies = $operator.process(proxies);
- console.log(`Applying operator "${item.type}" with arguments: \n >>> ${item.args || "None"}`);
- }
- }
- }
- return $parser.produce(proxies);
-}
-
-// Subscriptions
-async function getSub(req, res) {
- const {name} = req.params;
- const sub = $.read(SUBS_KEY)[name];
- if (sub) {
- res.json({
- status: "success",
- data: sub
- });
- } else {
- res.status(404).json({
- status: "failed",
- message: `未找到订阅:${name}!`
- });
- }
-}
-
-async function newSub(req, res) {
- const sub = req.body;
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[sub.name]) {
- res.status(500).json({
- status: "failed",
- message: `订阅${sub.name}已存在!`
- });
- }
- // validate name
- if (/^[\w-_]*$/.test(sub.name)) {
- allSubs[sub.name] = sub;
- $.write(allSubs, SUBS_KEY);
- res.status(201).json({
- status: "success",
- data: sub
- });
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
- })
- }
-}
-
-async function updateSub(req, res) {
- const {name} = req.params;
- let sub = req.body;
- const allSubs = $.read(SUBS_KEY);
- if (allSubs[name]) {
- const newSub = {
- ...allSubs[name],
- ...sub
- };
- allSubs[name] = newSub;
- $.write(allSubs, SUBS_KEY);
- res.json({
- status: "success",
- data: newSub
- })
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅${name}不存在,无法更新!`
- });
- }
-}
-
-async function deleteSub(req, res) {
- const {name} = req.params;
- let allSubs = $.read(SUBS_KEY);
- delete allSubs[name];
- $.write(allSubs, SUBS_KEY);
- res.json({
- status: "success"
- });
-}
-
-async function getAllSubs(req, res) {
- const allSubs = $.read(SUBS_KEY);
- res.json({
- status: "success",
- data: Object.keys(allSubs)
- });
-}
-
-async function deleteAllSubs(req, res) {
- $.write({}, SUBS_KEY);
- res.json({
- status: "success"
- });
-}
-
-// Collections
-async function downloadCollection(req, res) {
- const {name} = req.params;
- const collection = $.read(COLLECTIONS_KEY)[name];
- const platform = getPlatformFromHeaders(req.headers);
- if (collection) {
- const subs = collection.subscriptions || [];
- const output = await Promise.all(subs.map(async id => {
- const sub = $.read(SUBS_KEY)[id];
- try {
- return parseSub(sub, platform);
- } catch (err) {
- console.log(`ERROR when process subscription: ${id}`);
- return "";
- }
- }));
- res.send(output.join("\n"));
- } else {
- $.notify('[Sub-Store]', `❌ 未找到订阅集:${name}!`)
- res.status(404).json({
- status: "failed",
- message: `❌ 未找到订阅集:${name}!`
- });
- }
-}
-
-async function getCollection(req, res) {
- const {name} = req.params;
- const collection = $.read(COLLECTIONS_KEY)[name];
- if (collection) {
- res.json({
- status: "success",
- data: collection
- });
- } else {
- res.status(404).json({
- status: "failed",
- message: `未找到订阅集:${name}!`
- });
- }
-}
-
-async function newCollection(req, res) {
- const collection = req.body;
- const allCol = $.read(COLLECTIONS_KEY);
- if (allCol[collection.name]) {
- res.status(500).json({
- status: "failed",
- message: `订阅集${collection.name}已存在!`
- });
- }
- // validate name
- if (/^[\w-_]*$/.test(collection.name)) {
- allCol[collection.name] = collection;
- $.write(allCol, COLLECTIONS_KEY);
- res.status(201).json({
- status: "success",
- data: collection
- });
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
- })
- }
-}
-
-async function updateCollection(req, res) {
- const {name} = req.params;
- let collection = req.body;
- const allCol = $.read(COLLECTIONS_KEY);
- if (allCol[name]) {
- const newCol = {
- ...allCol[name],
- ...collection
- };
- allCol[name] = newCol;
- $.write(allCol, COLLECTIONS_KEY);
- res.json({
- status: "success",
- data: newCol
- })
- } else {
- res.status(500).json({
- status: "failed",
- message: `订阅集${name}不存在,无法更新!`
- });
- }
-}
-
-async function deleteCollection(req, res) {
- const {name} = req.params;
- let allCol = $.read(COLLECTIONS_KEY);
- delete allCol[name];
- $.write(allCol, COLLECTIONS_KEY);
- res.json({
- status: "success"
- });
-}
-
-async function getAllCollections(req, res) {
- const allCols = $.read(COLLECTIONS_KEY);
- res.json({
- status: "success",
- data: Object.keys(allCols)
- });
-}
-
-async function deleteAllCollections(req, res) {
- $.write({}, COLLECTIONS_KEY);
- res.json({
- status: "success"
- });
-}
-
-/**************************** Proxy Handlers ***************************************/
-function ProxyParser(targetPlatform) {
- // parser collections
- const parsers = [];
- const producers = [];
-
- function addParsers(...args) {
- args.forEach(a => parsers.push(a()));
- }
-
- function addProducers(...args) {
- args.forEach(a => producers.push(a()))
- }
-
- function parse(raw) {
- raw = preprocessing(raw);
- const lines = raw.split("\n");
- const result = [];
- // convert to json format
- for (let line of lines) {
- line = line.trim();
- if (line.length === 0) continue; // skip empty line
- if (line.startsWith("#")) continue; // skip comments
- let matched = false;
- for (const p of parsers) {
- const {patternTest, func} = p;
-
- // some lines with weird format may produce errors!
- let patternMatched;
- try {
- patternMatched = patternTest(line);
- } catch (err) {
- patternMatched = false;
- }
-
- if (patternMatched) {
- matched = true;
- // run parser safely.
- try {
- const proxy = func(line);
- if (!proxy) {
- // failed to parse this line
- console.log(`ERROR: parser return nothing for \n${line}\n`);
- break;
- }
- // skip unsupported proxies
- // if proxy.supported is undefined, assume that all platforms are supported.
- if (typeof proxy.supported === 'undefined' || proxy.supported[targetPlatform]) {
- delete proxy.supported;
- result.push(proxy);
- break;
- }
- } catch (err) {
- console.log(`ERROR: Failed to parse line: \n ${line}\n Reason: ${err}`);
- }
- }
- }
- if (!matched) {
- console.log(`ERROR: Failed to find a rule to parse line: \n${line}\n`);
- }
- }
- if (result.length === 0) {
- throw new Error(`ERROR: Input does not contains any valid node for platform ${targetPlatform}`)
- }
- return result;
- }
-
- function produce(proxies) {
- for (const p of producers) {
- if (p.targetPlatform === targetPlatform) {
- return proxies.map(proxy => {
- try {
- return p.output(proxy)
- } catch (err) {
- console.log(`ERROR: cannot produce proxy: ${JSON.stringify(proxy)}\nReason: ${err}`);
- return "";
- }
- }).join("\n");
- }
- }
- throw new Error(`Cannot find any producer for target platform: ${targetPlatform}`);
- }
-
- // preprocess raw input
- function preprocessing(raw) {
- let output;
- if (raw.indexOf("DOCTYPE html") !== -1) {
- // HTML format, maybe a wrong URL!
- throw new Error("Invalid format HTML!");
- }
- // check if content is based64 encoded
- const Base64 = new Base64Code();
- const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v"];
- if (keys.some(k => raw.indexOf(k) !== -1)) {
- output = Base64.safeDecode(raw);
- } else {
- output = raw;
- }
- output = output.split("\n");
- for (let i = 0; i < output.length; i++) {
- output[i] = output[i].trim(); // trim lines
- }
- return output.join("\n");
- }
-
- // Parsers
- addParsers(
- // URI format parsers
- URI_SS, URI_SSR, URI_VMess, URI_Trojan,
- // Quantumult X platform
- QX_SS, QX_SSR, QX_VMess, QX_Trojan, QX_Http,
- // Loon platform
- Loon_SS, Loon_SSR, Loon_VMess, Loon_Trojan, Loon_Http,
- // Surge platform
- Surge_SS, Surge_VMess, Surge_Trojan, Surge_Http
- );
-
- // Producers
- addProducers(
- QX_Producer, Loon_Producer, Surge_Producer, Raw_Producer
- );
-
- return {
- parse, produce
- };
-}
-
-function ProxyFilter() {
- const filters = [];
-
- function addFilters(...args) {
- args.forEach(a => filters.push(a));
- }
-
- // select proxies
- function process(proxies) {
- let selected = FULL(proxies.length, true);
- for (const filter of filters) {
- try {
- selected = AND(selected, filter.func(proxies));
- } catch (err) {
- console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
- }
- }
- return proxies.filter((_, i) => selected[i])
- }
-
- return {
- process, addFilters
- }
-}
-
-function ProxyOperator() {
- const operators = [];
-
- function addOperators(...args) {
- args.forEach(a => operators.push(a));
- }
-
- // run all operators
- function process(proxies) {
- let output = clone(proxies);
- for (const op of operators) {
- try {
- const output_ = op.func(output);
- if (output_) output = output_;
- } catch (err) {
- // print log and skip this operator
- console.log(`ERROR: cannot apply operator ${op.name}! Reason: ${err}`);
- }
- }
- return output;
- }
-
- return {addOperators, process}
-}
-
-/**************************** URI Format ***************************************/
-// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
-// reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
-function URI_SS() {
- const patternTest = (line) => {
- return /^ss:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- // parse url
- let content = line.split("ss://")[1];
-
- const proxy = {
- name: decodeURIComponent(line.split("#")[1]),
- type: "ss",
- supported
- }
- content = content.split("#")[0]; // strip proxy name
-
- // handle IPV4 and IPV6
- const serverAndPort = content.match(/@([^\/]*)\//)[1];
- const portIdx = serverAndPort.lastIndexOf(":");
- proxy.server = serverAndPort.substring(0, portIdx);
- proxy.port = serverAndPort.substring(portIdx + 1);
-
- const userInfo = Base64.safeDecode(content.split("@")[0]).split(":");
- proxy.cipher = userInfo[0];
- proxy.password = userInfo[1];
-
- // handle obfs
- const idx = content.indexOf("?plugin=");
- if (idx !== -1) {
- const pluginInfo = ("plugin=" + decodeURIComponent(content.split("?plugin=")[1])).split(";");
- const params = {};
- for (const item of pluginInfo) {
- const [key, val] = item.split("=");
- if (key) params[key] = val || true; // some options like "tls" will not have value
- }
- switch (params.plugin) {
- case 'simple-obfs':
- proxy.plugin = 'obfs'
- proxy['plugin-opts'] = {
- mode: params.obfs,
- host: params['obfs-host']
- }
- break
- case 'v2ray-plugin':
- proxy.supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Loon: false,
- Surge: false
- }
- proxy.obfs = 'v2ray-plugin'
- proxy['plugin-opts'] = {
- mode: "websocket",
- host: params['obfs-host'],
- path: params.path || ""
- }
- break
- default:
- throw new Error(`Unsupported plugin option: ${params.plugin}`)
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-// Parse URI SSR format, such as ssr://xxx
-function URI_SSR() {
- const patternTest = (line) => {
- return /^ssr:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Surge: false
- }
-
- const func = (line) => {
- line = Base64.safeDecode(line.split("ssr://")[1]);
-
- // handle IPV6 & IPV4 format
- let splitIdx = line.indexOf(':origin');
- if (splitIdx === -1) {
- splitIdx = line.indexOf(":auth_");
- }
- const serverAndPort = line.substring(0, splitIdx);
- const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
- const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1);
-
- let params = line.substring(splitIdx + 1).split("/?")[0].split(":");
- let proxy = {
- type: "ssr",
- server,
- port,
- protocol: params[0],
- cipher: params[1],
- obfs: params[2],
- password: Base64.safeDecode(params[3]),
- supported
- }
- // get other params
- params = {};
- line = line.split("/?")[1].split("&");
- if (line.length > 1) {
- for (const item of line) {
- const [key, val] = item.split("=");
- params[key] = val;
- }
- }
- proxy = {
- ...proxy,
- name: Base64.safeDecode(params.remarks),
- "protocol-param": Base64.safeDecode(params.protoparam).replace(/\s/g, "") || "",
- "obfs-param": Base64.safeDecode(params.obfsparam).replace(/\s/g, "") || ""
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-// V2rayN URI VMess format
-// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
-function URI_VMess() {
- const patternTest = (line) => {
- return /^vmess:\/\//.test(line);
- }
- const Base64 = new Base64Code();
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- line = line.split("vmess://")[1];
- const params = JSON.parse(Base64.safeDecode(line));
- const proxy = {
- name: params.ps,
- type: "vmess",
- server: params.add,
- port: params.port,
- cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default.
- uuid: params.id,
- alterId: params.aid || 0,
- tls: JSON.parse(params.tls || "false"),
- supported
- }
- // handle obfs
- if (params.net === 'ws') {
- proxy.network = 'ws';
- proxy['ws-path'] = params.path;
- proxy['ws-headers'] = {
- Host: params.host || params.add
- }
- }
- return proxy
- }
- return {patternTest, func};
-}
-
-// Trojan URI format
-function URI_Trojan() {
- const patternTest = (line) => {
- return /^trojan:\/\//.test(line);
- }
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- const func = (line) => {
- // trojan forces to use 443 port
- if (line.indexOf(":443") === -1) {
- throw new Error("Trojan port should always be 443!");
- }
- line = line.split("trojan://")[1];
- const server = line.split("@")[1].split(":443")[0];
-
- return {
- name: `[Trojan] ${server}`, // trojan uri has no server tag!
- type: "trojan",
- server,
- port: 443,
- password: line.split("@")[0],
- supported
- }
- }
- return {patternTest, func};
-}
-
-/**************************** Quantumult X ***************************************/
-function QX_SS() {
- const patternTest = (line) => {
- return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") === -1;
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- name: params.tag,
- type: "ss",
- server: params.server,
- port: params.port,
- cipher: params.method,
- password: params.password,
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- supported: clone(DEFAULT_SUPPORTED_PLATFORMS)
- };
- // handle obfs options
- if (params.obfs) {
- proxy["plugin-opts"] = {
- host: params['obfs-host'] || proxy.server
- };
- switch (params.obfs) {
- case "http":
- case "tls":
- proxy.plugin = "obfs";
- proxy["plugin-opts"].mode = params.obfs;
- break;
- case "ws":
- case "wss":
- proxy["plugin-opts"] = {
- ...proxy["plugin-opts"],
- mode: "websocket",
- path: params['obfs-uri'],
- tls: params.obfs === 'wss'
- }
- proxy.plugin = "v2ray-plugin"
- // Surge and Loon lack support for v2ray-plugin obfs
- proxy.supported.Surge = false
- proxy.supported.Loon = false
- break;
- }
- }
- return proxy;
- };
- return {patternTest, func};
-}
-
-function QX_SSR() {
- const patternTest = (line) => {
- return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") !== -1;
- };
- const supported = {
- ...DEFAULT_SUPPORTED_PLATFORMS,
- Surge: false
- }
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- name: params.tag,
- type: "ssr",
- server: params.server,
- port: params.port,
- cipher: params.method,
- password: params.password,
- protocol: params["ssr-protocol"],
- obfs: "plain", // default obfs
- "protocol-param": params['ssr-protocol-param'],
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- supported
- }
- // handle obfs options
- if (params.obfs) {
- proxy.obfs = params.obfs;
- proxy['obfs-param'] = params['obfs-host']
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function QX_VMess() {
- const patternTest = (line) => {
- return /^vmess\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line)
- const proxy = {
- type: "vmess",
- name: params.tag,
- server: params.server,
- port: params.port,
- cipher: params.method || 'none',
- uuid: params.password,
- alterId: 0,
- tls: params.obfs === 'over-tls' || params.obfs === 'wss',
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- if (proxy.tls) {
- proxy.sni = params['obfs-host'] || params.server;
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- }
- // handle ws headers
- if (params.obfs === 'ws' || params.obfs === 'wss') {
- proxy.network = 'ws';
- proxy['ws-path'] = params['obfs-uri'];
- proxy['ws-headers'] = {
- Host: params['obfs-host'] || params.server // if no host provided, use the same as server
- }
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-function QX_Trojan() {
- const patternTest = (line) => {
- return /^trojan\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- type: "trojan",
- name: params.tag,
- server: params.server,
- port: params.port,
- password: params.password,
- sni: params['tls-host'] || params.server,
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- return proxy;
- }
- return {patternTest, func}
-}
-
-function QX_Http() {
- const patternTest = (line) => {
- return /^http\s*=/.test(line.split(",")[0].trim());
- };
- const func = (line) => {
- const params = getQXParams(line);
- const proxy = {
- type: "http",
- name: params.tag,
- server: params.server,
- port: params.port,
- username: params.username,
- password: params.password,
- tls: JSON.parse(params['over-tls'] || "false"),
- udp: JSON.parse(params["udp-relay"] || "false"),
- tfo: JSON.parse(params["fast-open"] || "false"),
- }
- if (proxy.tls) {
- proxy.sni = params['tls-host'] || proxy.server;
- proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
- }
- return proxy;
- }
-
- return {patternTest, func};
-}
-
-function getQXParams(line) {
- const groups = line.split(",");
- const params = {};
- const protocols = ["shadowsocks", "vmess", "http", "trojan"];
- groups.forEach((g) => {
- const [key, value] = g.split("=");
- if (protocols.indexOf(key) !== -1) {
- params.type = key;
- const conf = value.split(":");
- params.server = conf[0];
- params.port = conf[1];
- } else {
- params[key.trim()] = value.trim();
- }
- });
- return params;
-}
-
-/**************************** Loon ***************************************/
-function Loon_SS() {
- const patternTest = (line) => {
- return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocks';
- }
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "ss",
- server: params[1],
- port: params[2],
- cipher: params[3],
- password: params[4].replace(/"/g, "")
- }
- // handle obfs
- if (params.length > 5) {
- proxy.plugin = 'obfs';
- proxy['plugin-opts'] = {
- mode: proxy.obfs,
- host: params[6]
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Loon_SSR() {
- const patternTest = (line) => {
- return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocksr';
- }
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
- supported.Surge = false;
- return {
- name: line.split("=")[0].trim(),
- type: "ssr",
- server: params[1],
- port: params[2],
- cipher: params[3],
- password: params[4].replace(/"/g, ""),
- protocol: params[5],
- "protocol-param": params[6].match(/{(.*)}/)[1],
- supported,
- obfs: params[7],
- 'obfs-param': params[8].match(/{(.*)}/)[1]
- }
- }
- return {patternTest, func};
-}
-
-function Loon_VMess() {
- const patternTest = (line) => {
- // distinguish between surge vmess
- return /^.*=\s*vmess/i.test(line.split(",")[0]) && line.indexOf("username") === -1;
- }
- const func = (line) => {
- let params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "vmess",
- server: params[1],
- port: params[2],
- cipher: params[3] || 'none',
- uuid: params[4].replace(/"/g, ""),
- alterId: 0,
- }
- // get transport options
- params = params.splice(5);
- for (const item of params) {
- const [key, val] = item.split(":");
- params[key] = val;
- }
- proxy.tls = JSON.parse(params['over-tls'] || 'false');
- if (proxy.tls) {
- proxy.sni = params['tls-name'] || proxy.server;
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
- }
- switch (params.transport) {
- case "tcp":
- break;
- case "ws":
- proxy.network = params.transport
- proxy['ws-path'] = params.path
- proxy['ws-headers'] = {
- Host: params.host
- }
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false')
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Loon_Trojan() {
- const patternTest = (line) => {
- return /^.*=\s*trojan/i.test(line.split(",")[0]) && line.indexOf("password") === -1;
- }
-
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "trojan",
- server: params[1],
- port: params[2],
- password: params[3].replace(/"/g, ""),
- sni: params[1], // default sni is the server itself
- scert: JSON.parse(params['skip-cert-verify'] || 'false')
- }
- // trojan sni
- if (params.length > 4) {
- const [key, val] = params[4].split(":");
- if (key === 'tls-name') proxy.sni = val;
- else throw new Error(`ERROR: unknown option ${key} for line: \n${line}`);
- }
- return proxy;
- }
-
- return {patternTest, func}
-}
-
-function Loon_Http() {
- const patternTest = (line) => {
- return /^.*=\s*http/i.test(line.split(",")[0])
- && line.split(",").length === 5
- && line.indexOf("username") === -1
- && line.indexOf("password") === -1
- }
-
- const func = (line) => {
- const params = line.split("=")[1].split(",");
- const proxy = {
- name: line.split("=")[0].trim(),
- type: "http",
- server: params[1],
- port: params[2],
- tls: params[2] === "443", // port 443 is considered as https type
- username: (params[3] || "").replace(/"/g, ""),
- password: (params[4] || "").replace(/"/g, "")
- }
- if (proxy.tls) {
- proxy.sni = params['tls-name'] || proxy.server;
- proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
- }
-
- return proxy;
- }
- return {patternTest, func}
-}
-
-/**************************** Surge ***************************************/
-function Surge_SS() {
- const patternTest = (line) => {
- return /^.*=\s*ss/.test(line.split(",")[0]);
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "ss",
- server: params.server,
- port: params.port,
- cipher: params['encrypt-method'],
- password: params.password,
- tfo: JSON.parse(params.tfo || "false"),
- udp: JSON.parse(params['udp-relay'] || "false"),
- }
- // handle obfs
- if (params.obfs) {
- proxy.plugin = 'obfs';
- proxy['plugin-opts'] = {
- mode: params.obfs,
- host: params['obfs-host']
- }
- }
- return proxy;
- }
- return {patternTest, func}
-}
-
-function Surge_VMess() {
- const patternTest = (line) => {
- return /^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1;
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "vmess",
- server: params.server,
- port: params.port,
- uuid: params.username,
- alterId: 0, // surge does not have this field
- cipher: "none", // surge does not have this field
- tls: JSON.parse(params.tls || "false"),
- tfo: JSON.parse(params.tfo || "false"),
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
- proxy.sni = params['sni'] || params.server;
- }
- // use websocket
- if (JSON.parse(params.ws || "false")) {
- proxy.network = 'ws';
- proxy['ws-path'] = params['ws-path'];
- proxy['ws-headers'] = {
- Host: params.sni
- }
- }
- return proxy;
- }
- return {patternTest, func};
-}
-
-function Surge_Trojan() {
- const patternTest = (line) => {
- return /^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1;
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- return {
- name: params.name,
- type: "trojan",
- server: params.server,
- port: params.port,
- password: params.password,
- sni: params.sni || params.server,
- tfo: JSON.parse(params.tfo || "false"),
- scert: JSON.parse(params['skip-cert-verify'] || "false"),
- }
- }
-
- return {patternTest, func};
-}
-
-function Surge_Http() {
- const patternTest = (line) => {
- return /^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().patternTest(line)
- }
- const func = (line) => {
- const params = getSurgeParams(line);
- const proxy = {
- name: params.name,
- type: "http",
- server: params.server,
- port: params.port,
- tls: JSON.parse(params.tls || "false"),
- tfo: JSON.parse(params.tfo || "false"),
- }
- if (proxy.tls) {
- proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
- proxy.sni = params.sni || params.server;
- }
- if (params.username !== 'none') proxy.username = params.username;
- if (params.password !== 'none') proxy.password = params.password;
- return proxy;
- }
- return {patternTest, func}
-}
-
-function getSurgeParams(line) {
- const params = {};
- params.name = line.split("=")[0].trim();
- const segments = line.split(",");
- params.server = segments[1].trim();
- params.port = segments[2].trim();
- for (let i = 3; i < segments.length; i++) {
- const item = segments[i]
- if (item.indexOf("=") !== -1) {
- const [key, value] = item.split("=");
- params[key.trim()] = value.trim();
- }
- }
- return params;
-}
-
-/**************************** Output Functions ***************************************/
-function QX_Producer() {
- const targetPlatform = "QX";
- const output = (proxy) => {
- let obfs_opts;
- let tls_opts;
- switch (proxy.type) {
- case 'ss':
- obfs_opts = "";
- if (proxy.plugin === 'obfs') {
- obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`;
- }
- if (proxy.plugin === 'v2ray-plugin') {
- const {tls, host, path} = proxy['plugin-opts'];
- obfs_opts = `,obfs=${tls ? 'wss' : 'ws'},obfs-host=${host}${path ? ',obfs-uri=' + path : ""}`;
- }
- return `shadowsocks = ${proxy.server}:${proxy.port}, method=${proxy.cipher}, password=${proxy.password}${obfs_opts}${proxy.tfo ? ", fast-open=true" : ", fast-open=false"}${proxy.udp ? ", udp-relay=true" : ", udp-relay=false"}, tag=${proxy.name}`
- case 'ssr':
- return `shadowsocks=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.password},ssr-protocol=${proxy.protocol}${proxy['protocol-param'] ? ",ssr-protocol-param=" + proxy['protocol-param'] : ""}${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${proxy['obfs-param'] ? ",obfs-host=" + proxy['obfs-param'] : ""}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'vmess':
- obfs_opts = "";
- if (proxy.network === 'ws') {
- // websocket
- if (proxy.tls) {
- // ws-tls
- obfs_opts = `,obfs=wss,obfs-host=${proxy.sni}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""},tls-verification=${proxy.scert ? "false" : "true"}`;
- } else {
- // ws
- obfs_opts = `,obfs=ws,obfs-host=${proxy['ws-headers'].Host}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""}`;
- }
- } else {
- // tcp
- if (proxy.tls) {
- obfs_opts = `,obfs=over-tls,obfs-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}`;
- }
- }
- return `vmess=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.uuid}${obfs_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'trojan':
- return `trojan=${proxy.server}:${proxy.port},password=${proxy.password},tls-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
- case 'http':
- tls_opts = "";
- if (proxy.tls) {
- tls_opts = `,over-tls=true,tls-verification=${proxy.scert ? "false" : "true"},tls-host=${proxy.sni}`;
- }
- return `http=${proxy.server}:${proxy.port},username=${proxy.username},password=${proxy.password}${tls_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"},tag=${proxy.name}`;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output};
-}
-
-function Loon_Producer() {
- const targetPlatform = "Loon";
- const output = (proxy) => {
- let obfs_opts, tls_opts;
- switch (proxy.type) {
- case "ss":
- obfs_opts = ",,";
- if (proxy.plugin === 'obfs') {
- const {mode, host} = proxy['plugin-opts'];
- obfs_opts = `,${mode},${host}`
- }
- return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password}${obfs_opts}`;
- case "ssr":
- return `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password},${proxy.protocol},{${proxy['protocol-param']}},${proxy.obfs},{${proxy['obfs-param']}}`
- case "vmess":
- obfs_opts = "";
- if (proxy.network === 'ws') {
- const host = proxy['ws-headers'].Host;
- obfs_opts = `,transport:ws,host:${host},path:${proxy['ws-path']}`;
- } else {
- obfs_opts = `,transport:tcp`;
- }
- if (proxy.tls) {
- obfs_opts += `,tls-name=${proxy.sni},skip-cert-verify:${proxy.scert}`;
- }
- return `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},over-tls:${proxy.tls}${obfs_opts}`;
- case "trojan":
- return `${proxy.name}=trojan,${proxy.server},${proxy.port},${proxy.password},tls-name:${proxy.sni},skip-cert-verify:${proxy.scert}`;
- case "http":
- tls_opts = "";
- const base = `${proxy.name}=${proxy.tls ? 'http' : 'https'},${proxy.server},${proxy.port},${proxy.username || ""},${proxy.password || ""}`;
- if (proxy.tls) {
- // https
- tls_opts = `,skip-cert-verify:${proxy.scert},tls-name:${proxy.sni}`;
- return base + tls_opts;
- } else return base;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output}
-}
-
-function Surge_Producer() {
- const targetPlatform = "Surge";
- const output = (proxy) => {
- let obfs_opts, tls_opts;
- switch (proxy.type) {
- case 'ss':
- obfs_opts = "";
- if (proxy.plugin === "obfs") {
- obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`
- } else {
- throw new Error(`Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}`);
- }
- return `${proxy.name}=ss,${proxy.server},${proxy.port},encrypt-method=${proxy.cipher},password=${proxy.password}${obfs_opts},tfo=${proxy.tfo || 'false'},udp-relay=${proxy.udp || 'false'}`;
- case 'vmess':
- tls_opts = "";
- let config = `${proxy.name}=vmess,${proxy.server},${proxy.port},username=${proxy.uuid},tls=${proxy.tls},tfo=${proxy.tfo || "false"}`;
- if (proxy.network === 'ws') {
- const path = proxy['ws-path'];
- const host = proxy['ws-headers'].Host;
- config += `,ws=true${path ? ',ws-path=' + path : ""}${host ? ',ws-headers=HOST:' + host : ""}`;
- }
- if (proxy.tls) {
- config += `,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
- }
- return config;
- case 'trojan':
- return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${proxy.password},sni=${proxy.sni},tfo=${proxy.tfo || 'false'}`;
- case 'http':
- tls_opts = ",tls=false";
- if (proxy.tls) {
- tls_opts = `,tls=true,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
- }
- return `${proxy.name}=http,${proxy.server},${proxy.port}${proxy.username ? ",username=" + proxy.username : ""}${proxy.password ? ",password=" + proxy.password : ""}${tls_opts},tfo=${proxy.tfo || 'false'}`;
- }
- throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
- }
- return {targetPlatform, output};
-}
-
-function Raw_Producer() {
- const targetPlatform = "Raw";
- const output = (proxy) => {
- return JSON.stringify(proxy);
- }
- return {targetPlatform, output};
-}
-
-/**************************** Operators ***************************************/
-// force to set some properties (e.g., scert, udp, tfo, etc.)
-function SetPropertyOperator(key, val) {
- return {
- name: "Set Property Operator",
- func: proxies => {
- return proxies.map(p => {
- p[key] = val;
- return p;
- })
- }
- }
-}
-
-// add or remove flag for proxies
-function FlagOperator(type) {
- return {
- name: "Flag Operator",
- func: proxies => {
- return proxies.map(proxy => {
- switch (type) {
- case 0:
- // no flag
- proxy.name = removeFlag(proxy.name);
- break
- case 1:
- // get flag
- const newFlag = getFlag(proxy.name);
- // remove old flag
- proxy.name = removeFlag(proxy.name);
- proxy.name = newFlag + " " + proxy.name;
- proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳");
- break;
- default:
- throw new Error("Unknown flag type: " + type);
- }
- return proxy;
- })
- }
- }
-}
-
-// sort proxies according to their names
-function SortOperator(order = 'asc') {
- return {
- name: "Sort Operator",
- func: proxies => {
- switch (order) {
- case "asc":
- case 'desc':
- return proxies.sort((a, b) => {
- let res = (a.name > b.name) ? 1 : -1;
- res *= order === 'desc' ? -1 : 1;
- return res
- })
- case 'random':
- return shuffle(proxies);
- default:
- throw new Error("Unknown sort option: " + order);
- }
- }
- }
-}
-
-// sort by keywords
-function KeywordSortOperator(...keywords) {
- return {
- name: "Keyword Sort Operator",
- func: proxies => proxies.sort((a, b) => {
- const oA = getKeywordOrder(keywords, a.name);
- const oB = getKeywordOrder(keywords, b.name);
- if (oA && !oB) return -1;
- if (oB && !oA) return 1;
- if (oA && oB) return oA < oB ? -1 : 1;
- if ((!oA && !oB) || (oA && oB && oA === oB)) return a.name < b.name ? -1 : 1; // fallback to normal sort
- })
- }
-}
-
-function getKeywordOrder(keywords, str) {
- let order = null;
- for (let i = 0; i < keywords.length; i++) {
- if (str.indexOf(keywords[i]) !== -1) {
- order = i + 1; // plus 1 is important! 0 will be treated as false!!!
- break;
- }
- }
- return order;
-}
-
-// rename by keywords
-// keywords: [{old: "old", now: "now"}]
-function KeywordRenameOperator(...keywords) {
- return {
- name: "Keyword Rename Operator",
- func: proxies => {
- return proxies.map(proxy => {
- for (const {old, now} of keywords) {
- proxy.name = proxy.name.replace(old, now);
- }
- return proxy;
- })
- }
- }
-}
-
-// rename by regex
-// keywords: [{expr: "string format regex", now: "now"}]
-function RegexRenameOperator(...regex) {
- return {
- name: "Regex Rename Operator",
- func: proxies => {
- return proxies.map(proxy => {
- for (const {expr, now} of regex) {
- proxy.name = proxy.name.replace(new RegExp(expr, "g"), now);
- }
- return proxy;
- })
- }
- }
-}
-
-// delete keywords operator
-// keywords: ['a', 'b', 'c']
-function KeywordDeleteOperator(...keywords) {
- const keywords_ = keywords.map(k => {
- return {
- old: k,
- now: ""
- }
- })
- return {
- name: "Keyword Delete Operator",
- func: KeywordRenameOperator(keywords_).func
- }
-}
-
-// delete regex operator
-// regex: ['a', 'b', 'c']
-function RegexDeleteOperator(...regex) {
- const regex_ = regex.map(r => {
- return {
- expr: r,
- now: ""
- }
- });
- return {
- name: "Regex Delete Operator",
- func: RegexRenameOperator(regex_).func
- }
-}
-
-// use base64 encoded script to rename
-/** Example script
- function func(proxies) {
- // do something
- return proxies;
- }
-
- WARNING:
- 1. This function name should be `func`!
- 2. Always declare variable before using it!
- */
-function ScriptOperator(script) {
- return {
- name: "Script Operator",
- func: (proxies) => {
- ;(function () {
- eval(script);
- return func(proxies);
- })();
- }
- }
-}
-
-/**************************** Filters ***************************************/
-// filter by keywords
-function KeywordFilter(...keywords) {
- return {
- name: "Keyword Filter",
- func: (proxies) => {
- return proxies.map(proxy => keywords.some(k => proxy.name.indexOf(k) !== -1));
- }
- }
-}
-
-function DiscardKeywordFilter(...keywords) {
- return {
- name: "Discard Keyword Filter",
- func: proxies => {
- const filter = KeywordFilter(keywords).func;
- return NOT(filter(proxies));
- }
- }
-}
-
-// filter useless proxies
-function UselessFilter() {
- const KEYWORDS = ["流量", "时间", "应急", "过期", "Bandwidth", "expire"];
- return {
- name: "Useless Filter",
- func: DiscardKeywordFilter(KEYWORDS).func
- }
-}
-
-// filter by regions
-function RegionFilter(...regions) {
- const REGION_MAP = {
- "HK": "🇭🇰",
- "TW": "🇹🇼",
- "US": "🇺🇸",
- "SG": "🇸🇬",
- "JP": "🇯🇵",
- "UK": "🇬🇧",
- "KR": "🇰🇷"
- };
- return {
- name: "Region Filter",
- func: (proxies) => {
- // this would be high memory usage
- return proxies.map(proxy => {
- const flag = getFlag(proxy.name);
- return regions.some(r => REGION_MAP[r] === flag);
- })
- }
- }
-}
-
-// filter by regex
-function RegexFilter(...regex) {
- return {
- name: "Regex Filter",
- func: (proxies) => {
- return proxies.map(proxy => regex.some(r => r.test(proxy.name)));
- }
- }
-}
-
-function DiscardRegexFilter(...regex) {
- return {
- name: "Discard Regex Filter",
- func: proxies => {
- const filter = RegexFilter(regex).func;
- return NOT(filter(proxies));
- }
- }
-}
-
-// filter by proxy types
-function TypeFilter(...types) {
- return {
- name: "Type Filter",
- func: (proxies) => {
- return proxies.map(proxy => types.some(t => proxy.type === t));
- }
- }
-}
-
-// use base64 encoded script to filter proxies
-/** Script Example
- function func(proxies) {
- const selected = FULL(proxies.length, true);
- // do something
- return selected;
- }
- WARNING:
- 1. This function name should be `func`!
- 2. Always declare variable before using it!
- */
-function ScriptFilter(script) {
- return {
- name: "Script Filter",
- func: (proxies) => {
- !(function () {
- eval(script);
- return filter(proxies);
- })();
- }
- }
-}
-
-/******************************** Utility Functions *********************************************/
-// get proxy flag according to its name
-function getFlag(name) {
- // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
- const flags = {
- "🏳️🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
- "🇦🇨": ["AC"],
- "🇦🇹": ["奥地利", "维也纳"],
- "🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
- "🇧🇪": ["BE", "比利时"],
- "🇧🇬": ["保加利亚", "Bulgaria"],
- "🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
- "🇨🇦": ["Canada", "Waterloo", "加拿大", "蒙特利尔", "温哥华", "楓葉", "枫叶", "滑铁卢", "多伦多"],
- "🇨🇭": ["瑞士", "苏黎世", "Switzerland"],
- "🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"],
- "🇩🇰": ["丹麦"],
- "🇪🇸": ["ES", "西班牙", "Spain"],
- "🇪🇺": ["EU", "欧盟", "欧罗巴"],
- "🇫🇮": ["Finland", "芬兰", "赫尔辛基"],
- "🇫🇷": ["FR", "France", "法国", "法國", "巴黎"],
- "🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"],
- "🇲🇴": ["MO", "Macao", "澳门", "CTM"],
- "🇭🇺": ["匈牙利", "Hungary"],
- "🇭🇰": ["HK", "Hongkong", "Hong Kong", "香港", "深港", "沪港", "呼港", "HKT", "HKBN", "HGC", "WTT", "CMI", "穗港", "京港", "港"],
- "🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"],
- "🇮🇪": ["Ireland", "爱尔兰", "都柏林"],
- "🇮🇳": ["India", "印度", "孟买", "Mumbai"],
- "🇰🇵": ["KP", "朝鲜"],
- "🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"],
- "🇱🇻": ["Latvia", "Latvija", "拉脱维亚"],
- "🇲🇽️": ["MEX", "MX", "墨西哥"],
- "🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"],
- "🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"],
- "🇵🇭": ["PH", "Philippines", "菲律宾"],
- "🇷🇴": ["RO", "罗马尼亚"],
- "🇷🇺": ["RU", "Russia", "俄罗斯", "俄羅斯", "伯力", "莫斯科", "圣彼得堡", "西伯利亚", "新西伯利亚", "京俄", "杭俄"],
- "🇸🇦": ["沙特", "迪拜"],
- "🇸🇪": ["SE", "Sweden"],
- "🇸🇬": ["SG", "Singapore", "新加坡", "狮城", "沪新", "京新", "泉新", "穗新", "深新", "杭新", "广新"],
- "🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"],
- "🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"],
- "🇹🇼": ["TW", "Taiwan", "台湾", "台北", "台中", "新北", "彰化", "CHT", "台", "HINET"],
- "🇺🇸": ["US", "USA", "America", "United States", "美国", "美", "京美", "波特兰", "达拉斯", "俄勒冈", "凤凰城", "费利蒙", "硅谷", "矽谷", "拉斯维加斯", "洛杉矶", "圣何塞", "圣克拉拉", "西雅图", "芝加哥", "沪美", "哥伦布", "纽约"],
- "🇻🇳": ["VN", "越南", "胡志明市"],
- "🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"],
- "🇿🇦": ["South Africa", "南非"],
- "🇦🇪": ["United Arab Emirates", "阿联酋"],
- "🇯🇵": ["JP", "Japan", "日", "日本", "东京", "大阪", "埼玉", "沪日", "穗日", "川日", "中日", "泉日", "杭日", "深日", "辽日", "广日"],
- "🇦🇷": ["AR", "阿根廷"],
- "🇳🇴": ["Norway", "挪威", "NO"],
- "🇨🇳": ["CN", "China", "回国", "中国", "江苏", "北京", "上海", "广州", "深圳", "杭州", "徐州", "青岛", "宁波", "镇江", "back"]
- };
- for (let k of Object.keys(flags)) {
- if (flags[k].some((item => name.indexOf(item) !== -1))) {
- return k;
- }
- }
- // no flag found
- const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0];
- return oldFlag || "🏴☠️";
-}
-
-// remove flag
-function removeFlag(str) {
- return str.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "").trim();
-}
-
-// clone an object
-function clone(obj) {
- return JSON.parse(JSON.stringify(obj))
-}
-
-// shuffle array
-function shuffle(array) {
- let currentIndex = array.length, temporaryValue, randomIndex;
-
- // While there remain elements to shuffle...
- while (0 !== currentIndex) {
-
- // Pick a remaining element...
- randomIndex = Math.floor(Math.random() * currentIndex);
- currentIndex -= 1;
-
- // And swap it with the current element.
- temporaryValue = array[currentIndex];
- array[currentIndex] = array[randomIndex];
- array[randomIndex] = temporaryValue;
- }
-
- return array;
-}
-
-// some logical functions for proxy filters
-function AND(...args) {
- return args.reduce((a, b) => a.map((c, i) => b[i] && c));
-}
-
-function OR(...args) {
- return args.reduce((a, b) => a.map((c, i) => b[i] || c))
-}
-
-function NOT(array) {
- return array.map(c => !c);
-}
-
-function FULL(length, bool) {
- return [...Array(length).keys()].map(() => bool);
-}
-
-// UUID
-// source: https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
-function UUID() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
-}
-
-// get platform form UA
-function getPlatformFromHeaders(headers) {
- const keys = Object.keys(headers);
- let UA = "";
- for (let k of keys) {
- if (k.match(/USER-AGENT/i)) {
- UA = headers[k];
- break;
- }
- }
- if (UA.indexOf("Quantumult%20X") !== -1) {
- return "QX";
- } else if (UA.indexOf("Surge") !== -1) {
- return "Surge";
- } else if (UA.indexOf("Decar") !== -1) {
- return "Loon";
- } else {
- // browser
- return FALL_BACK_TARGET;
- }
-}
-
-/*********************************** OpenAPI *************************************/
-// OpenAPI
-// prettier-ignore
-function ENV() {
- const isQX = typeof $task != "undefined";
- const isLoon = typeof $loon != "undefined";
- const isSurge = typeof $httpClient != "undefined" && !this.isLoon;
- const isJSBox = typeof require == "function" && typeof $jsbox != "undefined";
- const isNode = typeof require == "function" && !isJSBox;
- const isRequest = typeof $request !== "undefined";
- return {isQX, isLoon, isSurge, isNode, isJSBox, isRequest};
-}
-
-function HTTP(baseURL, defaultOptions = {}) {
- const {isQX, isLoon, isSurge} = ENV();
- const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"];
-
- function send(method, options) {
- options = typeof options === "string" ? {url: options} : options;
- options.url = baseURL ? baseURL + options.url : options.url;
- options = {...defaultOptions, ...options};
- const timeout = options.timeout;
- const events = {
- ...{
- onRequest: () => {
- },
- onResponse: (resp) => resp,
- onTimeout: () => {
- },
- },
- ...options.events,
- };
-
- events.onRequest(method, options);
-
- let worker;
- if (isQX) {
- worker = $task.fetch({method, ...options});
- } else {
- worker = new Promise((resolve, reject) => {
- const request = isSurge || isLoon ? $httpClient : require("request");
- request[method.toLowerCase()](options, (err, response, body) => {
- if (err) reject(err);
- else
- resolve({
- statusCode: response.status || response.statusCode,
- headers: response.headers,
- body,
- });
- });
- });
- }
-
- let timeoutid;
- const timer = timeout
- ? new Promise((_, reject) => {
- timeoutid = setTimeout(() => {
- events.onTimeout();
- return reject(
- `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`
- );
- }, timeout);
- })
- : null;
-
- return (timer
- ? Promise.race([timer, worker]).then((res) => {
- clearTimeout(timeoutid);
- return res;
- })
- : worker
- )
- .then((resp) => events.onResponse(resp))
- }
-
- const http = {};
- methods.forEach(
- (method) =>
- (http[method.toLowerCase()] = (options) => send(method, options))
- );
- return http;
-}
-
-function API(name = "untitled", debug = false) {
- const {isQX, isLoon, isSurge, isNode, isJSBox} = ENV();
- return new (class {
- constructor(name, debug) {
- this.name = name;
- this.debug = debug;
-
- this.http = HTTP();
- this.env = ENV();
-
- this.node = (() => {
- if (isNode) {
- const fs = require("fs");
-
- return {
- fs,
- };
- } else {
- return null;
- }
- })();
- this.initCache();
-
- const delay = (t, v) =>
- new Promise(function (resolve) {
- setTimeout(resolve.bind(null, v), t);
- });
-
- Promise.prototype.delay = function (t) {
- return this.then(function (v) {
- return delay(t, v);
- });
- };
- }
-
- // persistance
-
- // initialize cache
- initCache() {
- if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}");
- if (isLoon || isSurge)
- this.cache = JSON.parse($persistentStore.read(this.name) || "{}");
-
- if (isNode) {
- // create a json for root cache
- let fpath = "root.json";
- if (!this.node.fs.existsSync(fpath)) {
- this.node.fs.writeFileSync(
- fpath,
- JSON.stringify({}),
- {flag: "wx"},
- (err) => console.log(err)
- );
- }
- this.root = {};
-
- // create a json file with the given name if not exists
- fpath = `${this.name}.json`;
- if (!this.node.fs.existsSync(fpath)) {
- this.node.fs.writeFileSync(
- fpath,
- JSON.stringify({}),
- {flag: "wx"},
- (err) => console.log(err)
- );
- this.cache = {};
- } else {
- this.cache = JSON.parse(
- this.node.fs.readFileSync(`${this.name}.json`)
- );
- }
- }
- }
-
- // store cache
- persistCache() {
- const data = JSON.stringify(this.cache);
- if (isQX) $prefs.setValueForKey(data, this.name);
- if (isLoon || isSurge) $persistentStore.write(data, this.name);
- if (isNode) {
- this.node.fs.writeFileSync(
- `${this.name}.json`,
- data,
- {flag: "w"},
- (err) => console.log(err)
- );
- this.node.fs.writeFileSync(
- "root.json",
- JSON.stringify(this.root),
- {flag: "w"},
- (err) => console.log(err)
- );
- }
- }
-
- write(data, key) {
- this.log(`SET ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- $persistentStore.write(data, key);
- }
- if (isQX) {
- $prefs.setValueForKey(data, key);
- }
- if (isNode) {
- this.root[key] = data;
- }
- } else {
- this.cache[key] = data;
- }
- this.persistCache();
- }
-
- read(key) {
- this.log(`READ ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- return $persistentStore.read(key);
- }
- if (isQX) {
- return $prefs.valueForKey(key);
- }
- if (isNode) {
- return this.root[key];
- }
- } else {
- return this.cache[key];
- }
- }
-
- delete(key) {
- this.log(`DELETE ${key}`);
- if (key.indexOf("#") !== -1) {
- key = key.substr(1);
- if (isSurge & isLoon) {
- $persistentStore.write(null, key);
- }
- if (isQX) {
- $prefs.removeValueForKey(key);
- }
- if (isNode) {
- delete this.root[key];
- }
- } else {
- delete this.cache[key];
- }
- this.persistCache();
- }
-
- // notification
- notify(title, subtitle = "", content = "", options = {}) {
- const openURL = options["open-url"];
- const mediaURL = options["media-url"];
-
- const content_ =
- content +
- (openURL ? `\n点击跳转: ${openURL}` : "") +
- (mediaURL ? `\n多媒体: ${mediaURL}` : "");
-
- if (isQX) $notify(title, subtitle, content, options);
- if (isSurge) $notification.post(title, subtitle, content_);
- if (isLoon) $notification.post(title, subtitle, content, openURL);
- if (isNode) {
- if (isJSBox) {
- const push = require("push");
- push.schedule({
- title: title,
- body: (subtitle ? subtitle + "\n" : "") + content_,
- });
- } else {
- console.log(`${title}\n${subtitle}\n${content_}\n\n`);
- }
- }
- }
-
- // other helper functions
- log(msg) {
- if (this.debug) console.log(msg);
- }
-
- info(msg) {
- console.log(msg);
- }
-
- error(msg) {
- console.log("ERROR: " + msg);
- }
-
- wait(millisec) {
- return new Promise((resolve) => setTimeout(resolve, millisec));
- }
-
- done(value = {}) {
- if (isQX || isLoon || isSurge) {
- $done(value);
- } else if (isNode && !isJSBox) {
- if (typeof $context !== "undefined") {
- $context.headers = value.headers;
- $context.statusCode = value.statusCode;
- $context.body = value.body;
- }
- }
- }
- })(name, debug);
-}
-
-/*********************************** Mini Express *************************************/
-function express(port = 3000) {
- const {isNode} = ENV();
-
- // node support
- if (isNode) {
- const express_ = require("express");
- const bodyParser = require("body-parser");
- const app = express_();
- app.use(bodyParser.json({verify: rawBodySaver}));
- app.use(bodyParser.urlencoded({verify: rawBodySaver, extended: true}));
- app.use(bodyParser.raw({verify: rawBodySaver, type: '*/*'}));
-
- // adapter
- app.start = () => {
- app.listen(port, () => {
- console.log(`Express started on port: ${port}`);
- })
- }
- return app;
- }
-
- // route handlers
- const handlers = [];
-
- // http methods
- const METHODS_NAMES = [
- "GET",
- "POST",
- "PUT",
- "DELETE",
- "PATCH",
- "OPTIONS",
- "HEAD'",
- "ALL",
- ];
-
- // dispatch url to route
- const dispatch = (request, start = 0) => {
- let {method, url, headers, body} = request;
- method = method.toUpperCase();
- const {path, query} = extractURL(url);
- let handler = null;
- let i;
-
- for (i = start; i < handlers.length; i++) {
- if (handlers[i].method === "ALL" || method === handlers[i].method) {
- const {pattern} = handlers[i];
- if (patternMatched(pattern, path)) {
- handler = handlers[i];
- break;
- }
- }
- }
- if (handler) {
- // dispatch to next handler
- const next = () => {
- dispatch(method, url, i);
- };
- const req = {
- method, url, path, query,
- params: extractPathParams(handler.pattern, path),
- headers, body
- };
- const res = Response();
- handler.callback(req, res, next).catch(err => {
- res.status(500).json({
- status: "failed",
- message: err
- });
- });
- } else {
- // no route, return 404
- const res = Response();
- res.status("404").json({
- status: "failed",
- message: "ERROR: 404 not found"
- });
- }
- };
-
- const app = {};
-
- // attach http methods
- METHODS_NAMES.forEach((method) => {
- app[method.toLowerCase()] = (pattern, callback) => {
- // add handler
- handlers.push({method, pattern, callback});
- };
- });
-
- // chainable route
- app.route = (pattern) => {
- const chainApp = {};
- METHODS_NAMES.forEach((method) => {
- chainApp[method.toLowerCase()] = (callback) => {
- // add handler
- handlers.push({method, pattern, callback});
- return chainApp;
- };
- });
- return chainApp;
- };
-
- // start service
- app.start = () => {
- dispatch($request);
- };
-
- return app;
-
- /************************************************
- Utility Functions
- *************************************************/
- function rawBodySaver(req, res, buf, encoding) {
- if (buf && buf.length) {
- req.rawBody = buf.toString(encoding || 'utf8');
- }
- }
-
- function Response() {
- let statusCode = "200";
- const {isQX, isLoon, isSurge} = ENV();
- const headers = {
- "Content-Type": "text/plain;charset=UTF-8",
- };
- return new (class {
- status(code) {
- statusCode = code;
- return this;
- }
-
- send(body = "") {
- const response = {
- status: statusCode,
- body,
- headers,
- };
- if (isQX) {
- $done(...response);
- } else if (isLoon || isSurge) {
- $done({
- response,
- });
- }
- }
-
- end() {
- this.send();
- }
-
- html(data) {
- this.set("Content-Type", "text/html;charset=UTF-8");
- this.send(data);
- }
-
- json(data) {
- this.set("Content-Type", "application/json;charset=UTF-8");
- this.send(JSON.stringify(data));
- }
-
- set(key, val) {
- headers[key] = val;
- return this;
- }
- })();
- }
-
- function patternMatched(pattern, path) {
- if (pattern instanceof RegExp && pattern.test(path)) {
- return true;
- } else {
- // root pattern, match all
- if (pattern === "/") return true;
- // normal string pattern
- if (pattern.indexOf(":") === -1) {
- const spath = path.split("/");
- const spattern = pattern.split("/");
- for (let i = 0; i < spattern.length; i++) {
- if (spath[i] !== spattern[i]) {
- return false;
- }
- }
- return true;
- }
- // string pattern with path parameters
- else if (extractPathParams(pattern, path)) {
- return true;
- }
- }
- return false;
- }
-
- function extractURL(url) {
- // extract path
- const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
- const path = match[1] || "/";
-
- // extract query string
- const split = url.indexOf("?");
- const query = {};
- if (split !== -1) {
- let hashes = url.slice(url.indexOf("?") + 1).split("&");
- for (let i = 0; i < hashes.length; i++) {
- hash = hashes[i].split("=");
- query[hash[0]] = hash[1];
- }
- }
- return {
- path,
- query,
- };
- }
-
- function extractPathParams(pattern, path) {
- if (pattern.indexOf(":") === -1) {
- return null;
- } else {
- const params = {};
- for (let i = 0, j = 0; i < pattern.length; i++, j++) {
- if (pattern[i] === ":") {
- let key = [];
- let val = [];
- while (pattern[++i] !== "/" && i < pattern.length) {
- key.push(pattern[i]);
- }
- while (path[j] !== "/" && j < path.length) {
- val.push(path[j++]);
- }
- params[key.join("")] = val.join("");
- } else {
- if (pattern[i] !== path[j]) {
- return null;
- }
- }
- }
- return params;
- }
- }
-}
-
-/******************************** Base 64 *********************************************/
-// Base64 Coding Library
-// https://github.com/dankogai/js-base64#readme
-// Under BSD License
-function Base64Code() {
- // constants
- const b64chars
- = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- const b64tab = function (bin) {
- const t = {};
- let i = 0;
- const l = bin.length;
- for (; i < l; i++) t[bin.charAt(i)] = i;
- return t;
- }(b64chars);
- const fromCharCode = String.fromCharCode;
- // encoder stuff
- const cb_utob = function (c) {
- let cc;
- if (c.length < 2) {
- cc = c.charCodeAt(0);
- return cc < 0x80 ? c
- : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
- + fromCharCode(0x80 | (cc & 0x3f)))
- : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
- + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
- + fromCharCode(0x80 | (cc & 0x3f)));
- } else {
- cc = 0x10000
- + (c.charCodeAt(0) - 0xD800) * 0x400
- + (c.charCodeAt(1) - 0xDC00);
- return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
- + fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
- + fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
- + fromCharCode(0x80 | (cc & 0x3f)));
- }
- };
- const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
- const utob = function (u) {
- return u.replace(re_utob, cb_utob);
- };
- const cb_encode = function (ccc) {
- const padlen = [0, 2, 1][ccc.length % 3],
- ord = ccc.charCodeAt(0) << 16
- | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
- | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
- chars = [
- b64chars.charAt(ord >>> 18),
- b64chars.charAt((ord >>> 12) & 63),
- padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
- padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
- ];
- return chars.join('');
- };
- const btoa = function (b) {
- return b.replace(/[\s\S]{1,3}/g, cb_encode);
- };
- this.encode = function (u) {
- const isUint8Array = Object.prototype.toString.call(u) === '[object Uint8Array]';
- return isUint8Array ? u.toString('base64')
- : btoa(utob(String(u)));
- }
- const uriencode = function (u, urisafe) {
- return !urisafe
- ? _encode(u)
- : _encode(String(u)).replace(/[+\/]/g, function (m0) {
- return m0 === '+' ? '-' : '_';
- }).replace(/=/g, '');
- };
- const encodeURI = function (u) {
- return uriencode(u, true)
- };
- // decoder stuff
- const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
- const cb_btou = function (cccc) {
- switch (cccc.length) {
- case 4:
- const cp = ((0x07 & cccc.charCodeAt(0)) << 18)
- | ((0x3f & cccc.charCodeAt(1)) << 12)
- | ((0x3f & cccc.charCodeAt(2)) << 6)
- | (0x3f & cccc.charCodeAt(3)),
- offset = cp - 0x10000;
- return (fromCharCode((offset >>> 10) + 0xD800)
- + fromCharCode((offset & 0x3FF) + 0xDC00));
- case 3:
- return fromCharCode(
- ((0x0f & cccc.charCodeAt(0)) << 12)
- | ((0x3f & cccc.charCodeAt(1)) << 6)
- | (0x3f & cccc.charCodeAt(2))
- );
- default:
- return fromCharCode(
- ((0x1f & cccc.charCodeAt(0)) << 6)
- | (0x3f & cccc.charCodeAt(1))
- );
- }
- };
- const btou = function (b) {
- return b.replace(re_btou, cb_btou);
- };
- const cb_decode = function (cccc) {
- const len = cccc.length,
- padlen = len % 4,
- n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
- | (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
- | (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
- | (len > 3 ? b64tab[cccc.charAt(3)] : 0),
- chars = [
- fromCharCode(n >>> 16),
- fromCharCode((n >>> 8) & 0xff),
- fromCharCode(n & 0xff)
- ];
- chars.length -= [0, 0, 2, 1][padlen];
- return chars.join('');
- };
- const _atob = function (a) {
- return a.replace(/\S{1,4}/g, cb_decode);
- };
- const atob = function (a) {
- return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
- };
- const _decode = function (u) {
- return btou(_atob(u))
- };
- this.decode = function (a) {
- return _decode(
- String(a).replace(/[-_]/g, function (m0) {
- return m0 === '-' ? '+' : '/'
- })
- .replace(/[^A-Za-z0-9\+\/]/g, '')
- ).replace(/>/g, ">").replace(/</g, "<");
- };
- this.safeEncode = function (a) {
- return this.encode(a.replace(/\+/g, "-").replace(/\//g, "_"));
- }
- this.safeDecode = function (a) {
- return this.decode(a.replace(/-/g, "+").replace(/_/g, "/"));
- }
-}
\ No newline at end of file
diff --git a/sub.json b/sub.json
deleted file mode 100644
index ab3f9198d0..0000000000
--- a/sub.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "The Name of Subscription",
- "url": "The URL of Subscription",
- "process": [
- {
- "type": "Flag Operator",
- "args": [1]
- },
- {
- "type": "Useless Filter"
- },
- {
- "type": "Keyword Sort Operator",
- "args": ["HK", "TW", "JP"]
- },
- {
- "type": "Keyword Filter",
- "args": ["IEPL", "IPLC"]
- },
- {
- "type": "Region Filter",
- "args": ["HK", "TW", "JP"]
- }
- ]
-}
\ No newline at end of file
diff --git a/support.nodeseek.com_page_promotion_id=8.png b/support.nodeseek.com_page_promotion_id=8.png
new file mode 100644
index 0000000000..52abcc87ae
Binary files /dev/null and b/support.nodeseek.com_page_promotion_id=8.png differ
diff --git a/vs.code-workspace b/vs.code-workspace
new file mode 100644
index 0000000000..876a1499c0
--- /dev/null
+++ b/vs.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file