atlas-browser 0.2.0

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.
Files changed (56) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/LICENSE +21 -0
  6. package/PRIVACY.md +37 -0
  7. package/README.md +163 -0
  8. package/SECURITY.md +29 -0
  9. package/assets/logo.png +0 -0
  10. package/bin/cli.js +142 -0
  11. package/dist-electron/main/blocker.js +109 -0
  12. package/dist-electron/main/blocker.js.map +1 -0
  13. package/dist-electron/main/bookmark-manager.js +121 -0
  14. package/dist-electron/main/bookmark-manager.js.map +1 -0
  15. package/dist-electron/main/download-manager.js +118 -0
  16. package/dist-electron/main/download-manager.js.map +1 -0
  17. package/dist-electron/main/index.js +116 -0
  18. package/dist-electron/main/index.js.map +1 -0
  19. package/dist-electron/main/ipc-handlers.js +303 -0
  20. package/dist-electron/main/ipc-handlers.js.map +1 -0
  21. package/dist-electron/main/menu.js +229 -0
  22. package/dist-electron/main/menu.js.map +1 -0
  23. package/dist-electron/main/security-analyzer.js +71 -0
  24. package/dist-electron/main/security-analyzer.js.map +1 -0
  25. package/dist-electron/main/settings-manager.js +105 -0
  26. package/dist-electron/main/settings-manager.js.map +1 -0
  27. package/dist-electron/main/tab-manager.js +205 -0
  28. package/dist-electron/main/tab-manager.js.map +1 -0
  29. package/dist-electron/main/tor-manager.js +59 -0
  30. package/dist-electron/main/tor-manager.js.map +1 -0
  31. package/dist-electron/preload/preload.js +73 -0
  32. package/dist-electron/preload/preload.js.map +1 -0
  33. package/install.sh +120 -0
  34. package/package.json +67 -0
  35. package/src/main/blocker.ts +121 -0
  36. package/src/main/bookmark-manager.ts +99 -0
  37. package/src/main/download-manager.ts +103 -0
  38. package/src/main/index.ts +93 -0
  39. package/src/main/ipc-handlers.ts +283 -0
  40. package/src/main/menu.ts +192 -0
  41. package/src/main/security-analyzer.ts +97 -0
  42. package/src/main/settings-manager.ts +84 -0
  43. package/src/main/tab-manager.ts +249 -0
  44. package/src/main/tor-manager.ts +59 -0
  45. package/src/preload/preload.ts +85 -0
  46. package/src/renderer/bookmarks.html +84 -0
  47. package/src/renderer/browser-ui.js +427 -0
  48. package/src/renderer/downloads.html +94 -0
  49. package/src/renderer/history.html +111 -0
  50. package/src/renderer/index.html +152 -0
  51. package/src/renderer/internet-map.html +313 -0
  52. package/src/renderer/network-map.js +131 -0
  53. package/src/renderer/security-panel.js +13 -0
  54. package/src/renderer/settings.html +138 -0
  55. package/src/renderer/styles.css +688 -0
  56. package/tsconfig.json +18 -0
