Skip to content

Commit 660e05e

Browse files
authored
Merge pull request #36 from urnetwork/fix/lint-errors
fix: clear the 12 pre-existing lint errors (1 React refs correctness fix + typing debt)
2 parents 5372d24 + fbc4ec6 commit 660e05e

8 files changed

Lines changed: 113 additions & 34 deletions

File tree

src/background/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { applyKillSwitchSetting } from "../utils/kill-switch-apply";
66
import { isAllowedOrigin } from "../utils/origins";
77
import { initBridge, notifySessionChanged, handleExtensionLocationChange } from "../bridge/background";
88
import { startSsoFlow, clearSsoState, retrieveAndValidateState } from "../utils/sso";
9+
import type { FirefoxGlobal } from "../types/firefox-webext";
910

1011
const HEALTH_ALARM_NAME = "node-health-check";
1112
const MULTI_IP_SLOTS_KEY = "multi_ip_slots";
@@ -14,11 +15,11 @@ const MULTI_IP_SLOTS_KEY = "multi_ip_slots";
1415
initBridge();
1516

1617
function isFirefox(): boolean {
17-
return Boolean((globalThis as any).browser?.proxy?.onRequest);
18+
return Boolean((globalThis as FirefoxGlobal).browser?.proxy?.onRequest);
1819
}
1920

