@qpjoy/tunnel-cli 0.1.5 → 0.1.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/README.md CHANGED
@@ -45,7 +45,9 @@ The command:
45
45
  - downloads the HDO manifest from `electron-server`
46
46
  - writes a local WireGuard config
47
47
  - stores local HDO state and refresh credentials
48
- - starts a system-level tunnel at boot
48
+ - starts a system-level tunnel at boot. On Linux, if `wg-quick@.service` is not
49
+ installed by the OS, the CLI writes a compatible systemd unit that uses the
50
+ bundled WireGuard tools from the npm package.
49
51
 
50
52
  Useful follow-up commands:
51
53
 
package/README.setup.md CHANGED
@@ -13,4 +13,7 @@ sudo qp-tunnel-cli server-on
13
13
  sudo qp-tunnel-cli status
14
14
 
15
15
  qp-tunnel-cli curl google.com
16
+
17
+ # 删除mac的HDO进程
18
+ qp-tunnel-cli hdo down --interface hdo-client
16
19
  ```
package/dist/hdo.js CHANGED
@@ -58,6 +58,12 @@ Enroll options:
58
58
  --install-dir PATH WireGuard engine install/cache directory
59
59
  --state-file PATH HDO client state file
60
60
  --role ROLE Metadata role. Default: internal
61
+ --direct-listener Accept direct WireGuard peers from other HDO devices
62
+ --try-direct-peers Try direct HDO device peers from observed NAT endpoints
63
+ --public-endpoint HOST:PORT
64
+ Publish this device as a direct peer endpoint
65
+ --endpoint-host HOST Direct peer endpoint host
66
+ --listen-port PORT Local WireGuard listen port for direct peers
61
67
  --rotate-key Generate a new WireGuard keypair
62
68
  --no-start Write config without starting the system tunnel
63
69
 
@@ -66,6 +72,7 @@ Environment:
66
72
  HDO_USERNAME / QPJOY_HDO_USERNAME
67
73
  HDO_PASSWORD / QPJOY_HDO_PASSWORD
68
74
  HDO_TOKEN / QPJOY_HDO_TOKEN
75
+ HDO_PUBLIC_ENDPOINT / QPJOY_HDO_PUBLIC_ENDPOINT
69
76
 
70
77
  Examples:
71
78
  qp-tunnel-cli hdo enroll \\
@@ -78,6 +85,11 @@ Examples:
78
85
 
79
86
  Notes:
80
87
  Linux writes /etc/wireguard and enables wg-quick@<interface>.
88
+ If Linux does not provide wg-quick@.service, this CLI installs a compatible
89
+ systemd unit that uses the bundled WireGuard tools from npm.
90
+ Use --direct-listener --public-endpoint HOST:PORT on reachable Internal
91
+ machines. Use --try-direct-peers only when both devices are managed by this
92
+ CLI/plugin and you accept NAT hole-punching fallback risk.
81
93
  macOS installs a LaunchDaemon and may prompt for an administrator password.
82
94
  Windows installs a WireGuard tunnel service and may show a UAC prompt.
83
95
 
@@ -110,6 +122,7 @@ async function enrollCommand(args, refreshOnly) {
110
122
  `hdo-${process.platform}-${sanitizeId((0, node_os_1.hostname)())}`;
111
123
  const label = options.label || previous.label || `${process.platform} ${(0, node_os_1.hostname)()}`;
112
124
  const keys = resolveKeypair(options, previous, installDir);
125
+ const direct = resolveDirectEndpoint(options, previous);
113
126
  const registered = await apiJson(serverUrl, auth.accessToken, '/api/v1/hdo/devices/register', {
114
127
  method: 'POST',
115
128
  body: {
@@ -125,6 +138,12 @@ async function enrollCommand(args, refreshOnly) {
125
138
  wireGuard: {
126
139
  publicKey: keys.publicKey,
127
140
  interfaceName,
141
+ preferDirectPeers: direct.preferDirectPeers,
142
+ acceptDirectPeers: direct.directListener,
143
+ directListener: direct.directListener,
144
+ endpointHost: direct.endpointHost,
145
+ listenPort: direct.listenPort,
146
+ endpoint: direct.endpoint,
128
147
  updatedAt: new Date().toISOString(),
129
148
  },
130
149
  },
@@ -137,9 +156,11 @@ async function enrollCommand(args, refreshOnly) {
137
156
  writeWireGuardConfig(configPath, (0, electron_core_wireguard_1.renderHdoClientWireGuardConfig)({
138
157
  privateKey: runtime.privateKey,
139
158
  address: runtime.address,
159
+ listenPort: direct.listenPort,
140
160
  domesticPublicKey: runtime.domesticPublicKey,
141
161
  domesticEndpoint: runtime.domesticEndpoint,
142
162
  allowedIps: runtime.allowedIps,
163
+ directPeers: runtime.directPeers,
143
164
  persistentKeepalive: 25,
144
165
  }));
145
166
  const now = new Date().toISOString();
@@ -158,6 +179,10 @@ async function enrollCommand(args, refreshOnly) {
158
179
  privateKey: keys.privateKey,
159
180
  publicKey: keys.publicKey,
160
181
  overlayIp: runtime.overlayIp,
182
+ directListener: direct.directListener,
183
+ preferDirectPeers: direct.preferDirectPeers,
184
+ endpointHost: direct.endpointHost,
185
+ listenPort: direct.listenPort,
161
186
  lastManifestGeneration: runtime.generation,
162
187
  enrolledAt: previous.enrolledAt || now,
163
188
  updatedAt: now,
@@ -182,21 +207,26 @@ function statusCommand(input) {
182
207
  const interfaceName = sanitizeInterfaceName(input.interfaceName || state.interfaceName || defaultInterfaceName);
183
208
  const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
184
209
  const installDir = resolveInstallDir(input.installDir || state.installDir);
210
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
211
+ installDir,
212
+ allowSystemFallback: true,
213
+ });
185
214
  process.stdout.write(`State file: ${stateFile}\n`);
186
215
  process.stdout.write(`Server URL: ${state.serverUrl || 'unset'}\n`);
187
216
  process.stdout.write(`Device: ${state.deviceId || 'unset'}\n`);
188
217
  process.stdout.write(`Overlay IP: ${state.overlayIp || 'unset'}\n`);
189
218
  process.stdout.write(`WireGuard config: ${configPath}\n\n`);
190
219
  if (process.platform === 'linux') {
191
- inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
192
- process.stdout.write('\n');
193
- inherit('wg', ['show', interfaceName]);
220
+ if (commandAvailable('systemctl')) {
221
+ inherit('systemctl', ['status', `wg-quick@${interfaceName}`, '--no-pager']);
222
+ process.stdout.write('\n');
223
+ }
224
+ else {
225
+ process.stdout.write('systemctl unavailable; showing WireGuard runtime status only.\n\n');
226
+ }
227
+ printWireGuardRuntimeStatus(runtime, configPath);
194
228
  return;
195
229
  }
196
- const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
197
- installDir,
198
- allowSystemFallback: true,
199
- });
200
230
  if (process.platform === 'darwin') {
201
231
  const daemon = (0, electron_core_wireguard_1.getDarwinWireGuardLaunchDaemonStatus)({ runtime, configPath });
202
232
  process.stdout.write(`LaunchDaemon: ${daemon.loaded ? 'loaded' : 'not loaded'}; running=${daemon.running}\n`);
@@ -225,7 +255,18 @@ async function downCommand(input) {
225
255
  const configPath = resolveConfigPath(input.configPath || state.configPath, interfaceName);
226
256
  const installDir = resolveInstallDir(input.installDir || state.installDir);
227
257
  if (process.platform === 'linux') {
228
- inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
258
+ const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
259
+ installDir,
260
+ allowSystemFallback: true,
261
+ });
262
+ if (commandAvailable('systemctl') && systemdUnitExists('wg-quick@.service')) {
263
+ inheritRequired('systemctl', ['disable', '--now', `wg-quick@${interfaceName}`]);
264
+ return;
265
+ }
266
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'down' });
267
+ if (!result.ok)
268
+ throw new Error(result.message);
269
+ process.stdout.write(`${result.message}\n`);
229
270
  return;
230
271
  }
231
272
  const runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
@@ -254,6 +295,8 @@ function parseEnrollOptions(args) {
254
295
  role: 'internal',
255
296
  start: true,
256
297
  rotateKey: false,
298
+ directListener: false,
299
+ preferDirectPeers: false,
257
300
  };
258
301
  for (let index = 0; index < args.length; index += 1) {
259
302
  const arg = args[index];
@@ -305,6 +348,23 @@ function parseEnrollOptions(args) {
305
348
  case '--role':
306
349
  options.role = readValue();
307
350
  break;
351
+ case '--direct-listener':
352
+ options.directListener = true;
353
+ break;
354
+ case '--try-direct-peers':
355
+ case '--prefer-direct-peers':
356
+ options.preferDirectPeers = true;
357
+ break;
358
+ case '--public-endpoint':
359
+ options.publicEndpoint = readValue();
360
+ break;
361
+ case '--endpoint-host':
362
+ case '--public-host':
363
+ options.endpointHost = readValue();
364
+ break;
365
+ case '--listen-port':
366
+ options.listenPort = parsePort(readValue(), arg);
367
+ break;
308
368
  case '--rotate-key':
309
369
  options.rotateKey = true;
310
370
  break;
@@ -449,8 +509,67 @@ function resolveKeypair(options, previous, installDir) {
449
509
  }
450
510
  return (0, electron_core_wireguard_1.generateWireGuardKeyPairWithCli)(runtime.command);
451
511
  }
512
+ function resolveDirectEndpoint(options, previous) {
513
+ const publicEndpoint = options.publicEndpoint ??
514
+ process.env.HDO_PUBLIC_ENDPOINT ??
515
+ process.env.QPJOY_HDO_PUBLIC_ENDPOINT;
516
+ const parsedEndpoint = publicEndpoint ? parseEndpoint(publicEndpoint) : {};
517
+ const endpointHost = options.endpointHost ??
518
+ process.env.HDO_ENDPOINT_HOST ??
519
+ process.env.QPJOY_HDO_ENDPOINT_HOST ??
520
+ parsedEndpoint.host ??
521
+ previous.endpointHost;
522
+ const listenPort = options.listenPort ??
523
+ parseOptionalPort(process.env.HDO_LISTEN_PORT ?? process.env.QPJOY_HDO_LISTEN_PORT) ??
524
+ parsedEndpoint.port ??
525
+ previous.listenPort;
526
+ const directListener = Boolean(options.directListener ||
527
+ publicEndpoint ||
528
+ options.endpointHost ||
529
+ options.listenPort ||
530
+ previous.directListener);
531
+ const preferDirectPeers = Boolean(options.preferDirectPeers || previous.preferDirectPeers);
532
+ return {
533
+ directListener,
534
+ preferDirectPeers,
535
+ endpointHost,
536
+ listenPort,
537
+ endpoint: endpointHost && listenPort ? `${endpointHost}:${listenPort}` : undefined,
538
+ };
539
+ }
540
+ function directPeersFromManifest(wireGuard, ownOverlayIp) {
541
+ const ownIp = ownOverlayIp.split('/')[0] || ownOverlayIp;
542
+ const rows = Array.isArray(wireGuard.directPeers) ? wireGuard.directPeers : [];
543
+ return rows.flatMap((item) => {
544
+ const row = plainObject(item);
545
+ if (!row)
546
+ return [];
547
+ const publicKey = stringField(row.publicKey);
548
+ const overlayIp = stringField(row.overlayIp);
549
+ if (!publicKey || !overlayIp || overlayIp === ownIp)
550
+ return [];
551
+ const allowedIps = stringArray(row.allowedIps);
552
+ const peer = {
553
+ name: `HDO Direct ${stringField(row.label) ?? stringField(row.id) ?? overlayIp}`,
554
+ publicKey,
555
+ allowedIps: allowedIps.length ? allowedIps : [`${overlayIp}/32`],
556
+ endpoint: stringField(row.endpoint),
557
+ persistentKeepalive: 25,
558
+ };
559
+ return [peer];
560
+ });
561
+ }
452
562
  async function startSystemTunnel(interfaceName, configPath, installDir) {
453
563
  if (process.platform === 'linux') {
564
+ const runtime = await ensureLinuxWireGuardRuntime(installDir);
565
+ if (!commandAvailable('systemctl')) {
566
+ const result = await (0, electron_core_wireguard_1.setWireGuardTunnelState)({ runtime, configPath, action: 'restart' });
567
+ if (!result.ok)
568
+ throw new Error(result.message);
569
+ return `${result.message} systemctl is unavailable, so this tunnel is not installed as a boot service.`;
570
+ }
571
+ ensureLinuxWgQuickSystemdUnit(runtime);
572
+ inheritRequired('systemctl', ['daemon-reload']);
454
573
  inheritRequired('systemctl', ['enable', `wg-quick@${interfaceName}`]);
455
574
  inheritRequired('systemctl', ['restart', `wg-quick@${interfaceName}`]);
456
575
  return `Enabled and restarted wg-quick@${interfaceName}.`;
@@ -526,6 +645,7 @@ function hdoRuntimeFromManifest(manifest, registered, privateKey) {
526
645
  domesticPublicKey,
527
646
  domesticEndpoint,
528
647
  allowedIps,
648
+ directPeers: directPeersFromManifest(wireGuard, overlayIp),
529
649
  generation: numberField(root.generation),
530
650
  };
531
651
  }
@@ -596,6 +716,138 @@ function inheritRequired(command, args) {
596
716
  const result = (0, node_child_process_1.spawnSync)(command, args, { stdio: 'inherit' });
597
717
  assertSpawnOk(command, args, result);
598
718
  }
719
+ function printWireGuardRuntimeStatus(runtime, configPath) {
720
+ process.stdout.write(`Runtime: ${runtime.method}\n`);
721
+ if (runtime.warnings.length) {
722
+ process.stdout.write(`Runtime warnings: ${runtime.warnings.join('; ')}\n`);
723
+ }
724
+ try {
725
+ const tunnel = (0, electron_core_wireguard_1.getWireGuardTunnelStatus)({ runtime, configPath });
726
+ process.stdout.write(`WireGuard active: ${tunnel.active}\n`);
727
+ if (tunnel.realInterfaceName)
728
+ process.stdout.write(`Interface: ${tunnel.realInterfaceName}\n`);
729
+ if (tunnel.peers.length)
730
+ process.stdout.write(`Peers: ${tunnel.peers.length}\n`);
731
+ if (tunnel.error)
732
+ process.stdout.write(`Status detail: ${tunnel.error}\n`);
733
+ }
734
+ catch (err) {
735
+ process.stdout.write(`WireGuard status detail: ${errorMessage(err)}\n`);
736
+ }
737
+ }
738
+ async function ensureLinuxWireGuardRuntime(installDir) {
739
+ let runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
740
+ installDir,
741
+ allowSystemFallback: true,
742
+ });
743
+ if (runtime.available)
744
+ return runtime;
745
+ const installed = installLinuxWireGuardTools();
746
+ runtime = (0, electron_core_wireguard_1.resolveWireGuardConnectionRuntime)({
747
+ installDir,
748
+ allowSystemFallback: true,
749
+ });
750
+ if (runtime.available)
751
+ return runtime;
752
+ throw new Error(installed
753
+ ? runtime.error ?? 'WireGuard runtime unavailable after installing wireguard-tools.'
754
+ : `${runtime.error ?? 'WireGuard runtime unavailable'}. Install wireguard-tools or use an npm package with the matching @qpjoy/electron-core-wireguard-engine package.`);
755
+ }
756
+ function installLinuxWireGuardTools() {
757
+ const installers = [
758
+ { probe: 'apt-get', label: 'apt-get', commands: [['apt-get', 'update'], ['apt-get', 'install', '-y', 'wireguard-tools']] },
759
+ { probe: 'dnf', label: 'dnf', commands: [['dnf', 'install', '-y', 'wireguard-tools']] },
760
+ { probe: 'yum', label: 'yum', commands: [['yum', 'install', '-y', 'epel-release'], ['yum', 'install', '-y', 'wireguard-tools']] },
761
+ { probe: 'apk', label: 'apk', commands: [['apk', 'add', '--no-cache', 'wireguard-tools']] },
762
+ { probe: 'zypper', label: 'zypper', commands: [['zypper', '--non-interactive', 'install', 'wireguard-tools']] },
763
+ { probe: 'pacman', label: 'pacman', commands: [['pacman', '-Sy', '--noconfirm', 'wireguard-tools']] },
764
+ ];
765
+ for (const installer of installers) {
766
+ if (!commandAvailable(installer.probe))
767
+ continue;
768
+ process.stdout.write(`WireGuard tools are missing; installing wireguard-tools with ${installer.label}.\n`);
769
+ for (const command of installer.commands) {
770
+ const [name, ...args] = command;
771
+ const result = (0, node_child_process_1.spawnSync)(name, args, { stdio: 'inherit' });
772
+ if (result.status !== 0) {
773
+ process.stdout.write(`wireguard-tools install step failed: ${command.join(' ')}\n`);
774
+ return false;
775
+ }
776
+ }
777
+ return true;
778
+ }
779
+ return false;
780
+ }
781
+ function ensureLinuxWgQuickSystemdUnit(runtime) {
782
+ if (systemdUnitUsable())
783
+ return;
784
+ const wgQuick = runtime.wgQuick?.command;
785
+ if (!wgQuick) {
786
+ throw new Error(runtime.error ?? 'wg-quick unavailable; cannot install systemd boot service.');
787
+ }
788
+ const unitPath = '/etc/systemd/system/wg-quick@.service';
789
+ if ((0, node_fs_1.existsSync)(unitPath)) {
790
+ throw new Error(`${unitPath} exists but wg/wg-quick is not available in PATH. Install wireguard-tools or fix the existing unit.`);
791
+ }
792
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(unitPath), { recursive: true });
793
+ (0, node_fs_1.writeFileSync)(unitPath, renderLinuxWgQuickSystemdUnit(runtime, wgQuick), { mode: 0o644 });
794
+ chmodSyncSafe(unitPath, 0o644);
795
+ process.stdout.write(`Installed ${unitPath} using bundled WireGuard tools.\n`);
796
+ }
797
+ function renderLinuxWgQuickSystemdUnit(runtime, wgQuick) {
798
+ const pathDirs = uniqueStrings([
799
+ runtime.wg.command ? (0, node_path_1.dirname)(runtime.wg.command) : '',
800
+ runtime.wgQuick?.command ? (0, node_path_1.dirname)(runtime.wgQuick.command) : '',
801
+ runtime.wireGuardGo?.command ? (0, node_path_1.dirname)(runtime.wireGuardGo.command) : '',
802
+ '/usr/local/sbin',
803
+ '/usr/local/bin',
804
+ '/usr/sbin',
805
+ '/usr/bin',
806
+ '/sbin',
807
+ '/bin',
808
+ ].filter(Boolean));
809
+ return `[Unit]
810
+ Description=WireGuard via wg-quick(8) for %I
811
+ Documentation=man:wg-quick(8) man:wg(8)
812
+ Wants=network-online.target
813
+ After=network-online.target nss-lookup.target
814
+ ConditionPathExists=/etc/wireguard/%i.conf
815
+
816
+ [Service]
817
+ Type=oneshot
818
+ RemainAfterExit=yes
819
+ Environment=${systemdQuote(`PATH=${pathDirs.join(':')}`)}
820
+ Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity
821
+ ExecStart=${systemdQuote(wgQuick)} up %i
822
+ ExecStop=${systemdQuote(wgQuick)} down %i
823
+
824
+ [Install]
825
+ WantedBy=multi-user.target
826
+ `;
827
+ }
828
+ function systemdUnitUsable() {
829
+ return systemdUnitExists('wg-quick@.service') && commandAvailable('wg') && commandAvailable('wg-quick');
830
+ }
831
+ function systemdUnitExists(unitName) {
832
+ const paths = [
833
+ `/etc/systemd/system/${unitName}`,
834
+ `/run/systemd/system/${unitName}`,
835
+ `/lib/systemd/system/${unitName}`,
836
+ `/usr/lib/systemd/system/${unitName}`,
837
+ ];
838
+ if (paths.some((path) => (0, node_fs_1.existsSync)(path)))
839
+ return true;
840
+ if (!commandAvailable('systemctl'))
841
+ return false;
842
+ const result = (0, node_child_process_1.spawnSync)('systemctl', ['cat', unitName], { stdio: 'ignore' });
843
+ return result.status === 0;
844
+ }
845
+ function commandAvailable(command) {
846
+ const result = (0, node_child_process_1.spawnSync)('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command], {
847
+ stdio: 'ignore',
848
+ });
849
+ return result.status === 0;
850
+ }
599
851
  function uninstallDarwinLaunchDaemonByInterface(interfaceName) {
600
852
  const component = sanitizeLaunchDaemonComponent(interfaceName);
601
853
  const label = `com.qpjoy.hdo.wireguard.${component}`;
@@ -650,6 +902,28 @@ function sanitizeId(value) {
650
902
  .replace(/^-+|-+$/g, '')
651
903
  .slice(0, 80) || 'device';
652
904
  }
905
+ function parseEndpoint(value) {
906
+ const trimmed = value.trim();
907
+ const index = trimmed.lastIndexOf(':');
908
+ if (index <= 0 || index === trimmed.length - 1) {
909
+ throw new Error(`Invalid --public-endpoint, expected HOST:PORT: ${value}`);
910
+ }
911
+ const host = trimmed.slice(0, index).replace(/^\[|\]$/g, '');
912
+ const port = parsePort(trimmed.slice(index + 1), '--public-endpoint');
913
+ return { host, port };
914
+ }
915
+ function parsePort(value, label) {
916
+ const port = Number(value);
917
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
918
+ throw new Error(`Invalid ${label}: ${value}`);
919
+ }
920
+ return port;
921
+ }
922
+ function parseOptionalPort(value) {
923
+ if (!value)
924
+ return undefined;
925
+ return parsePort(value, 'listen port');
926
+ }
653
927
  function tokenExpired(value) {
654
928
  if (!value)
655
929
  return false;
@@ -676,6 +950,9 @@ function sanitizeLaunchDaemonComponent(value) {
676
950
  function shellQuote(value) {
677
951
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
678
952
  }
953
+ function systemdQuote(value) {
954
+ return `"${String(value).replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
955
+ }
679
956
  function appleScriptString(value) {
680
957
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
681
958
  }
@@ -687,6 +964,9 @@ function requireRecord(value, label) {
687
964
  throw new Error(`${label} is not an object.`);
688
965
  return value;
689
966
  }
967
+ function plainObject(value) {
968
+ return isRecord(value) ? value : null;
969
+ }
690
970
  function isRecord(value) {
691
971
  return typeof value === 'object' && value !== null && !Array.isArray(value);
692
972
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/tunnel-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Global QPJoy Tunnel CLI for mihomo-client and cross-platform HDO mesh enrollment.",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -22,7 +22,7 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@qpjoy/electron-core-wireguard": "^0.1.18"
25
+ "@qpjoy/electron-core-wireguard": "^0.1.19"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.10.7"