opendevbrowser 0.0.10 → 0.0.11
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 +150 -24
- package/dist/index.js +108 -2
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +108 -2
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/popup.js +58 -11
- package/extension/dist/relay-settings.js +1 -0
- package/extension/manifest.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](https://opencode.ai)
|
|
7
|
-
[](https://
|
|
7
|
+
[](https://registry.npmjs.org/opendevbrowser)
|
|
8
8
|
|
|
9
9
|
> **Script-first browser automation for AI agents.** Snapshot → Refs → Actions.
|
|
10
10
|
|
|
@@ -40,6 +40,12 @@ Recommended (CLI, installs plugin + config + bundled skills + extension assets):
|
|
|
40
40
|
npx opendevbrowser --full --global --no-prompt
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Explicit flags (config + skills, no prompt):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx opendevbrowser --global --with-config --skills-global --no-prompt
|
|
47
|
+
```
|
|
48
|
+
|
|
43
49
|
Manual fallback (edit OpenCode config):
|
|
44
50
|
|
|
45
51
|
```json
|
|
@@ -106,6 +112,107 @@ Restart OpenCode, then run `opendevbrowser_status` to verify the plugin is loade
|
|
|
106
112
|
|
|
107
113
|
---
|
|
108
114
|
|
|
115
|
+
## Tool Reference
|
|
116
|
+
|
|
117
|
+
OpenDevBrowser provides **30 tools** organized by category:
|
|
118
|
+
|
|
119
|
+
### Session Management
|
|
120
|
+
| Tool | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `opendevbrowser_launch` | Launch managed Chrome session with optional profile |
|
|
123
|
+
| `opendevbrowser_connect` | Connect to existing Chrome CDP endpoint |
|
|
124
|
+
| `opendevbrowser_disconnect` | Disconnect browser session |
|
|
125
|
+
| `opendevbrowser_status` | Get session status and connection info |
|
|
126
|
+
|
|
127
|
+
### Tab/Target Management
|
|
128
|
+
| Tool | Description |
|
|
129
|
+
|------|-------------|
|
|
130
|
+
| `opendevbrowser_targets_list` | List all browser tabs/targets |
|
|
131
|
+
| `opendevbrowser_target_use` | Switch to a specific tab by targetId |
|
|
132
|
+
| `opendevbrowser_target_new` | Open new tab (optionally with URL) |
|
|
133
|
+
| `opendevbrowser_target_close` | Close a tab by targetId |
|
|
134
|
+
|
|
135
|
+
### Named Pages
|
|
136
|
+
| Tool | Description |
|
|
137
|
+
|------|-------------|
|
|
138
|
+
| `opendevbrowser_page` | Open or focus a named page (logical tab alias) |
|
|
139
|
+
| `opendevbrowser_list` | List all named pages in session |
|
|
140
|
+
| `opendevbrowser_close` | Close a named page |
|
|
141
|
+
|
|
142
|
+
### Navigation & Interaction
|
|
143
|
+
| Tool | Description |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `opendevbrowser_goto` | Navigate to URL |
|
|
146
|
+
| `opendevbrowser_wait` | Wait for load state or element |
|
|
147
|
+
| `opendevbrowser_snapshot` | Capture page accessibility tree with refs |
|
|
148
|
+
| `opendevbrowser_click` | Click element by ref |
|
|
149
|
+
| `opendevbrowser_type` | Type text into input by ref |
|
|
150
|
+
| `opendevbrowser_select` | Select dropdown option by ref |
|
|
151
|
+
| `opendevbrowser_scroll` | Scroll page or element |
|
|
152
|
+
| `opendevbrowser_run` | Execute multiple actions in sequence |
|
|
153
|
+
|
|
154
|
+
### DOM Inspection
|
|
155
|
+
| Tool | Description |
|
|
156
|
+
|------|-------------|
|
|
157
|
+
| `opendevbrowser_dom_get_html` | Get outerHTML of element by ref |
|
|
158
|
+
| `opendevbrowser_dom_get_text` | Get innerText of element by ref |
|
|
159
|
+
|
|
160
|
+
### DevTools & Analysis
|
|
161
|
+
| Tool | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `opendevbrowser_console_poll` | Poll console logs since sequence |
|
|
164
|
+
| `opendevbrowser_network_poll` | Poll network requests since sequence |
|
|
165
|
+
| `opendevbrowser_screenshot` | Capture page screenshot |
|
|
166
|
+
| `opendevbrowser_perf` | Get page performance metrics |
|
|
167
|
+
| `opendevbrowser_prompting_guide` | Get best-practice prompting guidance |
|
|
168
|
+
|
|
169
|
+
### Export & Cloning
|
|
170
|
+
| Tool | Description |
|
|
171
|
+
|------|-------------|
|
|
172
|
+
| `opendevbrowser_clone_page` | Export page as React component + CSS |
|
|
173
|
+
| `opendevbrowser_clone_component` | Export element subtree as React component |
|
|
174
|
+
|
|
175
|
+
### Skills
|
|
176
|
+
| Tool | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| `opendevbrowser_skill_list` | List available skills |
|
|
179
|
+
| `opendevbrowser_skill_load` | Load a skill by name (with optional topic filter) |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Bundled Skills
|
|
184
|
+
|
|
185
|
+
OpenDevBrowser includes **5 task-specific skill packs**:
|
|
186
|
+
|
|
187
|
+
| Skill | Purpose |
|
|
188
|
+
|-------|---------|
|
|
189
|
+
| `opendevbrowser-best-practices` | Core prompting patterns and workflow guidance |
|
|
190
|
+
| `opendevbrowser-continuity-ledger` | Long-running task state management |
|
|
191
|
+
| `login-automation` | Authentication flow patterns |
|
|
192
|
+
| `form-testing` | Form validation and submission workflows |
|
|
193
|
+
| `data-extraction` | Structured data scraping patterns |
|
|
194
|
+
|
|
195
|
+
Skills are discovered from (priority order):
|
|
196
|
+
1. `.opencode/skill/` (project)
|
|
197
|
+
2. `~/.config/opencode/skill/` (global)
|
|
198
|
+
3. `.claude/skills/` (compatibility)
|
|
199
|
+
4. `~/.claude/skills/` (compatibility)
|
|
200
|
+
5. Custom paths via `skillPaths` config
|
|
201
|
+
|
|
202
|
+
Load a skill: `opendevbrowser_skill_load` with `name` and optional `topic` filter.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Browser Modes
|
|
207
|
+
|
|
208
|
+
| Mode | Tool | Use Case |
|
|
209
|
+
|------|------|----------|
|
|
210
|
+
| **Managed** | `opendevbrowser_launch` | Fresh browser, full control, automatic cleanup |
|
|
211
|
+
| **CDP Connect** | `opendevbrowser_connect` | Attach to existing Chrome with `--remote-debugging-port` |
|
|
212
|
+
| **Extension Relay** | Chrome Extension | Attach to logged-in tabs via relay server |
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
109
216
|
## Chrome Extension (Optional)
|
|
110
217
|
|
|
111
218
|
The extension enables **Mode C** - attach to existing logged-in browser tabs without launching a new browser.
|
|
@@ -114,17 +221,20 @@ The extension enables **Mode C** - attach to existing logged-in browser tabs wit
|
|
|
114
221
|
|
|
115
222
|
The plugin and extension can automatically pair:
|
|
116
223
|
|
|
117
|
-
1. **Plugin side**:
|
|
224
|
+
1. **Plugin side**: Starts a local relay server and config discovery endpoint
|
|
118
225
|
2. **Extension side**: Enable "Auto-Pair" toggle and click Connect
|
|
119
|
-
3. Extension fetches token from
|
|
226
|
+
3. Extension fetches relay port from discovery, then fetches token from the relay server
|
|
120
227
|
4. Connection established with color indicator (green = connected)
|
|
121
228
|
|
|
122
229
|
### Manual Setup
|
|
123
230
|
|
|
124
|
-
1.
|
|
125
|
-
2.
|
|
126
|
-
|
|
127
|
-
|
|
231
|
+
1. Start OpenCode once so the plugin can extract the extension assets.
|
|
232
|
+
2. Load unpacked from `~/.config/opencode/opendevbrowser/extension`
|
|
233
|
+
(fallback: `~/.cache/opencode/node_modules/opendevbrowser/extension`).
|
|
234
|
+
3. Open extension popup
|
|
235
|
+
4. Enter the same relay port and token as the plugin config
|
|
236
|
+
(if `relayToken` is missing, either add one to `opendevbrowser.jsonc` or use Auto-Pair).
|
|
237
|
+
5. Click Connect
|
|
128
238
|
|
|
129
239
|
---
|
|
130
240
|
|
|
@@ -134,42 +244,56 @@ Optional config file: `~/.config/opencode/opendevbrowser.jsonc`
|
|
|
134
244
|
|
|
135
245
|
```jsonc
|
|
136
246
|
{
|
|
247
|
+
// Browser settings
|
|
137
248
|
"headless": false,
|
|
138
249
|
"profile": "default",
|
|
139
250
|
"persistProfile": true,
|
|
251
|
+
"chromePath": "/path/to/chrome", // Custom Chrome executable
|
|
252
|
+
"flags": ["--disable-extensions"], // Additional Chrome flags
|
|
253
|
+
|
|
254
|
+
// Snapshot limits
|
|
140
255
|
"snapshot": { "maxChars": 16000, "maxNodes": 1000 },
|
|
256
|
+
|
|
257
|
+
// Export limits
|
|
141
258
|
"export": { "maxNodes": 1000, "inlineStyles": true },
|
|
259
|
+
|
|
260
|
+
// DevTools output
|
|
142
261
|
"devtools": { "showFullUrls": false, "showFullConsole": false },
|
|
262
|
+
|
|
263
|
+
// Security (all default false for safety)
|
|
143
264
|
"security": {
|
|
144
265
|
"allowRawCDP": false,
|
|
145
266
|
"allowNonLocalCdp": false,
|
|
146
267
|
"allowUnsafeExport": false
|
|
147
268
|
},
|
|
269
|
+
|
|
270
|
+
// Skills configuration
|
|
271
|
+
"skills": {
|
|
272
|
+
"nudge": {
|
|
273
|
+
"enabled": true,
|
|
274
|
+
"keywords": ["form", "login", "extract", "scrape"],
|
|
275
|
+
"maxAgeMs": 60000
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
"skillPaths": ["./custom-skills"], // Additional skill directories
|
|
279
|
+
|
|
280
|
+
// Continuity ledger
|
|
148
281
|
"continuity": {
|
|
149
282
|
"enabled": true,
|
|
150
283
|
"filePath": "opendevbrowser_continuity.md",
|
|
151
284
|
"nudge": {
|
|
152
285
|
"enabled": true,
|
|
153
|
-
"keywords": [
|
|
154
|
-
"plan",
|
|
155
|
-
"multi-step",
|
|
156
|
-
"multi step",
|
|
157
|
-
"long-running",
|
|
158
|
-
"long running",
|
|
159
|
-
"refactor",
|
|
160
|
-
"migration",
|
|
161
|
-
"rollout",
|
|
162
|
-
"release",
|
|
163
|
-
"upgrade",
|
|
164
|
-
"investigate",
|
|
165
|
-
"follow-up",
|
|
166
|
-
"continue"
|
|
167
|
-
],
|
|
286
|
+
"keywords": ["plan", "multi-step", "refactor", "migration"],
|
|
168
287
|
"maxAgeMs": 60000
|
|
169
288
|
}
|
|
170
289
|
},
|
|
290
|
+
|
|
291
|
+
// Extension relay
|
|
171
292
|
"relayPort": 8787,
|
|
172
|
-
"relayToken": "auto-generated-on-first-run"
|
|
293
|
+
"relayToken": "auto-generated-on-first-run",
|
|
294
|
+
|
|
295
|
+
// Updates
|
|
296
|
+
"checkForUpdates": false
|
|
173
297
|
}
|
|
174
298
|
```
|
|
175
299
|
|
|
@@ -214,7 +338,7 @@ rm -rf ~/.cache/opencode/node_modules/opendevbrowser
|
|
|
214
338
|
npx opendevbrowser --update
|
|
215
339
|
```
|
|
216
340
|
|
|
217
|
-
Release checklist:
|
|
341
|
+
Release checklist: [docs/DISTRIBUTION_PLAN.md](docs/DISTRIBUTION_PLAN.md)
|
|
218
342
|
|
|
219
343
|
---
|
|
220
344
|
|
|
@@ -226,6 +350,8 @@ npm run build # Compile to dist/
|
|
|
226
350
|
npm run test # Run tests with coverage
|
|
227
351
|
npm run lint # ESLint checks
|
|
228
352
|
npm run extension:build # Compile extension
|
|
353
|
+
npm run version:check # Verify package/extension version alignment
|
|
354
|
+
npm run extension:pack # Build extension zip for releases
|
|
229
355
|
```
|
|
230
356
|
|
|
231
357
|
---
|
package/dist/index.js
CHANGED
|
@@ -2276,21 +2276,30 @@ function buildContinuityNudgeMessage(filePath) {
|
|
|
2276
2276
|
import { createServer } from "http";
|
|
2277
2277
|
import { timingSafeEqual } from "crypto";
|
|
2278
2278
|
import { WebSocket, WebSocketServer } from "ws";
|
|
2279
|
+
var DEFAULT_DISCOVERY_PORT = 8787;
|
|
2280
|
+
var CONFIG_PATH = "/config";
|
|
2281
|
+
var PAIR_PATH = "/pair";
|
|
2279
2282
|
var RelayServer = class _RelayServer {
|
|
2280
2283
|
running = false;
|
|
2281
2284
|
baseUrl = null;
|
|
2282
2285
|
port = null;
|
|
2283
2286
|
server = null;
|
|
2287
|
+
discoveryServer = null;
|
|
2284
2288
|
extensionWss = null;
|
|
2285
2289
|
cdpWss = null;
|
|
2286
2290
|
extensionSocket = null;
|
|
2287
2291
|
cdpSocket = null;
|
|
2288
2292
|
extensionInfo = null;
|
|
2289
2293
|
pairingToken = null;
|
|
2294
|
+
configuredDiscoveryPort;
|
|
2295
|
+
discoveryPort = null;
|
|
2290
2296
|
handshakeAttempts = /* @__PURE__ */ new Map();
|
|
2291
2297
|
cdpAllowlist = null;
|
|
2292
2298
|
static MAX_HANDSHAKE_ATTEMPTS = 5;
|
|
2293
2299
|
static RATE_LIMIT_WINDOW_MS = 6e4;
|
|
2300
|
+
constructor(options = {}) {
|
|
2301
|
+
this.configuredDiscoveryPort = options.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
|
|
2302
|
+
}
|
|
2294
2303
|
async start(port = 8787) {
|
|
2295
2304
|
if (this.running && this.baseUrl && this.port !== null) {
|
|
2296
2305
|
return { url: this.baseUrl, port: this.port };
|
|
@@ -2335,7 +2344,15 @@ var RelayServer = class _RelayServer {
|
|
|
2335
2344
|
this.server.on("request", (request, response) => {
|
|
2336
2345
|
const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
|
|
2337
2346
|
const origin = request.headers.origin;
|
|
2338
|
-
if (pathname ===
|
|
2347
|
+
if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
|
|
2348
|
+
this.handleConfigPreflight(origin, response);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (pathname === CONFIG_PATH && request.method === "GET") {
|
|
2352
|
+
this.handleConfigRequest(origin, response);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (pathname === PAIR_PATH && request.method === "OPTIONS") {
|
|
2339
2356
|
if (origin && origin.startsWith("chrome-extension://")) {
|
|
2340
2357
|
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2341
2358
|
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
@@ -2345,7 +2362,7 @@ var RelayServer = class _RelayServer {
|
|
|
2345
2362
|
response.end();
|
|
2346
2363
|
return;
|
|
2347
2364
|
}
|
|
2348
|
-
if (pathname ===
|
|
2365
|
+
if (pathname === PAIR_PATH && request.method === "GET") {
|
|
2349
2366
|
const isLocalhost = !origin || origin.startsWith("chrome-extension://");
|
|
2350
2367
|
if (!isLocalhost) {
|
|
2351
2368
|
response.writeHead(403, { "Content-Type": "application/json" });
|
|
@@ -2405,6 +2422,13 @@ var RelayServer = class _RelayServer {
|
|
|
2405
2422
|
this.port = address.port;
|
|
2406
2423
|
this.baseUrl = `ws://127.0.0.1:${address.port}`;
|
|
2407
2424
|
this.running = true;
|
|
2425
|
+
try {
|
|
2426
|
+
await this.startDiscoveryServer();
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2429
|
+
console.warn(`[opendevbrowser] Discovery server failed to start: ${message}`);
|
|
2430
|
+
this.stopDiscoveryServer();
|
|
2431
|
+
}
|
|
2408
2432
|
return { url: this.baseUrl, port: address.port };
|
|
2409
2433
|
}
|
|
2410
2434
|
stop() {
|
|
@@ -2412,6 +2436,7 @@ var RelayServer = class _RelayServer {
|
|
|
2412
2436
|
this.baseUrl = null;
|
|
2413
2437
|
this.port = null;
|
|
2414
2438
|
this.extensionInfo = null;
|
|
2439
|
+
this.stopDiscoveryServer();
|
|
2415
2440
|
if (this.extensionSocket) {
|
|
2416
2441
|
this.extensionSocket.close(1e3, "Relay stopped");
|
|
2417
2442
|
this.extensionSocket = null;
|
|
@@ -2440,6 +2465,12 @@ var RelayServer = class _RelayServer {
|
|
|
2440
2465
|
getCdpUrl() {
|
|
2441
2466
|
return this.baseUrl ? `${this.baseUrl}/cdp` : null;
|
|
2442
2467
|
}
|
|
2468
|
+
getDiscoveryPort() {
|
|
2469
|
+
if (this.port !== null && this.port === this.configuredDiscoveryPort) {
|
|
2470
|
+
return this.port;
|
|
2471
|
+
}
|
|
2472
|
+
return this.discoveryPort;
|
|
2473
|
+
}
|
|
2443
2474
|
setToken(token) {
|
|
2444
2475
|
const trimmed = typeof token === "string" ? token.trim() : "";
|
|
2445
2476
|
this.pairingToken = trimmed.length ? trimmed : null;
|
|
@@ -2460,6 +2491,81 @@ var RelayServer = class _RelayServer {
|
|
|
2460
2491
|
}
|
|
2461
2492
|
return false;
|
|
2462
2493
|
}
|
|
2494
|
+
isExtensionOrigin(origin) {
|
|
2495
|
+
return Boolean(origin && origin.startsWith("chrome-extension://"));
|
|
2496
|
+
}
|
|
2497
|
+
handleConfigPreflight(origin, response) {
|
|
2498
|
+
if (this.isExtensionOrigin(origin)) {
|
|
2499
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2500
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
2501
|
+
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
2502
|
+
}
|
|
2503
|
+
response.writeHead(204);
|
|
2504
|
+
response.end();
|
|
2505
|
+
}
|
|
2506
|
+
handleConfigRequest(origin, response) {
|
|
2507
|
+
if (!this.isExtensionOrigin(origin)) {
|
|
2508
|
+
response.writeHead(403, { "Content-Type": "application/json" });
|
|
2509
|
+
response.end(JSON.stringify({ error: "Forbidden: extension origin required" }));
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (origin) {
|
|
2513
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
2514
|
+
}
|
|
2515
|
+
if (this.port === null) {
|
|
2516
|
+
response.writeHead(503, { "Content-Type": "application/json" });
|
|
2517
|
+
response.end(JSON.stringify({ error: "Relay not running" }));
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
response.writeHead(200, {
|
|
2521
|
+
"Content-Type": "application/json",
|
|
2522
|
+
"Cache-Control": "no-store"
|
|
2523
|
+
});
|
|
2524
|
+
response.end(JSON.stringify({
|
|
2525
|
+
relayPort: this.port,
|
|
2526
|
+
pairingRequired: Boolean(this.pairingToken)
|
|
2527
|
+
}));
|
|
2528
|
+
}
|
|
2529
|
+
async startDiscoveryServer() {
|
|
2530
|
+
if (this.port === null || this.discoveryServer) {
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (this.configuredDiscoveryPort > 0 && this.configuredDiscoveryPort === this.port) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
this.discoveryServer = createServer((request, response) => {
|
|
2537
|
+
const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
|
|
2538
|
+
const origin = request.headers.origin;
|
|
2539
|
+
if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
|
|
2540
|
+
this.handleConfigPreflight(origin, response);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (pathname === CONFIG_PATH && request.method === "GET") {
|
|
2544
|
+
this.handleConfigRequest(origin, response);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
response.writeHead(404);
|
|
2548
|
+
response.end();
|
|
2549
|
+
});
|
|
2550
|
+
await new Promise((resolve, reject) => {
|
|
2551
|
+
this.discoveryServer?.once("error", reject);
|
|
2552
|
+
this.discoveryServer?.listen(this.configuredDiscoveryPort, "127.0.0.1", () => {
|
|
2553
|
+
resolve();
|
|
2554
|
+
});
|
|
2555
|
+
});
|
|
2556
|
+
const address = this.discoveryServer.address();
|
|
2557
|
+
if (!address) {
|
|
2558
|
+
throw new Error("Discovery server did not expose a port");
|
|
2559
|
+
}
|
|
2560
|
+
this.discoveryPort = address.port;
|
|
2561
|
+
}
|
|
2562
|
+
stopDiscoveryServer() {
|
|
2563
|
+
if (this.discoveryServer) {
|
|
2564
|
+
this.discoveryServer.close();
|
|
2565
|
+
this.discoveryServer = null;
|
|
2566
|
+
}
|
|
2567
|
+
this.discoveryPort = null;
|
|
2568
|
+
}
|
|
2463
2569
|
isRateLimited(ip) {
|
|
2464
2570
|
const now = Date.now();
|
|
2465
2571
|
const record = this.handshakeAttempts.get(ip);
|