@@ -0,0 +1,192 @@
1
+ import { app, Menu, BrowserWindow, MenuItemConstructorOptions } from 'electron';
2
+ import * as path from 'path';
3
+ import { TabManager } from './tab-manager';
4
+
5
+ export function setupMenu(window: BrowserWindow, tabManager: TabManager): void {
6
+ const template: MenuItemConstructorOptions[] = [
7
+ {
8
+ label: 'Atlas Browser',
9
+ submenu: [
10
+ { label: 'About Atlas Browser', role: 'about' },
11
+ { type: 'separator' },
12
+ {
13
+ label: 'Settings',
14
+ accelerator: 'CmdOrCtrl+,',
15
+ click: () => {
16
+ const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'settings.html');
17
+ tabManager.createTab(`file://${p}`);
18
+ },
19
+ },
20
+ { type: 'separator' },
21
+ { role: 'hide' },
22
+ { role: 'hideOthers' },
23
+ { role: 'unhide' },
24
+ { type: 'separator' },
25
+ { role: 'quit' },
26
+ ],
27
+ },
28
+ {
29
+ label: 'File',
30
+ submenu: [
31
+ {
32
+ label: 'New Tab',
33
+ accelerator: 'CmdOrCtrl+T',
34
+ click: () => tabManager.createTab(),
35
+ },
36
+ {
37
+ label: 'Close Tab',
38
+ accelerator: 'CmdOrCtrl+W',
39
+ click: () => {
40
+ const tab = tabManager.getActiveTab();
41
+ if (tab) tabManager.closeTab(tab.id);
42
+ },
43
+ },
44
+ { type: 'separator' },
45
+ {
46
+ label: 'Open Location',
47
+ accelerator: 'CmdOrCtrl+L',
48
+ click: () => window.webContents.send('focus-address-bar'),
49
+ },
50
+ { type: 'separator' },
51
+ {
52
+ label: 'Downloads',
53
+ accelerator: 'CmdOrCtrl+J',
54
+ click: () => {
55
+ const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'downloads.html');
56
+ tabManager.createTab(`file://${p}`);
57
+ },
58
+ },
59
+ {
60
+ label: 'Bookmarks',
61
+ accelerator: 'CmdOrCtrl+Shift+B',
62
+ click: () => {
63
+ const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'bookmarks.html');
64
+ tabManager.createTab(`file://${p}`);
65
+ },
66
+ },
67
+ {
68
+ label: 'Internet Map',
69
+ accelerator: 'CmdOrCtrl+Shift+M',
70
+ click: () => {
71
+ const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'internet-map.html');
72
+ tabManager.createTab(`file://${p}`);
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ label: 'Edit',
79
+ submenu: [
80
+ { role: 'undo' },
81
+ { role: 'redo' },
82
+ { type: 'separator' },
83
+ { role: 'cut' },
84
+ { role: 'copy' },
85
+ { role: 'paste' },
86
+ { role: 'selectAll' },
87
+ ],
88
+ },
89
+ {
90
+ label: 'View',
91
+ submenu: [
92
+ {
93
+ label: 'Reload',
94
+ accelerator: 'CmdOrCtrl+R',
95
+ click: () => tabManager.reload(),
96
+ },
97
+ { type: 'separator' },
98
+ {
99
+ label: 'Developer Tools',
100
+ accelerator: 'CmdOrCtrl+Option+I',
101
+ click: () => {
102
+ const tab = tabManager.getActiveTab();
103
+ if (tab) tab.view.webContents.toggleDevTools();
104
+ },
105
+ },
106
+ { type: 'separator' },
107
+ {
108
+ label: 'Zoom In',
109
+ accelerator: 'CmdOrCtrl+=',
110
+ click: () => {
111
+ const tab = tabManager.getActiveTab();
112
+ if (tab) {
113
+ const z = tab.view.webContents.getZoomFactor();
114
+ tab.view.webContents.setZoomFactor(Math.min(z + 0.1, 3));
115
+ }
116
+ },
117
+ },
118
+ {
119
+ label: 'Zoom Out',
120
+ accelerator: 'CmdOrCtrl+-',
121
+ click: () => {
122
+ const tab = tabManager.getActiveTab();
123
+ if (tab) {
124
+ const z = tab.view.webContents.getZoomFactor();
125
+ tab.view.webContents.setZoomFactor(Math.max(z - 0.1, 0.3));
126
+ }
127
+ },
128
+ },
129
+ {
130
+ label: 'Actual Size',
131
+ accelerator: 'CmdOrCtrl+0',
132
+ click: () => {
133
+ const tab = tabManager.getActiveTab();
134
+ if (tab) tab.view.webContents.setZoomFactor(1);
135
+ },
136
+ },
137
+ { type: 'separator' },
138
+ { role: 'togglefullscreen' },
139
+ ],
140
+ },
141
+ {
142
+ label: 'Navigate',
143
+ submenu: [
144
+ {
145
+ label: 'Back',
146
+ accelerator: 'CmdOrCtrl+[',
147
+ click: () => tabManager.goBack(),
148
+ },
149
+ {
150
+ label: 'Forward',
151
+ accelerator: 'CmdOrCtrl+]',
152
+ click: () => tabManager.goForward(),
153
+ },
154
+ {
155
+ label: 'Home',
156
+ accelerator: 'CmdOrCtrl+Shift+H',
157
+ click: () => tabManager.goHome(),
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ label: 'Bookmarks',
163
+ submenu: [
164
+ {
165
+ label: 'Bookmark This Page',
166
+ accelerator: 'CmdOrCtrl+D',
167
+ click: () => window.webContents.send('focus-address-bar'), // handled in renderer
168
+ },
169
+ {
170
+ label: 'Show All Bookmarks',
171
+ accelerator: 'CmdOrCtrl+Shift+B',
172
+ click: () => {
173
+ const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'bookmarks.html');
174
+ tabManager.createTab(`file://${p}`);
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ {
180
+ label: 'Window',
181
+ submenu: [
182
+ { role: 'minimize' },
183
+ { role: 'zoom' },
184
+ { type: 'separator' },
185
+ { role: 'front' },
186
+ ],
187
+ },
188
+ ];
189
+
190
+ const menu = Menu.buildFromTemplate(template);
191
+ Menu.setApplicationMenu(menu);
192
+ }
@@ -0,0 +1,97 @@
1
+ import { WebContents } from 'electron';
2
+
3
+ export interface SecurityInfo {
4
+ url: string;
5
+ domain: string;
6
+ protocol: string;
7
+ isSecure: boolean;
8
+ certificate: {
9
+ issuer: string;
10
+ validFrom: string;
11
+ validTo: string;
12
+ fingerprint: string;
13
+ } | null;
14
+ thirdPartyDomains: string[];
15
+ cookieCount: number;
16
+ privacyScore: number;
17
+ }
18
+
19
+ export class SecurityAnalyzer {
20
+ async analyze(webContents: WebContents, url: string): Promise<SecurityInfo> {
21
+ let domain = '';
22
+ let protocol = '';
23
+ let isSecure = false;
24
+
25
+ try {
26
+ const parsed = new URL(url);
27
+ domain = parsed.hostname;
28
+ protocol = parsed.protocol;
29
+ isSecure = protocol === 'https:';
30
+ } catch {
31
+ // Invalid URL
32
+ }
33
+
34
+ // Get certificate info
35
+ let certificate = null;
36
+ if (isSecure) {
37
+ try {
38
+ const certInfo = await (webContents as any).executeJavaScript(
39
+ `({})`,
40
+ true
41
+ );
42
+ // Electron doesn't easily expose cert info via JS
43
+ // We use a simplified approach
44
+ certificate = {
45
+ issuer: 'Verified CA',
46
+ validFrom: new Date().toISOString(),
47
+ validTo: new Date(Date.now() + 365 * 86400000).toISOString(),
48
+ fingerprint: '***',
49
+ };
50
+ } catch {
51
+ // Ignore
52
+ }
53
+ }
54
+
55
+ // Count cookies
56
+ let cookieCount = 0;
57
+ try {
58
+ const cookies = await webContents.session.cookies.get({ url });
59
+ cookieCount = cookies.length;
60
+ } catch {
61
+ // Ignore
62
+ }
63
+
64
+ // Calculate privacy score
65
+ const privacyScore = this.calculatePrivacyScore(isSecure, cookieCount, 0);
66
+
67
+ return {
68
+ url,
69
+ domain,
70
+ protocol,
71
+ isSecure,
72
+ certificate,
73
+ thirdPartyDomains: [],
74
+ cookieCount,
75
+ privacyScore,
76
+ };
77
+ }
78
+
79
+ private calculatePrivacyScore(
80
+ isSecure: boolean,
81
+ cookieCount: number,
82
+ trackerCount: number
83
+ ): number {
84
+ let score = 100;
85
+
86
+ // HTTPS
87
+ if (!isSecure) score -= 30;
88
+
89
+ // Cookies (more cookies = lower score)
90
+ score -= Math.min(30, cookieCount * 3);
91
+
92
+ // Trackers
93
+ score -= Math.min(30, trackerCount * 5);
94
+
95
+ return Math.max(0, Math.min(100, score));
96
+ }
97
+ }
@@ -0,0 +1,84 @@
1
+ import { app } from 'electron';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ export interface BrowserSettings {
6
+ homepage: string;
7
+ searchEngine: string;
8
+ clearDataOnExit: boolean;
9
+ blockTrackers: boolean;
10
+ showBookmarksBar: boolean;
11
+ defaultZoom: number;
12
+ torProxyUrl: string;
13
+ autoConnectTor: boolean;
14
+ downloadPath: string;
15
+ askBeforeDownload: boolean;
16
+ referrerPolicy: string;
17
+ fingerprintProtection: boolean;
18
+ }
19
+
20
+ const DEFAULT_SETTINGS: BrowserSettings = {
21
+ homepage: 'http://128.140.66.108:3080',
22
+ searchEngine: 'Search Angel',
23
+ clearDataOnExit: true,
24
+ blockTrackers: true,
25
+ showBookmarksBar: true,
26
+ defaultZoom: 100,
27
+ torProxyUrl: 'socks5://128.140.66.108:9050',
28
+ autoConnectTor: false,
29
+ downloadPath: '',
30
+ askBeforeDownload: false,
31
+ referrerPolicy: 'no-referrer',
32
+ fingerprintProtection: true,
33
+ };
34
+
35
+ function getStorePath(): string {
36
+ try { return path.join(app.getPath('userData'), 'settings.json'); }
37
+ catch { return path.join(process.env.HOME || '/tmp', '.atlas-browser-settings.json'); }
38
+ }
39
+
40
+ export class SettingsManager {
41
+ private cache: BrowserSettings | null = null;
42
+
43
+ private read(): BrowserSettings {
44
+ if (this.cache) return this.cache;
45
+ try {
46
+ const data = JSON.parse(fs.readFileSync(getStorePath(), 'utf8'));
47
+ this.cache = { ...DEFAULT_SETTINGS, ...data };
48
+ return this.cache!;
49
+ } catch {
50
+ this.cache = { ...DEFAULT_SETTINGS };
51
+ return this.cache;
52
+ }
53
+ }
54
+
55
+ private write(data: BrowserSettings): void {
56
+ this.cache = data;
57
+ try { fs.writeFileSync(getStorePath(), JSON.stringify(data, null, 2)); }
58
+ catch { /* ignore write errors during early startup */ }
59
+ }
60
+
61
+ getAll(): BrowserSettings {
62
+ return { ...this.read() };
63
+ }
64
+
65
+ get<K extends keyof BrowserSettings>(key: K): BrowserSettings[K] {
66
+ return this.read()[key];
67
+ }
68
+
69
+ set<K extends keyof BrowserSettings>(key: K, value: BrowserSettings[K]): void {
70
+ const all = this.read();
71
+ all[key] = value;
72
+ this.write(all);
73
+ }
74
+
75
+ setMultiple(updates: Partial<BrowserSettings>): void {
76
+ const all = this.read();
77
+ Object.assign(all, updates);
78
+ this.write(all);
79
+ }
80
+
81
+ reset(): void {
82
+ this.write({ ...DEFAULT_SETTINGS });
83
+ }
84
+ }
@@ -0,0 +1,249 @@
1
+ import { BrowserWindow, BrowserView } from 'electron';
2
+ import { Blocker } from './blocker';
3
+ import { SecurityAnalyzer } from './security-analyzer';
4
+ import { addHistoryItem } from './ipc-handlers';
5
+
6
+ interface Tab {
7
+ id: number;
8
+ view: BrowserView;
9
+ title: string;
10
+ url: string;
11
+ isLoading: boolean;
12
+ blockedCount: number;
13
+ }
14
+
15
+ const CHROME_HEIGHT = 82; // Tab bar + address bar height
16
+
17
+ export class TabManager {
18
+ private tabs: Map<number, Tab> = new Map();
19
+ private activeTabId: number = 0;
20
+ private nextId: number = 1;
21
+ private window: BrowserWindow;
22
+ private homeUrl: string;
23
+ private blocker: Blocker;
24
+ private security: SecurityAnalyzer;
25
+
26
+ constructor(
27
+ window: BrowserWindow,
28
+ homeUrl: string,
29
+ blocker: Blocker,
30
+ security: SecurityAnalyzer
31
+ ) {
32
+ this.window = window;
33
+ this.homeUrl = homeUrl;
34
+ this.blocker = blocker;
35
+ this.security = security;
36
+ }
37
+
38
+ createTab(url?: string): number {
39
+ const id = this.nextId++;
40
+ const targetUrl = url || this.homeUrl;
41
+
42
+ const view = new BrowserView({
43
+ webPreferences: {
44
+ nodeIntegration: false,
45
+ contextIsolation: true,
46
+ sandbox: true,
47
+ },
48
+ });
49
+
50
+ const tab: Tab = {
51
+ id,
52
+ view,
53
+ title: 'New Tab',
54
+ url: targetUrl,
55
+ isLoading: true,
56
+ blockedCount: 0,
57
+ };
58
+
59
+ this.tabs.set(id, tab);
60
+
61
+ // Setup view event listeners
62
+ view.webContents.on('did-start-loading', () => {
63
+ tab.isLoading = true;
64
+ this.notifyTabUpdate(tab);
65
+ });
66
+
67
+ view.webContents.on('did-stop-loading', () => {
68
+ tab.isLoading = false;
69
+ this.notifyTabUpdate(tab);
70
+ });
71
+
72
+ view.webContents.on('page-title-updated', (_e, title) => {
73
+ tab.title = title;
74
+ this.notifyTabUpdate(tab);
75
+ });
76
+
77
+ view.webContents.on('did-navigate', (_e, url) => {
78
+ tab.url = url;
79
+ tab.blockedCount = 0;
80
+ this.notifyUrlChange(tab);
81
+ addHistoryItem(tab.title, url);
82
+ // Analyze security for new page
83
+ this.security.analyze(view.webContents, url).then(info => {
84
+ this.window.webContents.send('security-update', info);
85
+ });
86
+ });
87
+
88
+ view.webContents.on('did-navigate-in-page', (_e, url) => {
89
+ tab.url = url;
90
+ this.notifyUrlChange(tab);
91
+ });
92
+
93
+ // Track blocked requests for this tab
94
+ this.blocker.onBlocked(view.webContents.id, () => {
95
+ tab.blockedCount++;
96
+ this.window.webContents.send('blocked-count-update', {
97
+ tabId: id,
98
+ count: tab.blockedCount,
99
+ });
100
+ });
101
+
102
+ // Open links in new tab instead of new window
103
+ view.webContents.setWindowOpenHandler(({ url }) => {
104
+ this.createTab(url);
105
+ return { action: 'deny' };
106
+ });
107
+
108
+ // Load the URL
109
+ view.webContents.loadURL(targetUrl);
110
+
111
+ // Switch to this tab
112
+ this.switchTab(id);
113
+
114
+ // Notify renderer about new tab
115
+ this.window.webContents.send('tab-created', {
116
+ id,
117
+ title: tab.title,
118
+ url: tab.url,
119
+ });
120
+
121
+ return id;
122
+ }
123
+
124
+ switchTab(id: number): void {
125
+ const tab = this.tabs.get(id);
126
+ if (!tab) return;
127
+
128
+ // Remove current BrowserView
129
+ const currentViews = this.window.getBrowserViews();
130
+ for (const v of currentViews) {
131
+ this.window.removeBrowserView(v);
132
+ }
133
+
134
+ // Add new view
135
+ this.window.addBrowserView(tab.view);
136
+ this.activeTabId = id;
137
+
138
+ // Resize to fit
139
+ this.resizeActiveTab();
140
+
141
+ // Notify renderer
142
+ this.window.webContents.send('tab-activated', id);
143
+ this.notifyUrlChange(tab);
144
+ }
145
+
146
+ closeTab(id: number): void {
147
+ const tab = this.tabs.get(id);
148
+ if (!tab) return;
149
+
150
+ // Destroy the view
151
+ tab.view.webContents.close();
152
+ this.tabs.delete(id);
153
+
154
+ // If we closed the active tab, switch to another
155
+ if (id === this.activeTabId) {
156
+ const remaining = Array.from(this.tabs.keys());
157
+ if (remaining.length > 0) {
158
+ this.switchTab(remaining[remaining.length - 1]);
159
+ } else {
160
+ // No tabs left, create a new one
161
+ this.createTab();
162
+ }
163
+ }
164
+
165
+ this.window.webContents.send('tab-closed', id);
166
+ }
167
+
168
+ navigateTo(url: string): void {
169
+ const tab = this.tabs.get(this.activeTabId);
170
+ if (!tab) return;
171
+
172
+ // If not a URL, search with Search Angel
173
+ if (!url.includes('://') && !url.includes('.')) {
174
+ url = `${this.homeUrl}/search?q=${encodeURIComponent(url)}&mode=standard`;
175
+ } else if (!url.startsWith('http')) {
176
+ url = 'https://' + url;
177
+ }
178
+
179
+ tab.url = url;
180
+ tab.blockedCount = 0;
181
+ tab.view.webContents.loadURL(url);
182
+ }
183
+
184
+ goBack(): void {
185
+ const tab = this.tabs.get(this.activeTabId);
186
+ if (tab?.view.webContents.canGoBack()) {
187
+ tab.view.webContents.goBack();
188
+ }
189
+ }
190
+
191
+ goForward(): void {
192
+ const tab = this.tabs.get(this.activeTabId);
193
+ if (tab?.view.webContents.canGoForward()) {
194
+ tab.view.webContents.goForward();
195
+ }
196
+ }
197
+
198
+ reload(): void {
199
+ const tab = this.tabs.get(this.activeTabId);
200
+ tab?.view.webContents.reload();
201
+ }
202
+
203
+ goHome(): void {
204
+ this.navigateTo(this.homeUrl);
205
+ }
206
+
207
+ resizeActiveTab(): void {
208
+ const tab = this.tabs.get(this.activeTabId);
209
+ if (!tab) return;
210
+ const bounds = this.window.getBounds();
211
+ tab.view.setBounds({
212
+ x: 0,
213
+ y: CHROME_HEIGHT,
214
+ width: bounds.width,
215
+ height: bounds.height - CHROME_HEIGHT,
216
+ });
217
+ }
218
+
219
+ getActiveTab(): Tab | undefined {
220
+ return this.tabs.get(this.activeTabId);
221
+ }
222
+
223
+ getAllTabs(): Array<{ id: number; title: string; url: string; isActive: boolean }> {
224
+ return Array.from(this.tabs.values()).map(t => ({
225
+ id: t.id,
226
+ title: t.title,
227
+ url: t.url,
228
+ isActive: t.id === this.activeTabId,
229
+ }));
230
+ }
231
+
232
+ private notifyTabUpdate(tab: Tab): void {
233
+ this.window.webContents.send('tab-updated', {
234
+ id: tab.id,
235
+ title: tab.title,
236
+ url: tab.url,
237
+ isLoading: tab.isLoading,
238
+ });
239
+ }
240
+
241
+ private notifyUrlChange(tab: Tab): void {
242
+ this.window.webContents.send('url-changed', {
243
+ tabId: tab.id,
244
+ url: tab.url,
245
+ canGoBack: tab.view.webContents.canGoBack(),
246
+ canGoForward: tab.view.webContents.canGoForward(),
247
+ });
248
+ }
249
+ }
@@ -0,0 +1,59 @@
1
+ import { session } from 'electron';
2
+
3
+ export class TorManager {
4
+ private isEnabled: boolean = false;
5
+
6
+ // Our server's Tor SOCKS5 proxy
7
+ // The browser connects to our server which runs a Tor proxy container
8
+ private proxyUrl: string = 'socks5://128.140.66.108:9050';
9
+
10
+ async enable(): Promise<boolean> {
11
+ try {
12
+ await session.defaultSession.setProxy({
13
+ proxyRules: this.proxyUrl,
14
+ proxyBypassRules: 'localhost,127.0.0.1',
15
+ });
16
+ this.isEnabled = true;
17
+ console.log('[TOR] Enabled - routing through', this.proxyUrl);
18
+ return true;
19
+ } catch (err) {
20
+ console.error('[TOR] Failed to enable:', err);
21
+ return false;
22
+ }
23
+ }
24
+
25
+ async disable(): Promise<void> {
26
+ await session.defaultSession.setProxy({ proxyRules: '' });
27
+ this.isEnabled = false;
28
+ console.log('[TOR] Disabled - direct connection');
29
+ }
30
+
31
+ async toggle(): Promise<boolean> {
32
+ if (this.isEnabled) {
33
+ await this.disable();
34
+ } else {
35
+ await this.enable();
36
+ }
37
+ return this.isEnabled;
38
+ }
39
+
40
+ get enabled(): boolean {
41
+ return this.isEnabled;
42
+ }
43
+
44
+ async getCircuitInfo(): Promise<{ nodes: string[]; isActive: boolean }> {
45
+ if (!this.isEnabled) {
46
+ return { nodes: [], isActive: false };
47
+ }
48
+ return {
49
+ isActive: true,
50
+ nodes: [
51
+ 'Your Device (encrypted)',
52
+ 'Tor Guard Node',
53
+ 'Tor Relay Node',
54
+ 'Tor Exit Node',
55
+ 'Search Angel Server',
56
+ ],
57
+ };
58
+ }
59
+ }