2021
// Register Firefox proxy error listener
21-
const firefoxProxy = (globalThis as any).browser?.proxy;
22+
const firefoxProxy = (globalThis as FirefoxGlobal).browser?.proxy;
2223
if (firefoxProxy?.onError) {
2324
firefoxProxy.onError.addListener((error: { message: string }) => {
2425
console.error("Firefox proxy error:", error.message);
@@ -107,7 +108,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
107108

108109
// Firefox: restore proxy listener immediately — no need to wait for Chrome's
109110
// proxy.settings.get() to settle. The 2-second delay only benefits Chrome.
110-
if ((globalThis as any).browser?.proxy?.onRequest) {
111+
if ((globalThis as FirefoxGlobal).browser?.proxy?.onRequest) {
111112
proxyManager.restoreState().catch((err) => {
112113
console.error("Failed to restore Firefox proxy state on startup:", err);
113114
});
@@ -123,7 +124,7 @@ if ((globalThis as any).browser?.proxy?.onRequest) {
123124
// The main flow now uses chrome.identity.launchWebAuthFlow, which delivers the
124125
// auth code directly to the extension via a browser-controlled redirect URL.
125126
// This listener is kept only as a safety net for any legacy/manual tab flow.
126-
chrome.tabs.onUpdated.addListener((tabId, changeInfo, _tab) => {
127+
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
127128
const url = changeInfo.url;
128129
if (!url || !isSsoCompleteUrlLegacy(url)) return;
129130

src/types/firefox-webext.d.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* The Firefox-only WebExtensions surface this extension actually touches.
3+
*
4+
* `@types/chrome` is the only extension typing installed, and it declares
5+
* `chrome` alone -- nothing in the dependency tree declares Firefox's `browser`
6+
* global. These declarations fill that gap.
7+
*
8+
* Only the members this codebase calls are declared; nothing is asserted about
9+
* the rest of the namespace. Every member is optional because Chrome does not
10+
* define `browser` at all and Firefox's request-level proxy API is
11+
* version-dependent, so every call site feature-detects before using it.
12+
*/
13+
14+
/** One proxy entry returned from a `proxy.onRequest` listener. */
15+
export type FirefoxProxyInfo = {
16+
type: "http" | "https" | "socks" | "socks4" | "direct";
17+
host?: string;
18+
port?: number;
19+
username?: string;
20+
password?: string;
21+
failoverTimeout?: number;
22+
};
23+
24+
/** The request being proxied. Only `url` is read here. */
25+
export type FirefoxProxyDetails = {
26+
url: string;
27+
};
28+
29+
export type FirefoxProxyRequestListener = (
30+
details: FirefoxProxyDetails,
31+
) => FirefoxProxyInfo[];
32+
33+
export interface FirefoxProxyOnRequestEvent {
34+
addListener(listener: FirefoxProxyRequestListener, filter: { urls: string[] }): void;
35+
removeListener(listener: FirefoxProxyRequestListener): void;
36+
hasListener(listener: FirefoxProxyRequestListener): boolean;
37+
}
38+
39+
export interface FirefoxProxyOnErrorEvent {
40+
addListener(listener: (error: { message: string }) => void): void;
41+
}
42+
43+
export interface FirefoxProxyApi {
44+
onRequest?: FirefoxProxyOnRequestEvent;
45+
onError?: FirefoxProxyOnErrorEvent;
46+
}
47+
48+
/**
49+
* `globalThis` as seen from a Firefox WebExtension.
50+
*
51+
* Modelled as a cast target rather than a `declare global { var browser }` so
52+
* `browser` stays unreachable as a bare identifier: on Chrome it is undeclared,
53+
* and optional chaining does not guard an undeclared identifier -- `browser?.x`
54+
* would throw a ReferenceError there, while `globalThis.browser?.x` is just a
55+
* property read that yields `undefined`.
56+
*/
57+
export interface FirefoxGlobal {
58+
browser?: {
59+
proxy?: FirefoxProxyApi;
60+
};
61+
}

src/utils/connection-manager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { chromeStorageAdapter } from "./storage-adapter";
55
import { getKillSwitch } from "./kill-switch";
66
import { buildAuthParams } from "./auth-params";
77
import { clearBridgeSession } from "../bridge/session";
8+
import type { FirefoxGlobal } from "../types/firefox-webext";
89

910
const MULTI_IP_SLOTS_KEY = "multi_ip_slots";
1011

@@ -58,7 +59,7 @@ const MULTI_IP_PING_INTERVAL_S = 300;
5859
let lastPingAt = 0;
5960

6061
function isFirefox(): boolean {
61-
return Boolean((globalThis as any).browser?.proxy?.onRequest);
62+
return Boolean((globalThis as FirefoxGlobal).browser?.proxy?.onRequest);
6263
}
6364

6465
export class ConnectionManager {

src/utils/kill-switch-apply.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { buildPacScript, pacScriptToDataUrl, type PacSlot } from "./pac-script";
22
import { getSortedSlots, getStoredHealth } from "./node-health";
33
import { setKillSwitch } from "./kill-switch";
44
import { proxyManager } from "./proxy-manager";
5+
import type { FirefoxGlobal } from "../types/firefox-webext";
56

67
const MULTI_IP_SLOTS_KEY = "multi_ip_slots";
78

89
function isFirefox(): boolean {
9-
return Boolean((globalThis as any).browser?.proxy?.onRequest);
10+
return Boolean((globalThis as FirefoxGlobal).browser?.proxy?.onRequest);
1011
}
1112

1213
// Persist + apply the kill-switch setting. Shared by the popup message handler

src/utils/proxy-manager.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type { PacSlot } from "./pac-script";
22
import { shouldBypass, chromeBypassList, deviceRpcApiHost } from "./bypass-rules";
33
import { getKillSwitch } from "./kill-switch";
4+
import type {
5+
FirefoxGlobal,
6+
FirefoxProxyApi,
7+
FirefoxProxyDetails,
8+
FirefoxProxyInfo,
9+
FirefoxProxyRequestListener,
10+
} from "../types/firefox-webext";
411

512
export interface ProxyConfig {
613
host: string;
@@ -23,21 +30,8 @@ const STORAGE_KEYS = {
2330
CONFIG: "proxy_config",
2431
} as const;
2532

26-
type FirefoxProxyInfo = {
27-
type: "http" | "https" | "socks" | "socks4" | "direct";
28-
host?: string;
29-
port?: number;
30-
username?: string;
31-
password?: string;
32-
failoverTimeout?: number;
33-
};
34-
35-
type FirefoxProxyDetails = {
36-
url: string;
37-
};
38-
39-
function getFirefoxProxyApi(): any | null {
40-
return (globalThis as any).browser?.proxy ?? null;
33+
function getFirefoxProxyApi(): FirefoxProxyApi | null {
34+
return (globalThis as FirefoxGlobal).browser?.proxy ?? null;
4135
}
4236

4337
function isFirefoxProxyApiAvailable(): boolean {
@@ -51,7 +45,7 @@ function firefoxProxyType(scheme: ProxyConfig["scheme"]): FirefoxProxyInfo["type
5145
class ProxyManager {
5246
private state: ProxyState = { enabled: false, mode: "direct", config: null };
5347
private firefoxConfig: ProxyConfig | null = null;
54-
private firefoxListener: ((details: FirefoxProxyDetails) => FirefoxProxyInfo[]) | null = null;
48+
private firefoxListener: FirefoxProxyRequestListener | null = null;
5549
private firefoxMultiIpSlots: PacSlot[] = [];
5650
private killSwitchEnabled = true;
5751

@@ -77,8 +71,9 @@ class ProxyManager {
7771
}
7872
}
7973

80-
private ensureFirefoxListener(): void {
81-
if (this.firefoxListener) return;
74+
/** Returns the single-proxy listener, creating it on first use. */
75+
private ensureFirefoxListener(): FirefoxProxyRequestListener {
76+
if (this.firefoxListener) return this.firefoxListener;
8277

8378
this.firefoxListener = (details: FirefoxProxyDetails): FirefoxProxyInfo[] => {
8479
const config = this.firefoxConfig;
@@ -108,24 +103,26 @@ class ProxyManager {
108103
}
109104
return [proxyInfo, { type: "direct" }];
110105
};
106+
107+
return this.firefoxListener;
111108
}
112109

113110
private addFirefoxProxyListener(config: ProxyConfig): void {
114111
const firefoxProxyApi = getFirefoxProxyApi();
115112
if (!firefoxProxyApi?.onRequest) return;
116113

117-
this.ensureFirefoxListener();
114+
const listener = this.ensureFirefoxListener();
118115
this.firefoxConfig = config;
119116

120117
try {
121-
if (firefoxProxyApi.onRequest.hasListener(this.firefoxListener)) {
122-
firefoxProxyApi.onRequest.removeListener(this.firefoxListener);
118+
if (firefoxProxyApi.onRequest.hasListener(listener)) {
119+
firefoxProxyApi.onRequest.removeListener(listener);
123120
}
124121
} catch {
125122
// Ignore stale listener cleanup failures.
126123
}
127124

128-
firefoxProxyApi.onRequest.addListener(this.firefoxListener, {
125+
firefoxProxyApi.onRequest.addListener(listener, {
129126
urls: ["<all_urls>"],
130127
});
131128
}
@@ -151,7 +148,11 @@ class ProxyManager {
151148
enableMultiIp(slots: PacSlot[]): void {
152149
if (!isFirefoxProxyApiAvailable() || slots.length === 0) return;
153150

154-
const firefoxProxyApi = getFirefoxProxyApi();
151+
const onRequest = getFirefoxProxyApi()?.onRequest;
152+
// isFirefoxProxyApiAvailable() above already established this; the explicit
153+
// check is what narrows the optional event for the type checker.
154+
if (!onRequest) return;
155+
155156
this.removeFirefoxProxyListener();
156157

157158
this.firefoxMultiIpSlots = slots;
@@ -182,7 +183,7 @@ class ProxyManager {
182183
return proxies;
183184
};
184185

185-
firefoxProxyApi.onRequest.addListener(this.firefoxListener, {
186+
onRequest.addListener(this.firefoxListener, {
186187
urls: ["<all_urls>"],
187188
});
188189

src/utils/sso.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function launchWebAuthFlow(options: WebAuthFlowOptions): Promise<string | undefi
4848
identity.launchWebAuthFlow(options, (responseUrl) => {
4949
const err =
5050
chrome.runtime?.lastError?.message ||
51-
(typeof browser !== "undefined" && (browser as any).runtime?.lastError?.message);
51+
(typeof browser !== "undefined" && browser.runtime?.lastError?.message);
5252
if (err) {
5353
reject(new Error(err));
5454
return;

src/utils/use-connection-manager.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,24 @@ export function useConnectionManager(): UseConnectionManagerResult {
2222
const proxyChangeCbRef = useRef<((config: null) => void) | null>(null);
2323
const managerRef = useRef<ConnectionManager | null>(null);
2424

25+
// The ConnectionManager is created once and outlives the renders that produce
26+
// these two SDK callbacks, so it reads them through refs instead of capturing
27+
// them directly (recreating the manager would drop the live connection pool,
28+
// ping interval and renew timer).
29+
//
30+
// Refresh the refs from a commit, never from render. A render can be thrown
31+
// away, and a render-phase write would let a render that never committed
32+
// publish its callback into the manager -- which invokes them from timers
33+
// (silentRenew, scheduleReconnect) long after any render has finished. An
34+
// effect with no dependency array runs after every commit, which is exactly
35+
// the "latest committed value" semantics wanted here; the useRef seeds mean
36+
// there is never an empty ref before the first commit.
2537
const authFnRef = useRef(authNetworkClient);
2638
const removeFnRef = useRef(removeNetworkClient);
27-
authFnRef.current = authNetworkClient;
28-
removeFnRef.current = removeNetworkClient;
39+
useEffect(() => {
40+
authFnRef.current = authNetworkClient;
41+
removeFnRef.current = removeNetworkClient;
42+
});
2943

3044
const getManager = useCallback((): ConnectionManager => {
3145
if (!managerRef.current) {

src/utils/use-provider-list-enhanced.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function useProviderListEnhanced() {
190190
let result: FindLocationsResult;
191191

192192
if (searchQuery.length === 0) {
193-
result = await (api as any).networkProviderLocations();
193+
result = await api.networkProviderLocations();
194194
} else {
195195
const response = await fetch(`${API_BASE}/network/find-provider-locations`, {
196196
method: "POST",

0 commit comments

Comments
 (0)