@qpjoy/electron-tunnel 0.1.3 → 0.1.4

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.
@@ -7,6 +7,7 @@ const AdminServer_1 = require("./admin/AdminServer");
7
7
  const registerTunnelIpc_1 = require("./ipc/registerTunnelIpc");
8
8
  const MihomoManager_1 = require("./mihomo/MihomoManager");
9
9
  const electronProxy_1 = require("./system/electronProxy");
10
+ const TUNNEL_PLUGIN_ID = 'qpjoy.electron-tunnel';
10
11
  function defaultBundledEngineDir() {
11
12
  const resourcesPath = process.resourcesPath ?? process.cwd();
12
13
  const packageDir = typeof __dirname === 'undefined' ? process.cwd() : __dirname;
@@ -38,6 +39,13 @@ function createElectronTunnel(host, options = {}) {
38
39
  (0, registerTunnelIpc_1.registerTunnelIpc)(host.ipcMain, manager, {
39
40
  afterSettingsChange: applyProxy
40
41
  });
42
+ // Self-register in the shared marketplace.db so the panel (when it shows
43
+ // up later) knows the tunnel is here. Best-effort; failure is silent —
44
+ // tunnel must keep working even if marketplace-db is missing.
45
+ registerSelfInMarketplaceDb(host.app, options).catch((err) => {
46
+ // eslint-disable-next-line no-console
47
+ console.warn('[electron-tunnel] marketplace-db self-register failed:', err);
48
+ });
41
49
  return {
42
50
  manager,
43
51
  admin,
@@ -49,3 +57,71 @@ function createElectronTunnel(host, options = {}) {
49
57
  }
50
58
  };
51
59
  }
60
+ async function registerSelfInMarketplaceDb(app, options) {
61
+ // Late require so a missing peer dep doesn't break standalone use. Use a
62
+ // computed specifier + cast so tsc never tries to resolve types for it.
63
+ const specifier = '@qpjoy/marketplace-db';
64
+ let mod;
65
+ try {
66
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-var-requires
67
+ mod = require(specifier);
68
+ }
69
+ catch {
70
+ return; // package not installed alongside tunnel — fine.
71
+ }
72
+ const userDataPath = options.userDataPath ?? app.getPath('userData');
73
+ const dbPath = mod.resolveMarketplaceDbPath(userDataPath);
74
+ const packageJson = readNearbyJson('package.json');
75
+ const manifest = readNearbyJson('plugin.manifest.json', 'dist');
76
+ const db = mod.MarketplaceDB.open(dbPath);
77
+ try {
78
+ if (db.getInstalled(TUNNEL_PLUGIN_ID))
79
+ return; // host already registered us
80
+ const version = packageJson?.version ?? '0.0.0';
81
+ db.upsertInstalled({
82
+ id: TUNNEL_PLUGIN_ID,
83
+ npm: '@qpjoy/electron-tunnel',
84
+ version,
85
+ installPath: resolveTunnelPackageRoot(),
86
+ installSource: 'standalone',
87
+ manifest: {
88
+ id: TUNNEL_PLUGIN_ID,
89
+ name: 'QPJoy Tunnel',
90
+ version,
91
+ engines: { electronPlugin: '>=0.1.0', electron: '>=28' },
92
+ permissions: manifest?.permissions ?? [],
93
+ activationEvents: ['onStartup'],
94
+ contributes: { adminPanel: { url: 'http://127.0.0.1:23456', label: 'Tunnel' } }
95
+ },
96
+ // Standalone tunnel granted itself everything; the user implicitly
97
+ // accepted by choosing to install it directly (not via marketplace).
98
+ grantedPermissions: manifest?.permissions ?? [],
99
+ state: 'active',
100
+ errorMessage: null,
101
+ marketplaceEntryId: TUNNEL_PLUGIN_ID
102
+ });
103
+ }
104
+ finally {
105
+ db.close();
106
+ }
107
+ }
108
+ function readNearbyJson(name, sub) {
109
+ const packageDir = typeof __dirname === 'undefined' ? process.cwd() : __dirname;
110
+ const candidates = sub
111
+ ? [(0, path_1.resolve)(packageDir, sub, name), (0, path_1.resolve)(packageDir, '..', sub, name)]
112
+ : [(0, path_1.resolve)(packageDir, name), (0, path_1.resolve)(packageDir, '..', name)];
113
+ for (const p of candidates) {
114
+ try {
115
+ if ((0, fs_1.existsSync)(p))
116
+ return JSON.parse((0, fs_1.readFileSync)(p, 'utf8'));
117
+ }
118
+ catch {
119
+ // try next
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ function resolveTunnelPackageRoot() {
125
+ const packageDir = typeof __dirname === 'undefined' ? process.cwd() : __dirname;
126
+ return (0, path_1.resolve)(packageDir, '..');
127
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Plugin adapter for @qpjoy/electron-plugin.
3
+ *
4
+ * The same package keeps working as a standalone npm dependency (use
5
+ * `createElectronTunnel(...)` directly). When loaded through the plugin
6
+ * host, this default export is what gets invoked instead — wrapping the
7
+ * exact same runtime behind the standard plugin contract.
8
+ *
9
+ * No new state is introduced here: the underlying MihomoManager owns its
10
+ * own SQLite + admin server, so this adapter is a ~30-line shim.
11
+ */
12
+ import type { App, IpcMain, Session } from 'electron';
13
+ /**
14
+ * Subset of the SDK types — re-declared structurally so this file does not
15
+ * take a build-time dependency on `@qpjoy/plugin-sdk`. (Tunnel still ships
16
+ * fine when installed without the plugin host present.)
17
+ */
18
+ interface PluginHostBridge {
19
+ app: App;
20
+ ipcMain: IpcMain;
21
+ session: Session;
22
+ }
23
+ type ExposedApi = Record<string, (...args: any[]) => any>;
24
+ interface PluginContextLike<S> {
25
+ host: PluginHostBridge;
26
+ settings: {
27
+ get(): S;
28
+ };
29
+ log: {
30
+ info(m: string, meta?: Record<string, unknown>): void;
31
+ };
32
+ expose(api: ExposedApi): void;
33
+ }
34
+ export interface TunnelPluginSettings {
35
+ adminPort?: number;
36
+ controllerPort?: number;
37
+ mixedPort?: number;
38
+ dnsPort?: number;
39
+ bundledEngineDir?: string;
40
+ }
41
+ declare const tunnelPlugin: {
42
+ activate(ctx: PluginContextLike<TunnelPluginSettings>): Promise<() => void>;
43
+ };
44
+ export default tunnelPlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const createElectronTunnel_1 = require("./createElectronTunnel");
4
+ const tunnelPlugin = {
5
+ async activate(ctx) {
6
+ const settings = ctx.settings.get() ?? {};
7
+ const handle = (0, createElectronTunnel_1.createElectronTunnel)({ app: ctx.host.app, ipcMain: ctx.host.ipcMain, session: ctx.host.session }, {
8
+ adminPort: settings.adminPort ?? 23456,
9
+ controllerPort: settings.controllerPort ?? 23457,
10
+ mixedPort: settings.mixedPort ?? 23458,
11
+ dnsPort: settings.dnsPort ?? 23459,
12
+ bundledEngineDir: settings.bundledEngineDir
13
+ });
14
+ // Expose an RPC surface so the host (and other plugins) can drive the
15
+ // tunnel without poking at internals. Anything not on this list is
16
+ // intentionally private — bump the surface explicitly when needed.
17
+ ctx.expose({
18
+ status: () => handle.status(),
19
+ snapshot: () => handle.manager.snapshot(),
20
+ applyProxy: () => handle.applyProxy(),
21
+ addDomainRule: async (kind, domain) => {
22
+ const rule = handle.manager.addDomainRule(kind, domain);
23
+ await handle.manager.applyRuntimeConfigChange();
24
+ return rule;
25
+ },
26
+ removeDomainRule: async (id) => {
27
+ handle.manager.removeDomainRule(id);
28
+ await handle.manager.applyRuntimeConfigChange();
29
+ },
30
+ addPreset: async (preset) => {
31
+ const rules = handle.manager.addPreset(preset);
32
+ await handle.manager.applyRuntimeConfigChange();
33
+ return rules;
34
+ },
35
+ removePreset: async (preset) => {
36
+ const count = handle.manager.removePreset(preset);
37
+ await handle.manager.applyRuntimeConfigChange();
38
+ return count;
39
+ },
40
+ onEvent: (listener) => {
41
+ handle.manager.on('event', listener);
42
+ return () => handle.manager.off('event', listener);
43
+ }
44
+ });
45
+ ctx.log.info('tunnel activated', {
46
+ ports: handle.status().ports,
47
+ mode: handle.status().mode
48
+ });
49
+ return () => handle.close();
50
+ }
51
+ };
52
+ exports.default = tunnelPlugin;
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "qpjoy.electron-tunnel",
3
+ "name": "QPJoy Tunnel",
4
+ "version": "0.1.3",
5
+ "author": "qpjoy",
6
+ "description": "Mihomo-based tunnel runtime with TUN / system-proxy / rule modes. Provides outbound networking for other plugins.",
7
+ "engines": { "electronPlugin": ">=0.1.0", "electron": ">=28" },
8
+ "permissions": [
9
+ "fs:userData",
10
+ "net:listen:23456",
11
+ "net:listen:23457",
12
+ "net:listen:23458",
13
+ "net:listen:23459",
14
+ "system:proxy",
15
+ "system:exec:mihomo",
16
+ "ui:adminPanel"
17
+ ],
18
+ "activationEvents": ["onStartup"],
19
+ "contributes": {
20
+ "adminPanel": { "url": "http://127.0.0.1:23456", "label": "Tunnel" }
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qpjoy/electron-tunnel",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Reusable QPJoy tunnel runtime and CLI for Electron apps on macOS and Linux.",
6
6
  "license": "UNLICENSED",
@@ -12,6 +12,10 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
+ "./plugin": {
16
+ "types": "./dist/plugin.d.ts",
17
+ "default": "./dist/plugin.js"
18
+ },
15
19
  "./package.json": "./package.json"
16
20
  },
17
21
  "bin": {
@@ -23,11 +27,16 @@
23
27
  "dist",
24
28
  "resources/engine"
25
29
  ],
30
+ "qpjoyPlugin": {
31
+ "specVersion": 1,
32
+ "entry": "dist/plugin.js",
33
+ "manifest": "dist/plugin.manifest.json"
34
+ },
26
35
  "publishConfig": {
27
36
  "access": "public"
28
37
  },
29
38
  "scripts": {
30
- "build": "tsc -p tsconfig.json",
39
+ "build": "tsc -p tsconfig.json && node -e \"require('fs').copyFileSync('src/plugin.manifest.json','dist/plugin.manifest.json')\"",
31
40
  "prepack": "pnpm build",
32
41
  "typecheck": "tsc -p tsconfig.json --noEmit",
33
42
  "lint": "tsc -p tsconfig.json --noEmit"
@@ -43,6 +52,9 @@
43
52
  "better-sqlite3": "^11.8.1",
44
53
  "yaml": "^2.7.0"
45
54
  },
55
+ "optionalDependencies": {
56
+ "@qpjoy/marketplace-db": ">=0.1.0"
57
+ },
46
58
  "peerDependencies": {
47
59
  "electron": ">=28"
48
60
  },