@eooce/idx 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +175 -59
- package/package.json +7 -7
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const axios = require("axios");
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const fs = require("fs");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
require('dotenv').config();
|
|
9
10
|
const { promisify } = require('util');
|
|
10
11
|
const exec = promisify(require('child_process').exec);
|
|
11
12
|
const { execSync } = require('child_process');
|
|
@@ -15,24 +16,26 @@ const AUTO_ACCESS = process.env.AUTO_ACCESS || false; // false关闭自动保活
|
|
|
15
16
|
const YT_WARPOUT = process.env.YT_WARPOUT || false; // 设置为true时强制使用warp出站访问youtube,false时自动检测是否设置warp出站
|
|
16
17
|
const FILE_PATH = process.env.FILE_PATH || '.npm'; // sub.txt订阅文件路径
|
|
17
18
|
const SUB_PATH = process.env.SUB_PATH || 'sub'; // 订阅sub路径,默认为sub,例如:https://google.com/sub
|
|
18
|
-
const UUID = process.env.UUID || '
|
|
19
|
+
const UUID = process.env.UUID || '96c78932-b21d-42f9-906c-6de0d74e0e4b'; // 在不同的平台运行了v1哪吒请修改UUID,否则会覆盖
|
|
19
20
|
const NEZHA_SERVER = process.env.NEZHA_SERVER || ''; // 哪吒面板地址,v1形式:nz.serv00.net:8008 v0形式:nz.serv00.net
|
|
20
21
|
const NEZHA_PORT = process.env.NEZHA_PORT || ''; // v1哪吒请留空,v0 agent端口,当端口为{443,8443,2087,2083,2053,2096}时,自动开启tls
|
|
21
22
|
const NEZHA_KEY = process.env.NEZHA_KEY || ''; // v1的NZ_CLIENT_SECRET或v0 agwnt密钥
|
|
22
23
|
const ARGO_DOMAIN = process.env.ARGO_DOMAIN || ''; // argo固定隧道域名,留空即使用临时隧道
|
|
23
24
|
const ARGO_AUTH = process.env.ARGO_AUTH || ''; // argo固定隧道token或json,留空即使用临时隧道
|
|
24
25
|
const ARGO_PORT = process.env.ARGO_PORT || 8001; // argo固定隧道端口,使用token需在cloudflare控制台设置和这里一致,否则节点不通
|
|
26
|
+
const S5_PORT = process.env.S5_PORT || ''; // socks5端口,支持多端口的可以填写,否则留空
|
|
25
27
|
const TUIC_PORT = process.env.TUIC_PORT || ''; // tuic端口,支持多端口的可以填写,否则留空
|
|
26
28
|
const HY2_PORT = process.env.HY2_PORT || ''; // hy2端口,支持多端口的可以填写,否则留空
|
|
29
|
+
const ANYTLS_PORT = process.env.ANYTLS_PORT || ''; // AnyTLS端口,支持多端口的可以填写,否则留空
|
|
27
30
|
const REALITY_PORT = process.env.REALITY_PORT || ''; // reality端口,支持多端口的可以填写,否则留空
|
|
28
|
-
const
|
|
31
|
+
const ANYREALITY_PORT = process.env.ANYREALITY_PORT || ''; // Anyr-eality端口,支持多端口的可以填写,否则留空
|
|
32
|
+
const CFIP = process.env.CFIP || 'saas.sin.fan'; // 优选域名或优选IP
|
|
29
33
|
const CFPORT = process.env.CFPORT || 443; // 优选域名或优选IP对应端口
|
|
30
34
|
const PORT = process.env.PORT || 3000; // http订阅端口
|
|
31
35
|
const NAME = process.env.NAME || ''; // 节点名称
|
|
32
36
|
const CHAT_ID = process.env.CHAT_ID || ''; // Telegram chat_id 两个变量不全不推送节点到TG
|
|
33
37
|
const BOT_TOKEN = process.env.BOT_TOKEN || ''; // Telegram bot_token 两个变量不全不推送节点到TG
|
|
34
|
-
|
|
35
|
-
require('dotenv').config();
|
|
38
|
+
const DISABLE_ARGO = process.env.DISABLE_ARGO || false; // 设置为 true 时禁用argo,false开启
|
|
36
39
|
|
|
37
40
|
//创建运行文件夹
|
|
38
41
|
if (!fs.existsSync(FILE_PATH)) {
|
|
@@ -130,6 +133,11 @@ function cleanupOldFiles() {
|
|
|
130
133
|
|
|
131
134
|
// 获取固定隧道json
|
|
132
135
|
function argoType() {
|
|
136
|
+
if (DISABLE_ARGO === 'true' || DISABLE_ARGO === true) {
|
|
137
|
+
console.log("DISABLE_ARGO is set to true, disable argo tunnel");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
if (!ARGO_AUTH || !ARGO_DOMAIN) {
|
|
134
142
|
console.log("ARGO_DOMAIN or ARGO_AUTH variable is empty, use quick tunnels");
|
|
135
143
|
return;
|
|
@@ -436,25 +444,25 @@ eQ6OFb9LbLYL9f+sAiAffoMbi4y/0YUSlTtz7as9S8/lciBF5VCUoVIKS+vX2g==
|
|
|
436
444
|
}
|
|
437
445
|
],
|
|
438
446
|
"endpoints": [
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
447
|
+
{
|
|
448
|
+
"type": "wireguard",
|
|
449
|
+
"tag": "wireguard-out",
|
|
450
|
+
"mtu": 1280,
|
|
451
|
+
"address": [
|
|
452
|
+
"172.16.0.2/32",
|
|
453
|
+
"2606:4700:110:8dfe:d141:69bb:6b80:925/128"
|
|
454
|
+
],
|
|
455
|
+
"private_key": "YFYOAdbw1bKTHlNNi+aEjBM3BO7unuFC5rOkMRAz9XY=",
|
|
456
|
+
"peers": [
|
|
457
|
+
{
|
|
458
|
+
"address": "engage.cloudflareclient.com",
|
|
459
|
+
"port": 2408,
|
|
460
|
+
"public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=",
|
|
461
|
+
"allowed_ips": ["0.0.0.0/0", "::/0"],
|
|
462
|
+
"reserved": [78, 135, 76]
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
}
|
|
458
466
|
],
|
|
459
467
|
"outbounds": [
|
|
460
468
|
{
|
|
@@ -574,6 +582,82 @@ eQ6OFb9LbLYL9f+sAiAffoMbi4y/0YUSlTtz7as9S8/lciBF5VCUoVIKS+vX2g==
|
|
|
574
582
|
// 忽略错误,继续运行
|
|
575
583
|
}
|
|
576
584
|
|
|
585
|
+
// S5配置
|
|
586
|
+
try {
|
|
587
|
+
if (isValidPort(S5_PORT)) {
|
|
588
|
+
config.inbounds.push({
|
|
589
|
+
"tag": "s5-in",
|
|
590
|
+
"type": "socks",
|
|
591
|
+
"listen": "::",
|
|
592
|
+
"listen_port": parseInt(S5_PORT),
|
|
593
|
+
"users": [
|
|
594
|
+
{
|
|
595
|
+
"username": UUID.substring(0, 8),
|
|
596
|
+
"password": UUID.slice(-12)
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
} catch (error) {
|
|
602
|
+
// 忽略错误,继续运行
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// AnyTLS配置
|
|
606
|
+
try {
|
|
607
|
+
if (isValidPort(ANYTLS_PORT)) {
|
|
608
|
+
config.inbounds.push({
|
|
609
|
+
"tag": "anytls-in",
|
|
610
|
+
"type": "anytls",
|
|
611
|
+
"listen": "::",
|
|
612
|
+
"listen_port": parseInt(ANYTLS_PORT),
|
|
613
|
+
"users": [
|
|
614
|
+
{
|
|
615
|
+
"password": UUID
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
"tls": {
|
|
619
|
+
"enabled": true,
|
|
620
|
+
"certificate_path": path.join(FILE_PATH, "cert.pem"),
|
|
621
|
+
"key_path": path.join(FILE_PATH, "private.key")
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
} catch (error) {
|
|
626
|
+
// 忽略错误,继续运行
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// AnyReality配置
|
|
630
|
+
try {
|
|
631
|
+
if (isValidPort(ANYREALITY_PORT)) {
|
|
632
|
+
config.inbounds.push({
|
|
633
|
+
"tag": "anyreality-in",
|
|
634
|
+
"type": "anytls",
|
|
635
|
+
"listen": "::",
|
|
636
|
+
"listen_port": parseInt(ANYREALITY_PORT),
|
|
637
|
+
"users": [
|
|
638
|
+
{
|
|
639
|
+
"password": UUID
|
|
640
|
+
}
|
|
641
|
+
],
|
|
642
|
+
"tls": {
|
|
643
|
+
"enabled": true,
|
|
644
|
+
"server_name": "www.iij.ad.jp",
|
|
645
|
+
"reality": {
|
|
646
|
+
"enabled": true,
|
|
647
|
+
"handshake": {
|
|
648
|
+
"server": "www.iij.ad.jp",
|
|
649
|
+
"server_port": 443
|
|
650
|
+
},
|
|
651
|
+
"private_key": privateKey,
|
|
652
|
+
"short_id": [""]
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
// 忽略错误,继续运行
|
|
659
|
+
}
|
|
660
|
+
|
|
577
661
|
// 检测YouTube可访问性并智能配置出站规则
|
|
578
662
|
try {
|
|
579
663
|
// console.log(`YT_WARPOUT environment variable is set to: ${YT_WARPOUT}`);
|
|
@@ -703,29 +787,29 @@ eQ6OFb9LbLYL9f+sAiAffoMbi4y/0YUSlTtz7as9S8/lciBF5VCUoVIKS+vX2g==
|
|
|
703
787
|
}
|
|
704
788
|
|
|
705
789
|
// 运行cloud-fared
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
790
|
+
if (DISABLE_ARGO !== 'true' && DISABLE_ARGO !== true) {
|
|
791
|
+
if (fs.existsSync(path.join(FILE_PATH, botRandomName))) {
|
|
792
|
+
let args;
|
|
793
|
+
|
|
794
|
+
if (ARGO_AUTH.match(/^[A-Z0-9a-z=]{120,250}$/)) {
|
|
795
|
+
args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token ${ARGO_AUTH}`;
|
|
796
|
+
} else if (ARGO_AUTH.match(/TunnelSecret/)) {
|
|
797
|
+
args = `tunnel --edge-ip-version auto --config ${path.join(FILE_PATH, 'tunnel.yml')} run`;
|
|
798
|
+
} else {
|
|
799
|
+
args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile ${path.join(FILE_PATH, 'boot.log')} --loglevel info --url http://localhost:${ARGO_PORT}`;
|
|
800
|
+
}
|
|
717
801
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
802
|
+
try {
|
|
803
|
+
await execPromise(`nohup ${path.join(FILE_PATH, botRandomName)} ${args} >/dev/null 2>&1 &`);
|
|
804
|
+
console.log('bot is running');
|
|
805
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error(`Error executing command: ${error}`);
|
|
808
|
+
}
|
|
724
809
|
}
|
|
725
810
|
}
|
|
811
|
+
// 无论是否禁用 Argo,都需要生成节点信息
|
|
726
812
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
727
|
-
|
|
728
|
-
// 提取域名并生成sub.txt文件
|
|
729
813
|
await extractDomains();
|
|
730
814
|
});
|
|
731
815
|
};
|
|
@@ -784,6 +868,11 @@ function getFilesForArchitecture(architecture) {
|
|
|
784
868
|
|
|
785
869
|
// 获取临时隧道domain
|
|
786
870
|
async function extractDomains() {
|
|
871
|
+
if (DISABLE_ARGO === 'true' || DISABLE_ARGO === true) {
|
|
872
|
+
await generateLinks(null);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
787
876
|
let argoDomain;
|
|
788
877
|
|
|
789
878
|
if (ARGO_AUTH && ARGO_DOMAIN) {
|
|
@@ -842,14 +931,14 @@ async function getMetaInfo() {
|
|
|
842
931
|
try {
|
|
843
932
|
const response1 = await axios.get('https://api.ip.sb/geoip', { headers: { 'User-Agent': 'Mozilla/5.0', timeout: 3000 }});
|
|
844
933
|
if (response1.data && response1.data.country_code && response1.data.isp) {
|
|
845
|
-
return `${response1.data.country_code}
|
|
934
|
+
return `${response1.data.country_code}-${response1.data.isp}`.replace(/\s+/g, '_');
|
|
846
935
|
}
|
|
847
936
|
} catch (error) {
|
|
848
937
|
try {
|
|
849
938
|
// 备用 ip-api.com 获取isp
|
|
850
939
|
const response2 = await axios.get('http://ip-api.com/json', { headers: { 'User-Agent': 'Mozilla/5.0', timeout: 3000 }});
|
|
851
940
|
if (response2.data && response2.data.status === 'success' && response2.data.countryCode && response2.data.org) {
|
|
852
|
-
return `${response2.data.countryCode}
|
|
941
|
+
return `${response2.data.countryCode}-${response2.data.org}`.replace(/\s+/g, '_');
|
|
853
942
|
}
|
|
854
943
|
} catch (error) {
|
|
855
944
|
// console.error('Backup API also failed');
|
|
@@ -862,25 +951,36 @@ async function getMetaInfo() {
|
|
|
862
951
|
async function generateLinks(argoDomain) {
|
|
863
952
|
let SERVER_IP = '';
|
|
864
953
|
try {
|
|
865
|
-
const
|
|
866
|
-
SERVER_IP =
|
|
954
|
+
const ipv4Response = await axios.get('http://ipv4.ip.sb', { timeout: 3000 });
|
|
955
|
+
SERVER_IP = ipv4Response.data.trim();
|
|
867
956
|
} catch (err) {
|
|
868
957
|
try {
|
|
869
|
-
const response = await axios.get('https://ipv6.ip.sb', { timeout: 2000 });
|
|
870
|
-
SERVER_IP = `[${response.data.trim()}]`;
|
|
871
|
-
} catch (ipv6Err) {
|
|
872
958
|
SERVER_IP = execSync('curl -sm 3 ipv4.ip.sb').toString().trim();
|
|
959
|
+
} catch (curlErr) {
|
|
960
|
+
try {
|
|
961
|
+
const ipv6Response = await axios.get('http://ipv6.ip.sb', { timeout: 3000 });
|
|
962
|
+
SERVER_IP = `[${ipv6Response.data.trim()}]`;
|
|
963
|
+
} catch (ipv6AxiosErr) {
|
|
964
|
+
try {
|
|
965
|
+
SERVER_IP = `[${execSync('curl -sm 3 ipv6.ip.sb').toString().trim()}]`;
|
|
966
|
+
} catch (ipv6CurlErr) {
|
|
967
|
+
console.error('Failed to get IP address:', ipv6CurlErr.message);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
873
970
|
}
|
|
874
971
|
}
|
|
875
972
|
|
|
876
973
|
const ISP = await getMetaInfo();
|
|
877
974
|
const nodeName = NAME ? `${NAME}-${ISP}` : ISP;
|
|
878
|
-
|
|
879
975
|
return new Promise((resolve) => {
|
|
880
976
|
setTimeout(() => {
|
|
881
|
-
|
|
977
|
+
let subTxt = '';
|
|
882
978
|
|
|
883
|
-
|
|
979
|
+
// 只有当 DISABLE_ARGO 不为 'true' 且 argoDomain 存在时才生成默认的 vmess 节点
|
|
980
|
+
if ((DISABLE_ARGO !== 'true' && DISABLE_ARGO !== true) && argoDomain) {
|
|
981
|
+
const vmessNode = `vmess://${Buffer.from(JSON.stringify({ v: '2', ps: `${nodeName}`, add: CFIP, port: CFPORT, id: UUID, aid: '0', scy: 'auto', net: 'ws', type: 'none', host: argoDomain, path: '/vmess-argo?ed=2560', tls: 'tls', sni: argoDomain, alpn: '', fp: 'firefox'})).toString('base64')}`;
|
|
982
|
+
subTxt = vmessNode;
|
|
983
|
+
}
|
|
884
984
|
|
|
885
985
|
// TUIC_PORT是有效端口号时生成tuic节点
|
|
886
986
|
if (isValidPort(TUIC_PORT)) {
|
|
@@ -900,8 +1000,28 @@ async function generateLinks(argoDomain) {
|
|
|
900
1000
|
subTxt += vlessNode;
|
|
901
1001
|
}
|
|
902
1002
|
|
|
1003
|
+
// ANYTLS_PORT是有效端口号时生成anytls节点
|
|
1004
|
+
if (isValidPort(ANYTLS_PORT)) {
|
|
1005
|
+
const anytlsNode = `\nanytls://${UUID}@${SERVER_IP}:${ANYTLS_PORT}?security=tls&sni=${SERVER_IP}&fp=chrome&insecure=1&allowInsecure=1#${nodeName}`;
|
|
1006
|
+
subTxt += anytlsNode;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ANYREALITY_PORT是有效端口号时生成anyreality节点
|
|
1010
|
+
if (isValidPort(ANYREALITY_PORT)) {
|
|
1011
|
+
const anyrealityNode = `\nanytls://${UUID}@${SERVER_IP}:${ANYREALITY_PORT}?security=reality&sni=www.iij.ad.jp&fp=chrome&pbk=${publicKey}&type=tcp&headerType=none#${nodeName}`;
|
|
1012
|
+
subTxt += anyrealityNode;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// S5_PORT是有效端口号时生成socks5节点
|
|
1016
|
+
if (isValidPort(S5_PORT)) {
|
|
1017
|
+
const S5_AUTH = Buffer.from(`${UUID.substring(0, 8)}:${UUID.slice(-12)}`).toString('base64');
|
|
1018
|
+
const s5Node = `\nsocks://${S5_AUTH}@${SERVER_IP}:${S5_PORT}#${nodeName}`;
|
|
1019
|
+
subTxt += s5Node;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
903
1022
|
// 打印 sub.txt 内容到控制台
|
|
904
|
-
console.log(Buffer.from(subTxt).toString('base64'));
|
|
1023
|
+
console.log('\x1b[32m' + Buffer.from(subTxt).toString('base64') + '\x1b[0m'); // 输出绿色
|
|
1024
|
+
console.log('\x1b[35m' + 'Logs will be deleted in 90 seconds,you can copy the above nodes' + '\x1b[0m'); // 洋红色
|
|
905
1025
|
fs.writeFileSync(subPath, Buffer.from(subTxt).toString('base64'));
|
|
906
1026
|
fs.writeFileSync(listPath, subTxt, 'utf8');
|
|
907
1027
|
console.log(`${FILE_PATH}/sub.txt saved successfully`);
|
|
@@ -928,14 +1048,10 @@ function cleanFiles() {
|
|
|
928
1048
|
} else if (NEZHA_SERVER && NEZHA_KEY) {
|
|
929
1049
|
filesToDelete.push(phpPath);
|
|
930
1050
|
}
|
|
931
|
-
|
|
932
|
-
// 修改为使用随机文件名删除文件
|
|
933
1051
|
const filePathsToDelete = filesToDelete.map(file => {
|
|
934
|
-
// 对于已经使用随机路径的变量,直接使用
|
|
935
1052
|
if ([webPath, botPath, phpPath, npmPath].includes(file)) {
|
|
936
1053
|
return file;
|
|
937
1054
|
}
|
|
938
|
-
// 对于其他文件,使用原始路径
|
|
939
1055
|
return path.join(FILE_PATH, path.basename(file));
|
|
940
1056
|
});
|
|
941
1057
|
|
|
@@ -1048,7 +1164,7 @@ async function startserver() {
|
|
|
1048
1164
|
cleanupOldFiles();
|
|
1049
1165
|
argoType();
|
|
1050
1166
|
await downloadFilesAndRun();
|
|
1051
|
-
AddVisitTask();
|
|
1167
|
+
await AddVisitTask();
|
|
1052
1168
|
cleanFiles();
|
|
1053
1169
|
}
|
|
1054
1170
|
startserver();
|
|
@@ -1060,7 +1176,7 @@ app.get("/", async function(req, res) {
|
|
|
1060
1176
|
const data = await fs.promises.readFile(filePath, 'utf8');
|
|
1061
1177
|
res.send(data);
|
|
1062
1178
|
} catch (err) {
|
|
1063
|
-
res.send("Hello world!<br><br>You can
|
|
1179
|
+
res.send("Hello world!<br><br>You can access /{SUB_PATH}(Default: /sub) get your nodes!");
|
|
1064
1180
|
}
|
|
1065
1181
|
});
|
|
1066
1182
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eooce/idx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "@eooce/idx",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"keywords": [
|
|
10
10
|
"vmess",
|
|
11
11
|
"vless",
|
|
12
|
-
"hysteria2",
|
|
13
12
|
"tuic",
|
|
14
|
-
"reality",
|
|
15
13
|
"argo",
|
|
16
|
-
"tunnel"
|
|
14
|
+
"tunnel",
|
|
15
|
+
"reality",
|
|
16
|
+
"hysteria2"
|
|
17
17
|
],
|
|
18
18
|
"author": "eooce",
|
|
19
19
|
"license": "MIT",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"axios": "^1.
|
|
28
|
-
"
|
|
29
|
-
"
|
|
27
|
+
"axios": "^1.13.2",
|
|
28
|
+
"dotenv": "^17.2.3",
|
|
29
|
+
"express": "^5.2.1"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=14"
|