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 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
@@ -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
+ });
@@ -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 };