proxlens 1.2.1
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 +161 -0
- package/bin/proxlens.js +36 -0
- package/package.json +36 -0
- package/src/main/github.js +107 -0
- package/src/main/index.js +43 -0
- package/src/main/ipc.js +78 -0
- package/src/main/proxy.js +333 -0
- package/src/main/storage.js +63 -0
- package/src/main/tunnel.js +71 -0
- package/src/preload/index.js +32 -0
- package/src/renderer/index.html +188 -0
- package/src/renderer/index.js +288 -0
- package/src/renderer/styles.css +742 -0
- package/widget/index.js +396 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Proxlens
|
|
2
|
+
|
|
3
|
+
> Share your localhost with clients instantly. Built-in feedback widget, optional password protection, zero setup.
|
|
4
|
+
|
|
5
|
+
Proxlens is an open-source Electron desktop app. It wraps your local dev server in a public HTTPS URL via Cloudflare Tunnel, injects a feedback widget into every page your client sees, and saves their feedback locally on your machine — no accounts, no hosting, no cost.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What makes it different
|
|
10
|
+
|
|
11
|
+
Most tunnel tools just give you a URL. Proxlens adds a layer that's actually useful for client work:
|
|
12
|
+
|
|
13
|
+
- **Feedback widget** — automatically injected into every page. Clients click any element, leave a comment, hit Send. No browser extensions, no separate tool, no client setup required.
|
|
14
|
+
- **Local feedback storage** — every submission is saved to a JSON file on your machine, visible in the Feedback tab, persistent across sessions. Works with zero configuration.
|
|
15
|
+
- **GitHub Issues sync** — optional. If you configure a GitHub token and repo, feedback is also posted as Issues in a private repo you own. Useful for keeping a permanent record per project.
|
|
16
|
+
- **Password protection** — optional. Set a password in Settings; clients see a branded password page before accessing the preview.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
**Requirements:** Node.js 18+, npm
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/Chiarinze/proxlens.git
|
|
26
|
+
cd proxlens
|
|
27
|
+
npm install
|
|
28
|
+
npm start
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it. No `.env` file. No API keys. No accounts. The app opens and works immediately.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## How feedback works
|
|
36
|
+
|
|
37
|
+
### Default (no configuration needed)
|
|
38
|
+
|
|
39
|
+
1. Start a tunnel on any port
|
|
40
|
+
2. Send the URL to your client
|
|
41
|
+
3. Client sees your site with a **Feedback** button in the bottom-right corner
|
|
42
|
+
4. They click it, click any element on the page, type a comment, press Send
|
|
43
|
+
5. You get a desktop notification immediately
|
|
44
|
+
6. The feedback appears in the **Feedback tab** of the app
|
|
45
|
+
7. It's saved permanently to your machine — still there the next time you open Proxlens
|
|
46
|
+
|
|
47
|
+
### With GitHub Issues (optional upgrade)
|
|
48
|
+
|
|
49
|
+
When you configure a GitHub token and private repo in Settings:
|
|
50
|
+
|
|
51
|
+
- Feedback is saved locally **and** posted as a GitHub Issue in the background
|
|
52
|
+
- The Issue includes the page URL, the element selector, and the client's comment
|
|
53
|
+
- The feedback card in the app updates to show a link to the Issue
|
|
54
|
+
- If GitHub fails for any reason, the local save is unaffected — the client never sees an error
|
|
55
|
+
|
|
56
|
+
**To set up GitHub sync:**
|
|
57
|
+
|
|
58
|
+
1. Create a private GitHub repo to collect feedback (e.g. `yourname/client-feedback`)
|
|
59
|
+
2. Go to [github.com/settings/tokens/new](https://github.com/settings/tokens/new?scopes=repo&description=Proxlens), select the `repo` scope, generate the token
|
|
60
|
+
3. In Proxlens → Settings, enter the token and repo name, click Test, then Save
|
|
61
|
+
|
|
62
|
+
GitHub tokens expire. When yours expires, feedback will continue saving locally without interruption — you just won't get new Issues until you generate a fresh token and update Settings.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Password protection
|
|
67
|
+
|
|
68
|
+
1. Open Settings in the app
|
|
69
|
+
2. Toggle **Require a password** on
|
|
70
|
+
3. Type any password you want your client to use
|
|
71
|
+
4. Click Save
|
|
72
|
+
|
|
73
|
+
Clients see a simple password page before accessing the preview. The check happens locally in the proxy — the password never leaves your machine.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Client Browser
|
|
81
|
+
│ HTTPS
|
|
82
|
+
▼
|
|
83
|
+
Cloudflare Quick Tunnel (free, temporary *.trycloudflare.com URL)
|
|
84
|
+
│
|
|
85
|
+
▼
|
|
86
|
+
Proxlens Proxy (localhost:19xxx — runs silently on your machine)
|
|
87
|
+
├─ Password gate (if enabled)
|
|
88
|
+
├─ Injects feedback widget into all HTML responses
|
|
89
|
+
├─ Handles /feedback submissions:
|
|
90
|
+
│ 1. Saves to local JSON file immediately
|
|
91
|
+
│ 2. Posts to GitHub Issues in background (if configured)
|
|
92
|
+
└─ Tracks visits → desktop + in-app notifications
|
|
93
|
+
│
|
|
94
|
+
▼
|
|
95
|
+
Your Dev Server (localhost:3000 or whichever port you set)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Privacy & security
|
|
101
|
+
|
|
102
|
+
- Settings (password, GitHub token) are stored in your OS user data folder via Electron's `app.getPath('userData')`. They are never uploaded anywhere.
|
|
103
|
+
- Feedback is stored locally in the same folder as a plain JSON file.
|
|
104
|
+
- Your GitHub token is only ever used to call `POST /repos/{owner}/{repo}/issues` on the repo you specify. It is never logged or transmitted to any Proxlens server (there is no Proxlens server).
|
|
105
|
+
- The tunnel URL changes on every session. When you click Stop, the URL is dead instantly.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Exporting feedback
|
|
110
|
+
|
|
111
|
+
In the Feedback tab, click **Export JSON**. This opens the raw `proxlens-feedback.json` file from your user data folder. You can copy it, import it into a spreadsheet, or share it with a client directly.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
Contributions are welcome. To keep the codebase stable, all changes go through Pull Requests.
|
|
118
|
+
|
|
119
|
+
**How to contribute:**
|
|
120
|
+
|
|
121
|
+
1. Fork the repo
|
|
122
|
+
2. Create a branch: `git checkout -b feature/your-feature-name`
|
|
123
|
+
3. Make your changes
|
|
124
|
+
4. Commit with a clear message: `git commit -m "add: description of what you did"`
|
|
125
|
+
5. Push to your fork: `git push origin feature/your-feature-name`
|
|
126
|
+
6. Open a Pull Request describing what you changed and why
|
|
127
|
+
|
|
128
|
+
**Before submitting a PR:**
|
|
129
|
+
- Open an issue first for anything beyond small bug fixes, so we can discuss it before you invest time building it
|
|
130
|
+
- Keep PRs focused — one change per PR is easier to review than many changes bundled together
|
|
131
|
+
- Test that `npm start` runs cleanly before submitting
|
|
132
|
+
|
|
133
|
+
**Project structure:**
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
proxlens/
|
|
137
|
+
├── src/
|
|
138
|
+
│ ├── main/
|
|
139
|
+
│ │ ├── index.js # App entry — window creation only
|
|
140
|
+
│ │ ├── ipc.js # All IPC handlers in one place
|
|
141
|
+
│ │ ├── proxy.js # HTTP proxy, widget injection, password gate
|
|
142
|
+
│ │ ├── tunnel.js # Cloudflare tunnel lifecycle
|
|
143
|
+
│ │ ├── storage.js # Settings + feedback read/write
|
|
144
|
+
│ │ └── github.js # GitHub API calls only
|
|
145
|
+
│ ├── renderer/
|
|
146
|
+
│ │ ├── index.html # Clean HTML shell
|
|
147
|
+
│ │ ├── index.js # All UI logic
|
|
148
|
+
│ │ └── styles.css # All styles
|
|
149
|
+
│ └── preload/
|
|
150
|
+
│ └── index.js # Context bridge
|
|
151
|
+
├── widget/
|
|
152
|
+
│ └── index.js # Injected client widget
|
|
153
|
+
├── package.json
|
|
154
|
+
└── README.md
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/bin/proxlens.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proxlens CLI launcher.
|
|
5
|
+
* Invoked when the user runs `proxlens` in their terminal.
|
|
6
|
+
* Finds the Electron binary from the local install and launches the app.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
|
|
12
|
+
let electronBin;
|
|
13
|
+
try {
|
|
14
|
+
electronBin = require('electron');
|
|
15
|
+
} catch (_) {
|
|
16
|
+
console.error(
|
|
17
|
+
'\n Proxlens could not find the Electron runtime.\n' +
|
|
18
|
+
' Try reinstalling: npm install -g proxlens\n'
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// __dirname is .../node_modules/proxlens/bin
|
|
24
|
+
// The app root is one level up
|
|
25
|
+
const appRoot = path.join(__dirname, '..');
|
|
26
|
+
|
|
27
|
+
const proc = spawn(electronBin, [appRoot], {
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
windowsHide: false
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
proc.on('close', code => process.exit(code ?? 0));
|
|
33
|
+
proc.on('error', err => {
|
|
34
|
+
console.error('\n Failed to launch Proxlens:', err.message, '\n');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "proxlens",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "Share your localhost with clients. Built-in feedback widget + password protection.",
|
|
5
|
+
"main": "src/main/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"proxlens": "bin/proxlens.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"widget/index.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "electron .",
|
|
17
|
+
"dev": "electron . --dev",
|
|
18
|
+
"build:widget": "node widget/build.js"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"localhost",
|
|
25
|
+
"tunnel",
|
|
26
|
+
"client-preview",
|
|
27
|
+
"feedback",
|
|
28
|
+
"cloudflare",
|
|
29
|
+
"electron"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"cloudflared": "0.7.1",
|
|
34
|
+
"electron": "40.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post a feedback entry as a GitHub Issue.
|
|
5
|
+
* Returns { ok, issueUrl } or { ok: false, error }.
|
|
6
|
+
*/
|
|
7
|
+
function postIssue(entry, settings) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const { githubToken, githubRepo } = settings;
|
|
10
|
+
if (!githubToken || !githubRepo) {
|
|
11
|
+
return resolve({ ok: false, error: 'GitHub not configured' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const issueBody =
|
|
15
|
+
`**Page:** \`${entry.page}\` — ${entry.pageTitle || ''}\n` +
|
|
16
|
+
`**Element:** ${entry.elementLabel || entry.selector}\n` +
|
|
17
|
+
(entry.selector ? `**Selector:** \`${entry.selector}\`\n` : '') +
|
|
18
|
+
`\n**Feedback:**\n${entry.comment}`;
|
|
19
|
+
|
|
20
|
+
const payload = JSON.stringify({
|
|
21
|
+
title: `[Feedback] ${entry.pageTitle || entry.page}`,
|
|
22
|
+
body: issueBody,
|
|
23
|
+
labels: ['client-feedback']
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const [owner, repo] = githubRepo.split('/');
|
|
27
|
+
const options = {
|
|
28
|
+
hostname: 'api.github.com',
|
|
29
|
+
path: `/repos/${owner}/${repo}/issues`,
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
34
|
+
'Authorization': `Bearer ${githubToken}`,
|
|
35
|
+
'User-Agent': 'Proxlens-App',
|
|
36
|
+
'Accept': 'application/vnd.github+json'
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const req = https.request(options, (res) => {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
res.on('data', c => chunks.push(c));
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
46
|
+
if (res.statusCode === 201) {
|
|
47
|
+
resolve({ ok: true, issueUrl: data.html_url });
|
|
48
|
+
} else {
|
|
49
|
+
resolve({ ok: false, error: data.message || 'GitHub API error' });
|
|
50
|
+
}
|
|
51
|
+
} catch (_) {
|
|
52
|
+
resolve({ ok: false, error: 'Failed to parse GitHub response' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
req.on('error', err => resolve({ ok: false, error: err.message }));
|
|
58
|
+
req.write(payload);
|
|
59
|
+
req.end();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Test whether a token has access to a given repo.
|
|
65
|
+
* Returns { ok, name } or { ok: false, error }.
|
|
66
|
+
*/
|
|
67
|
+
function testConnection(token, repo) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const [owner, repoName] = (repo || '').split('/');
|
|
70
|
+
if (!owner || !repoName) {
|
|
71
|
+
return resolve({ ok: false, error: 'Repo must be in format: owner/repo' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const options = {
|
|
75
|
+
hostname: 'api.github.com',
|
|
76
|
+
path: `/repos/${owner}/${repoName}`,
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${token}`,
|
|
80
|
+
'User-Agent': 'Proxlens-App',
|
|
81
|
+
'Accept': 'application/vnd.github+json'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const req = https.request(options, (res) => {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
res.on('data', c => chunks.push(c));
|
|
88
|
+
res.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
91
|
+
if (res.statusCode === 200) {
|
|
92
|
+
resolve({ ok: true, name: data.full_name });
|
|
93
|
+
} else {
|
|
94
|
+
resolve({ ok: false, error: data.message || 'Repo not found or no access' });
|
|
95
|
+
}
|
|
96
|
+
} catch (_) {
|
|
97
|
+
resolve({ ok: false, error: 'Failed to parse GitHub response' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
req.on('error', err => resolve({ ok: false, error: err.message }));
|
|
103
|
+
req.end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { postIssue, testConnection };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { app, BrowserWindow } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ipc = require('./ipc');
|
|
5
|
+
const proxy = require('./proxy');
|
|
6
|
+
|
|
7
|
+
let mainWindow;
|
|
8
|
+
|
|
9
|
+
function createWindow() {
|
|
10
|
+
mainWindow = new BrowserWindow({
|
|
11
|
+
width: 640,
|
|
12
|
+
height: 860,
|
|
13
|
+
minWidth: 560,
|
|
14
|
+
minHeight: 700,
|
|
15
|
+
resizable: true,
|
|
16
|
+
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
|
17
|
+
backgroundColor: '#0c0c0e',
|
|
18
|
+
webPreferences: {
|
|
19
|
+
preload: path.join(__dirname, '../preload/index.js'),
|
|
20
|
+
contextIsolation: true,
|
|
21
|
+
nodeIntegration: false
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
|
26
|
+
|
|
27
|
+
// Give the proxy access to the window for sending IPC events
|
|
28
|
+
proxy.setMainWindow(mainWindow);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
app.whenReady().then(() => {
|
|
32
|
+
ipc.register();
|
|
33
|
+
createWindow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.on('window-all-closed', () => {
|
|
37
|
+
ipc.cleanup();
|
|
38
|
+
app.quit();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.on('activate', () => {
|
|
42
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
43
|
+
});
|
package/src/main/ipc.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { ipcMain, clipboard, shell } = require('electron');
|
|
2
|
+
|
|
3
|
+
const storage = require('./storage');
|
|
4
|
+
const github = require('./github');
|
|
5
|
+
const tunnel = require('./tunnel');
|
|
6
|
+
const proxy = require('./proxy');
|
|
7
|
+
|
|
8
|
+
let proxyServer = null;
|
|
9
|
+
let settings = {};
|
|
10
|
+
|
|
11
|
+
function getSettings() {
|
|
12
|
+
return settings;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function register(mainWindow) {
|
|
16
|
+
settings = storage.loadSettings();
|
|
17
|
+
|
|
18
|
+
// ── Settings ────────────────────────────────────────────────────────────
|
|
19
|
+
ipcMain.handle('get-settings', () => settings);
|
|
20
|
+
|
|
21
|
+
ipcMain.handle('save-settings', (_, newSettings) => {
|
|
22
|
+
settings = newSettings;
|
|
23
|
+
storage.saveSettings(newSettings);
|
|
24
|
+
return { ok: true };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
ipcMain.handle('test-github', (_, { token, repo }) => {
|
|
28
|
+
return github.testConnection(token, repo);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ── Tunnel ──────────────────────────────────────────────────────────────
|
|
32
|
+
ipcMain.handle('start-tunnel', async (_, localPort) => {
|
|
33
|
+
try {
|
|
34
|
+
// Stop any existing session first
|
|
35
|
+
tunnel.stopTunnel();
|
|
36
|
+
if (proxyServer) { proxyServer.close(); proxyServer = null; }
|
|
37
|
+
proxy.resetVisitCooldowns();
|
|
38
|
+
|
|
39
|
+
const proxyPort = await tunnel.getFreePort(19000);
|
|
40
|
+
|
|
41
|
+
proxyServer = proxy.buildProxyServer(localPort, settings);
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
proxyServer.listen(proxyPort, resolve);
|
|
44
|
+
proxyServer.on('error', reject);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return await tunnel.startTunnel(proxyPort);
|
|
48
|
+
|
|
49
|
+
} catch (err) {
|
|
50
|
+
tunnel.stopTunnel();
|
|
51
|
+
if (proxyServer) { proxyServer.close(); proxyServer = null; }
|
|
52
|
+
return { error: err.message };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
ipcMain.handle('stop-tunnel', () => {
|
|
57
|
+
tunnel.stopTunnel();
|
|
58
|
+
if (proxyServer) { proxyServer.close(); proxyServer = null; }
|
|
59
|
+
proxy.resetVisitCooldowns();
|
|
60
|
+
return { ok: true };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── Feedback storage ────────────────────────────────────────────────────
|
|
64
|
+
ipcMain.handle('get-feedback', () => storage.loadFeedback());
|
|
65
|
+
ipcMain.handle('clear-feedback', () => { storage.clearFeedback(); return { ok: true }; });
|
|
66
|
+
ipcMain.handle('export-feedback',() => storage.FEEDBACK_PATH);
|
|
67
|
+
|
|
68
|
+
// ── Utilities ───────────────────────────────────────────────────────────
|
|
69
|
+
ipcMain.handle('copy-to-clipboard', (_, text) => { clipboard.writeText(text); return true; });
|
|
70
|
+
ipcMain.handle('open-url', (_, url) => { shell.openExternal(url); return true; });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cleanup() {
|
|
74
|
+
tunnel.stopTunnel();
|
|
75
|
+
if (proxyServer) { proxyServer.close(); proxyServer = null; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { register, cleanup, getSettings };
|