crawlio-browser 1.3.0 → 1.4.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.
- package/.claude-plugin/plugin.json +2 -2
- package/dist/mcp-server/chunk-JRZS5IP6.js +39 -0
- package/dist/mcp-server/index.js +862 -2506
- package/dist/mcp-server/init-4ITWQXXA.js +722 -0
- package/dist/mcp-server/tool-embeddings.json +6 -0
- package/package.json +9 -4
- package/skills/browser-automation/SKILL.md +76 -3
- package/skills/browser-automation/reference.md +11 -2
- package/dist/mcp-server/chunk-JSBRDJBE.js +0 -30
- package/dist/mcp-server/init-TRQTWLAB.js +0 -492
package/dist/mcp-server/index.js
CHANGED
|
@@ -1,1540 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
exports.isInSubnet = isInSubnet;
|
|
12
|
-
exports.isCorrect = isCorrect;
|
|
13
|
-
exports.numberToPaddedHex = numberToPaddedHex;
|
|
14
|
-
exports.stringToPaddedHex = stringToPaddedHex;
|
|
15
|
-
exports.testBit = testBit;
|
|
16
|
-
function isInSubnet(address) {
|
|
17
|
-
if (this.subnetMask < address.subnetMask) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
if (this.mask(address.subnetMask) === address.mask()) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
function isCorrect(defaultBits) {
|
|
26
|
-
return function() {
|
|
27
|
-
if (this.addressMinusSuffix !== this.correctForm()) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
if (this.subnetMask === defaultBits && !this.parsedSubnet) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
return this.parsedSubnet === String(this.subnetMask);
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function numberToPaddedHex(number) {
|
|
37
|
-
return number.toString(16).padStart(2, "0");
|
|
38
|
-
}
|
|
39
|
-
function stringToPaddedHex(numberString) {
|
|
40
|
-
return numberToPaddedHex(parseInt(numberString, 10));
|
|
41
|
-
}
|
|
42
|
-
function testBit(binaryValue, position) {
|
|
43
|
-
const { length } = binaryValue;
|
|
44
|
-
if (position > length) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
const positionInString = length - position;
|
|
48
|
-
return binaryValue.substring(positionInString, positionInString + 1) === "1";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// node_modules/ip-address/dist/v4/constants.js
|
|
54
|
-
var require_constants = __commonJS({
|
|
55
|
-
"node_modules/ip-address/dist/v4/constants.js"(exports) {
|
|
56
|
-
"use strict";
|
|
57
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
-
exports.RE_SUBNET_STRING = exports.RE_ADDRESS = exports.GROUPS = exports.BITS = void 0;
|
|
59
|
-
exports.BITS = 32;
|
|
60
|
-
exports.GROUPS = 4;
|
|
61
|
-
exports.RE_ADDRESS = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/g;
|
|
62
|
-
exports.RE_SUBNET_STRING = /\/\d{1,2}$/;
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// node_modules/ip-address/dist/address-error.js
|
|
67
|
-
var require_address_error = __commonJS({
|
|
68
|
-
"node_modules/ip-address/dist/address-error.js"(exports) {
|
|
69
|
-
"use strict";
|
|
70
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
71
|
-
exports.AddressError = void 0;
|
|
72
|
-
var AddressError = class extends Error {
|
|
73
|
-
constructor(message, parseMessage) {
|
|
74
|
-
super(message);
|
|
75
|
-
this.name = "AddressError";
|
|
76
|
-
this.parseMessage = parseMessage;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
exports.AddressError = AddressError;
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// node_modules/ip-address/dist/ipv4.js
|
|
84
|
-
var require_ipv4 = __commonJS({
|
|
85
|
-
"node_modules/ip-address/dist/ipv4.js"(exports) {
|
|
86
|
-
"use strict";
|
|
87
|
-
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
88
|
-
if (k2 === void 0) k2 = k;
|
|
89
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
90
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
91
|
-
desc = { enumerable: true, get: function() {
|
|
92
|
-
return m[k];
|
|
93
|
-
} };
|
|
94
|
-
}
|
|
95
|
-
Object.defineProperty(o, k2, desc);
|
|
96
|
-
}) : (function(o, m, k, k2) {
|
|
97
|
-
if (k2 === void 0) k2 = k;
|
|
98
|
-
o[k2] = m[k];
|
|
99
|
-
}));
|
|
100
|
-
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
101
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
102
|
-
}) : function(o, v) {
|
|
103
|
-
o["default"] = v;
|
|
104
|
-
});
|
|
105
|
-
var __importStar = exports && exports.__importStar || function(mod) {
|
|
106
|
-
if (mod && mod.__esModule) return mod;
|
|
107
|
-
var result = {};
|
|
108
|
-
if (mod != null) {
|
|
109
|
-
for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
110
|
-
}
|
|
111
|
-
__setModuleDefault(result, mod);
|
|
112
|
-
return result;
|
|
113
|
-
};
|
|
114
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
115
|
-
exports.Address4 = void 0;
|
|
116
|
-
var common = __importStar(require_common());
|
|
117
|
-
var constants = __importStar(require_constants());
|
|
118
|
-
var address_error_1 = require_address_error();
|
|
119
|
-
var Address4 = class _Address4 {
|
|
120
|
-
constructor(address) {
|
|
121
|
-
this.groups = constants.GROUPS;
|
|
122
|
-
this.parsedAddress = [];
|
|
123
|
-
this.parsedSubnet = "";
|
|
124
|
-
this.subnet = "/32";
|
|
125
|
-
this.subnetMask = 32;
|
|
126
|
-
this.v4 = true;
|
|
127
|
-
this.isCorrect = common.isCorrect(constants.BITS);
|
|
128
|
-
this.isInSubnet = common.isInSubnet;
|
|
129
|
-
this.address = address;
|
|
130
|
-
const subnet = constants.RE_SUBNET_STRING.exec(address);
|
|
131
|
-
if (subnet) {
|
|
132
|
-
this.parsedSubnet = subnet[0].replace("/", "");
|
|
133
|
-
this.subnetMask = parseInt(this.parsedSubnet, 10);
|
|
134
|
-
this.subnet = `/${this.subnetMask}`;
|
|
135
|
-
if (this.subnetMask < 0 || this.subnetMask > constants.BITS) {
|
|
136
|
-
throw new address_error_1.AddressError("Invalid subnet mask.");
|
|
137
|
-
}
|
|
138
|
-
address = address.replace(constants.RE_SUBNET_STRING, "");
|
|
139
|
-
}
|
|
140
|
-
this.addressMinusSuffix = address;
|
|
141
|
-
this.parsedAddress = this.parse(address);
|
|
142
|
-
}
|
|
143
|
-
static isValid(address) {
|
|
144
|
-
try {
|
|
145
|
-
new _Address4(address);
|
|
146
|
-
return true;
|
|
147
|
-
} catch (e) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/*
|
|
152
|
-
* Parses a v4 address
|
|
153
|
-
*/
|
|
154
|
-
parse(address) {
|
|
155
|
-
const groups = address.split(".");
|
|
156
|
-
if (!address.match(constants.RE_ADDRESS)) {
|
|
157
|
-
throw new address_error_1.AddressError("Invalid IPv4 address.");
|
|
158
|
-
}
|
|
159
|
-
return groups;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Returns the correct form of an address
|
|
163
|
-
* @memberof Address4
|
|
164
|
-
* @instance
|
|
165
|
-
* @returns {String}
|
|
166
|
-
*/
|
|
167
|
-
correctForm() {
|
|
168
|
-
return this.parsedAddress.map((part) => parseInt(part, 10)).join(".");
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Converts a hex string to an IPv4 address object
|
|
172
|
-
* @memberof Address4
|
|
173
|
-
* @static
|
|
174
|
-
* @param {string} hex - a hex string to convert
|
|
175
|
-
* @returns {Address4}
|
|
176
|
-
*/
|
|
177
|
-
static fromHex(hex) {
|
|
178
|
-
const padded = hex.replace(/:/g, "").padStart(8, "0");
|
|
179
|
-
const groups = [];
|
|
180
|
-
let i;
|
|
181
|
-
for (i = 0; i < 8; i += 2) {
|
|
182
|
-
const h = padded.slice(i, i + 2);
|
|
183
|
-
groups.push(parseInt(h, 16));
|
|
184
|
-
}
|
|
185
|
-
return new _Address4(groups.join("."));
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Converts an integer into a IPv4 address object
|
|
189
|
-
* @memberof Address4
|
|
190
|
-
* @static
|
|
191
|
-
* @param {integer} integer - a number to convert
|
|
192
|
-
* @returns {Address4}
|
|
193
|
-
*/
|
|
194
|
-
static fromInteger(integer) {
|
|
195
|
-
return _Address4.fromHex(integer.toString(16));
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Return an address from in-addr.arpa form
|
|
199
|
-
* @memberof Address4
|
|
200
|
-
* @static
|
|
201
|
-
* @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address
|
|
202
|
-
* @returns {Adress4}
|
|
203
|
-
* @example
|
|
204
|
-
* var address = Address4.fromArpa(42.2.0.192.in-addr.arpa.)
|
|
205
|
-
* address.correctForm(); // '192.0.2.42'
|
|
206
|
-
*/
|
|
207
|
-
static fromArpa(arpaFormAddress) {
|
|
208
|
-
const leader = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.$/, "");
|
|
209
|
-
const address = leader.split(".").reverse().join(".");
|
|
210
|
-
return new _Address4(address);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Converts an IPv4 address object to a hex string
|
|
214
|
-
* @memberof Address4
|
|
215
|
-
* @instance
|
|
216
|
-
* @returns {String}
|
|
217
|
-
*/
|
|
218
|
-
toHex() {
|
|
219
|
-
return this.parsedAddress.map((part) => common.stringToPaddedHex(part)).join(":");
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Converts an IPv4 address object to an array of bytes
|
|
223
|
-
* @memberof Address4
|
|
224
|
-
* @instance
|
|
225
|
-
* @returns {Array}
|
|
226
|
-
*/
|
|
227
|
-
toArray() {
|
|
228
|
-
return this.parsedAddress.map((part) => parseInt(part, 10));
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Converts an IPv4 address object to an IPv6 address group
|
|
232
|
-
* @memberof Address4
|
|
233
|
-
* @instance
|
|
234
|
-
* @returns {String}
|
|
235
|
-
*/
|
|
236
|
-
toGroup6() {
|
|
237
|
-
const output = [];
|
|
238
|
-
let i;
|
|
239
|
-
for (i = 0; i < constants.GROUPS; i += 2) {
|
|
240
|
-
output.push(`${common.stringToPaddedHex(this.parsedAddress[i])}${common.stringToPaddedHex(this.parsedAddress[i + 1])}`);
|
|
241
|
-
}
|
|
242
|
-
return output.join(":");
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Returns the address as a `bigint`
|
|
246
|
-
* @memberof Address4
|
|
247
|
-
* @instance
|
|
248
|
-
* @returns {bigint}
|
|
249
|
-
*/
|
|
250
|
-
bigInt() {
|
|
251
|
-
return BigInt(`0x${this.parsedAddress.map((n) => common.stringToPaddedHex(n)).join("")}`);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Helper function getting start address.
|
|
255
|
-
* @memberof Address4
|
|
256
|
-
* @instance
|
|
257
|
-
* @returns {bigint}
|
|
258
|
-
*/
|
|
259
|
-
_startAddress() {
|
|
260
|
-
return BigInt(`0b${this.mask() + "0".repeat(constants.BITS - this.subnetMask)}`);
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* The first address in the range given by this address' subnet.
|
|
264
|
-
* Often referred to as the Network Address.
|
|
265
|
-
* @memberof Address4
|
|
266
|
-
* @instance
|
|
267
|
-
* @returns {Address4}
|
|
268
|
-
*/
|
|
269
|
-
startAddress() {
|
|
270
|
-
return _Address4.fromBigInt(this._startAddress());
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* The first host address in the range given by this address's subnet ie
|
|
274
|
-
* the first address after the Network Address
|
|
275
|
-
* @memberof Address4
|
|
276
|
-
* @instance
|
|
277
|
-
* @returns {Address4}
|
|
278
|
-
*/
|
|
279
|
-
startAddressExclusive() {
|
|
280
|
-
const adjust = BigInt("1");
|
|
281
|
-
return _Address4.fromBigInt(this._startAddress() + adjust);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Helper function getting end address.
|
|
285
|
-
* @memberof Address4
|
|
286
|
-
* @instance
|
|
287
|
-
* @returns {bigint}
|
|
288
|
-
*/
|
|
289
|
-
_endAddress() {
|
|
290
|
-
return BigInt(`0b${this.mask() + "1".repeat(constants.BITS - this.subnetMask)}`);
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* The last address in the range given by this address' subnet
|
|
294
|
-
* Often referred to as the Broadcast
|
|
295
|
-
* @memberof Address4
|
|
296
|
-
* @instance
|
|
297
|
-
* @returns {Address4}
|
|
298
|
-
*/
|
|
299
|
-
endAddress() {
|
|
300
|
-
return _Address4.fromBigInt(this._endAddress());
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* The last host address in the range given by this address's subnet ie
|
|
304
|
-
* the last address prior to the Broadcast Address
|
|
305
|
-
* @memberof Address4
|
|
306
|
-
* @instance
|
|
307
|
-
* @returns {Address4}
|
|
308
|
-
*/
|
|
309
|
-
endAddressExclusive() {
|
|
310
|
-
const adjust = BigInt("1");
|
|
311
|
-
return _Address4.fromBigInt(this._endAddress() - adjust);
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Converts a BigInt to a v4 address object
|
|
315
|
-
* @memberof Address4
|
|
316
|
-
* @static
|
|
317
|
-
* @param {bigint} bigInt - a BigInt to convert
|
|
318
|
-
* @returns {Address4}
|
|
319
|
-
*/
|
|
320
|
-
static fromBigInt(bigInt) {
|
|
321
|
-
return _Address4.fromHex(bigInt.toString(16));
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Returns the first n bits of the address, defaulting to the
|
|
325
|
-
* subnet mask
|
|
326
|
-
* @memberof Address4
|
|
327
|
-
* @instance
|
|
328
|
-
* @returns {String}
|
|
329
|
-
*/
|
|
330
|
-
mask(mask) {
|
|
331
|
-
if (mask === void 0) {
|
|
332
|
-
mask = this.subnetMask;
|
|
333
|
-
}
|
|
334
|
-
return this.getBitsBase2(0, mask);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Returns the bits in the given range as a base-2 string
|
|
338
|
-
* @memberof Address4
|
|
339
|
-
* @instance
|
|
340
|
-
* @returns {string}
|
|
341
|
-
*/
|
|
342
|
-
getBitsBase2(start, end) {
|
|
343
|
-
return this.binaryZeroPad().slice(start, end);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Return the reversed ip6.arpa form of the address
|
|
347
|
-
* @memberof Address4
|
|
348
|
-
* @param {Object} options
|
|
349
|
-
* @param {boolean} options.omitSuffix - omit the "in-addr.arpa" suffix
|
|
350
|
-
* @instance
|
|
351
|
-
* @returns {String}
|
|
352
|
-
*/
|
|
353
|
-
reverseForm(options) {
|
|
354
|
-
if (!options) {
|
|
355
|
-
options = {};
|
|
356
|
-
}
|
|
357
|
-
const reversed = this.correctForm().split(".").reverse().join(".");
|
|
358
|
-
if (options.omitSuffix) {
|
|
359
|
-
return reversed;
|
|
360
|
-
}
|
|
361
|
-
return `${reversed}.in-addr.arpa.`;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Returns true if the given address is a multicast address
|
|
365
|
-
* @memberof Address4
|
|
366
|
-
* @instance
|
|
367
|
-
* @returns {boolean}
|
|
368
|
-
*/
|
|
369
|
-
isMulticast() {
|
|
370
|
-
return this.isInSubnet(new _Address4("224.0.0.0/4"));
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Returns a zero-padded base-2 string representation of the address
|
|
374
|
-
* @memberof Address4
|
|
375
|
-
* @instance
|
|
376
|
-
* @returns {string}
|
|
377
|
-
*/
|
|
378
|
-
binaryZeroPad() {
|
|
379
|
-
return this.bigInt().toString(2).padStart(constants.BITS, "0");
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Groups an IPv4 address for inclusion at the end of an IPv6 address
|
|
383
|
-
* @returns {String}
|
|
384
|
-
*/
|
|
385
|
-
groupForV6() {
|
|
386
|
-
const segments = this.parsedAddress;
|
|
387
|
-
return this.address.replace(constants.RE_ADDRESS, `<span class="hover-group group-v4 group-6">${segments.slice(0, 2).join(".")}</span>.<span class="hover-group group-v4 group-7">${segments.slice(2, 4).join(".")}</span>`);
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
exports.Address4 = Address4;
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// node_modules/ip-address/dist/v6/constants.js
|
|
395
|
-
var require_constants2 = __commonJS({
|
|
396
|
-
"node_modules/ip-address/dist/v6/constants.js"(exports) {
|
|
397
|
-
"use strict";
|
|
398
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
399
|
-
exports.RE_URL_WITH_PORT = exports.RE_URL = exports.RE_ZONE_STRING = exports.RE_SUBNET_STRING = exports.RE_BAD_ADDRESS = exports.RE_BAD_CHARACTERS = exports.TYPES = exports.SCOPES = exports.GROUPS = exports.BITS = void 0;
|
|
400
|
-
exports.BITS = 128;
|
|
401
|
-
exports.GROUPS = 8;
|
|
402
|
-
exports.SCOPES = {
|
|
403
|
-
0: "Reserved",
|
|
404
|
-
1: "Interface local",
|
|
405
|
-
2: "Link local",
|
|
406
|
-
4: "Admin local",
|
|
407
|
-
5: "Site local",
|
|
408
|
-
8: "Organization local",
|
|
409
|
-
14: "Global",
|
|
410
|
-
15: "Reserved"
|
|
411
|
-
};
|
|
412
|
-
exports.TYPES = {
|
|
413
|
-
"ff01::1/128": "Multicast (All nodes on this interface)",
|
|
414
|
-
"ff01::2/128": "Multicast (All routers on this interface)",
|
|
415
|
-
"ff02::1/128": "Multicast (All nodes on this link)",
|
|
416
|
-
"ff02::2/128": "Multicast (All routers on this link)",
|
|
417
|
-
"ff05::2/128": "Multicast (All routers in this site)",
|
|
418
|
-
"ff02::5/128": "Multicast (OSPFv3 AllSPF routers)",
|
|
419
|
-
"ff02::6/128": "Multicast (OSPFv3 AllDR routers)",
|
|
420
|
-
"ff02::9/128": "Multicast (RIP routers)",
|
|
421
|
-
"ff02::a/128": "Multicast (EIGRP routers)",
|
|
422
|
-
"ff02::d/128": "Multicast (PIM routers)",
|
|
423
|
-
"ff02::16/128": "Multicast (MLDv2 reports)",
|
|
424
|
-
"ff01::fb/128": "Multicast (mDNSv6)",
|
|
425
|
-
"ff02::fb/128": "Multicast (mDNSv6)",
|
|
426
|
-
"ff05::fb/128": "Multicast (mDNSv6)",
|
|
427
|
-
"ff02::1:2/128": "Multicast (All DHCP servers and relay agents on this link)",
|
|
428
|
-
"ff05::1:2/128": "Multicast (All DHCP servers and relay agents in this site)",
|
|
429
|
-
"ff02::1:3/128": "Multicast (All DHCP servers on this link)",
|
|
430
|
-
"ff05::1:3/128": "Multicast (All DHCP servers in this site)",
|
|
431
|
-
"::/128": "Unspecified",
|
|
432
|
-
"::1/128": "Loopback",
|
|
433
|
-
"ff00::/8": "Multicast",
|
|
434
|
-
"fe80::/10": "Link-local unicast"
|
|
435
|
-
};
|
|
436
|
-
exports.RE_BAD_CHARACTERS = /([^0-9a-f:/%])/gi;
|
|
437
|
-
exports.RE_BAD_ADDRESS = /([0-9a-f]{5,}|:{3,}|[^:]:$|^:[^:]|\/$)/gi;
|
|
438
|
-
exports.RE_SUBNET_STRING = /\/\d{1,3}(?=%|$)/;
|
|
439
|
-
exports.RE_ZONE_STRING = /%.*$/;
|
|
440
|
-
exports.RE_URL = /^\[{0,1}([0-9a-f:]+)\]{0,1}/;
|
|
441
|
-
exports.RE_URL_WITH_PORT = /\[([0-9a-f:]+)\]:([0-9]{1,5})/;
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
// node_modules/ip-address/dist/v6/helpers.js
|
|
446
|
-
var require_helpers = __commonJS({
|
|
447
|
-
"node_modules/ip-address/dist/v6/helpers.js"(exports) {
|
|
448
|
-
"use strict";
|
|
449
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
450
|
-
exports.spanAllZeroes = spanAllZeroes;
|
|
451
|
-
exports.spanAll = spanAll;
|
|
452
|
-
exports.spanLeadingZeroes = spanLeadingZeroes;
|
|
453
|
-
exports.simpleGroup = simpleGroup;
|
|
454
|
-
function spanAllZeroes(s) {
|
|
455
|
-
return s.replace(/(0+)/g, '<span class="zero">$1</span>');
|
|
456
|
-
}
|
|
457
|
-
function spanAll(s, offset = 0) {
|
|
458
|
-
const letters = s.split("");
|
|
459
|
-
return letters.map((n, i) => `<span class="digit value-${n} position-${i + offset}">${spanAllZeroes(n)}</span>`).join("");
|
|
460
|
-
}
|
|
461
|
-
function spanLeadingZeroesSimple(group) {
|
|
462
|
-
return group.replace(/^(0+)/, '<span class="zero">$1</span>');
|
|
463
|
-
}
|
|
464
|
-
function spanLeadingZeroes(address) {
|
|
465
|
-
const groups = address.split(":");
|
|
466
|
-
return groups.map((g) => spanLeadingZeroesSimple(g)).join(":");
|
|
467
|
-
}
|
|
468
|
-
function simpleGroup(addressString, offset = 0) {
|
|
469
|
-
const groups = addressString.split(":");
|
|
470
|
-
return groups.map((g, i) => {
|
|
471
|
-
if (/group-v4/.test(g)) {
|
|
472
|
-
return g;
|
|
473
|
-
}
|
|
474
|
-
return `<span class="hover-group group-${i + offset}">${spanLeadingZeroesSimple(g)}</span>`;
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// node_modules/ip-address/dist/v6/regular-expressions.js
|
|
481
|
-
var require_regular_expressions = __commonJS({
|
|
482
|
-
"node_modules/ip-address/dist/v6/regular-expressions.js"(exports) {
|
|
483
|
-
"use strict";
|
|
484
|
-
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
485
|
-
if (k2 === void 0) k2 = k;
|
|
486
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
487
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
488
|
-
desc = { enumerable: true, get: function() {
|
|
489
|
-
return m[k];
|
|
490
|
-
} };
|
|
491
|
-
}
|
|
492
|
-
Object.defineProperty(o, k2, desc);
|
|
493
|
-
}) : (function(o, m, k, k2) {
|
|
494
|
-
if (k2 === void 0) k2 = k;
|
|
495
|
-
o[k2] = m[k];
|
|
496
|
-
}));
|
|
497
|
-
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
498
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
499
|
-
}) : function(o, v) {
|
|
500
|
-
o["default"] = v;
|
|
501
|
-
});
|
|
502
|
-
var __importStar = exports && exports.__importStar || function(mod) {
|
|
503
|
-
if (mod && mod.__esModule) return mod;
|
|
504
|
-
var result = {};
|
|
505
|
-
if (mod != null) {
|
|
506
|
-
for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
507
|
-
}
|
|
508
|
-
__setModuleDefault(result, mod);
|
|
509
|
-
return result;
|
|
510
|
-
};
|
|
511
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
512
|
-
exports.ADDRESS_BOUNDARY = void 0;
|
|
513
|
-
exports.groupPossibilities = groupPossibilities;
|
|
514
|
-
exports.padGroup = padGroup;
|
|
515
|
-
exports.simpleRegularExpression = simpleRegularExpression;
|
|
516
|
-
exports.possibleElisions = possibleElisions;
|
|
517
|
-
var v6 = __importStar(require_constants2());
|
|
518
|
-
function groupPossibilities(possibilities) {
|
|
519
|
-
return `(${possibilities.join("|")})`;
|
|
520
|
-
}
|
|
521
|
-
function padGroup(group) {
|
|
522
|
-
if (group.length < 4) {
|
|
523
|
-
return `0{0,${4 - group.length}}${group}`;
|
|
524
|
-
}
|
|
525
|
-
return group;
|
|
526
|
-
}
|
|
527
|
-
exports.ADDRESS_BOUNDARY = "[^A-Fa-f0-9:]";
|
|
528
|
-
function simpleRegularExpression(groups) {
|
|
529
|
-
const zeroIndexes = [];
|
|
530
|
-
groups.forEach((group, i) => {
|
|
531
|
-
const groupInteger = parseInt(group, 16);
|
|
532
|
-
if (groupInteger === 0) {
|
|
533
|
-
zeroIndexes.push(i);
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
const possibilities = zeroIndexes.map((zeroIndex) => groups.map((group, i) => {
|
|
537
|
-
if (i === zeroIndex) {
|
|
538
|
-
const elision = i === 0 || i === v6.GROUPS - 1 ? ":" : "";
|
|
539
|
-
return groupPossibilities([padGroup(group), elision]);
|
|
540
|
-
}
|
|
541
|
-
return padGroup(group);
|
|
542
|
-
}).join(":"));
|
|
543
|
-
possibilities.push(groups.map(padGroup).join(":"));
|
|
544
|
-
return groupPossibilities(possibilities);
|
|
545
|
-
}
|
|
546
|
-
function possibleElisions(elidedGroups, moreLeft, moreRight) {
|
|
547
|
-
const left = moreLeft ? "" : ":";
|
|
548
|
-
const right = moreRight ? "" : ":";
|
|
549
|
-
const possibilities = [];
|
|
550
|
-
if (!moreLeft && !moreRight) {
|
|
551
|
-
possibilities.push("::");
|
|
552
|
-
}
|
|
553
|
-
if (moreLeft && moreRight) {
|
|
554
|
-
possibilities.push("");
|
|
555
|
-
}
|
|
556
|
-
if (moreRight && !moreLeft || !moreRight && moreLeft) {
|
|
557
|
-
possibilities.push(":");
|
|
558
|
-
}
|
|
559
|
-
possibilities.push(`${left}(:0{1,4}){1,${elidedGroups - 1}}`);
|
|
560
|
-
possibilities.push(`(0{1,4}:){1,${elidedGroups - 1}}${right}`);
|
|
561
|
-
possibilities.push(`(0{1,4}:){${elidedGroups - 1}}0{1,4}`);
|
|
562
|
-
for (let groups = 1; groups < elidedGroups - 1; groups++) {
|
|
563
|
-
for (let position = 1; position < elidedGroups - groups; position++) {
|
|
564
|
-
possibilities.push(`(0{1,4}:){${position}}:(0{1,4}:){${elidedGroups - position - groups - 1}}0{1,4}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return groupPossibilities(possibilities);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// node_modules/ip-address/dist/ipv6.js
|
|
573
|
-
var require_ipv6 = __commonJS({
|
|
574
|
-
"node_modules/ip-address/dist/ipv6.js"(exports) {
|
|
575
|
-
"use strict";
|
|
576
|
-
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
577
|
-
if (k2 === void 0) k2 = k;
|
|
578
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
579
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
580
|
-
desc = { enumerable: true, get: function() {
|
|
581
|
-
return m[k];
|
|
582
|
-
} };
|
|
583
|
-
}
|
|
584
|
-
Object.defineProperty(o, k2, desc);
|
|
585
|
-
}) : (function(o, m, k, k2) {
|
|
586
|
-
if (k2 === void 0) k2 = k;
|
|
587
|
-
o[k2] = m[k];
|
|
588
|
-
}));
|
|
589
|
-
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
590
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
591
|
-
}) : function(o, v) {
|
|
592
|
-
o["default"] = v;
|
|
593
|
-
});
|
|
594
|
-
var __importStar = exports && exports.__importStar || function(mod) {
|
|
595
|
-
if (mod && mod.__esModule) return mod;
|
|
596
|
-
var result = {};
|
|
597
|
-
if (mod != null) {
|
|
598
|
-
for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
599
|
-
}
|
|
600
|
-
__setModuleDefault(result, mod);
|
|
601
|
-
return result;
|
|
602
|
-
};
|
|
603
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
604
|
-
exports.Address6 = void 0;
|
|
605
|
-
var common = __importStar(require_common());
|
|
606
|
-
var constants4 = __importStar(require_constants());
|
|
607
|
-
var constants6 = __importStar(require_constants2());
|
|
608
|
-
var helpers = __importStar(require_helpers());
|
|
609
|
-
var ipv4_1 = require_ipv4();
|
|
610
|
-
var regular_expressions_1 = require_regular_expressions();
|
|
611
|
-
var address_error_1 = require_address_error();
|
|
612
|
-
var common_1 = require_common();
|
|
613
|
-
function assert(condition) {
|
|
614
|
-
if (!condition) {
|
|
615
|
-
throw new Error("Assertion failed.");
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
function addCommas(number) {
|
|
619
|
-
const r = /(\d+)(\d{3})/;
|
|
620
|
-
while (r.test(number)) {
|
|
621
|
-
number = number.replace(r, "$1,$2");
|
|
622
|
-
}
|
|
623
|
-
return number;
|
|
624
|
-
}
|
|
625
|
-
function spanLeadingZeroes4(n) {
|
|
626
|
-
n = n.replace(/^(0{1,})([1-9]+)$/, '<span class="parse-error">$1</span>$2');
|
|
627
|
-
n = n.replace(/^(0{1,})(0)$/, '<span class="parse-error">$1</span>$2');
|
|
628
|
-
return n;
|
|
629
|
-
}
|
|
630
|
-
function compact(address, slice) {
|
|
631
|
-
const s1 = [];
|
|
632
|
-
const s2 = [];
|
|
633
|
-
let i;
|
|
634
|
-
for (i = 0; i < address.length; i++) {
|
|
635
|
-
if (i < slice[0]) {
|
|
636
|
-
s1.push(address[i]);
|
|
637
|
-
} else if (i > slice[1]) {
|
|
638
|
-
s2.push(address[i]);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return s1.concat(["compact"]).concat(s2);
|
|
642
|
-
}
|
|
643
|
-
function paddedHex(octet) {
|
|
644
|
-
return parseInt(octet, 16).toString(16).padStart(4, "0");
|
|
645
|
-
}
|
|
646
|
-
function unsignByte(b) {
|
|
647
|
-
return b & 255;
|
|
648
|
-
}
|
|
649
|
-
var Address62 = class _Address6 {
|
|
650
|
-
constructor(address, optionalGroups) {
|
|
651
|
-
this.addressMinusSuffix = "";
|
|
652
|
-
this.parsedSubnet = "";
|
|
653
|
-
this.subnet = "/128";
|
|
654
|
-
this.subnetMask = 128;
|
|
655
|
-
this.v4 = false;
|
|
656
|
-
this.zone = "";
|
|
657
|
-
this.isInSubnet = common.isInSubnet;
|
|
658
|
-
this.isCorrect = common.isCorrect(constants6.BITS);
|
|
659
|
-
if (optionalGroups === void 0) {
|
|
660
|
-
this.groups = constants6.GROUPS;
|
|
661
|
-
} else {
|
|
662
|
-
this.groups = optionalGroups;
|
|
663
|
-
}
|
|
664
|
-
this.address = address;
|
|
665
|
-
const subnet = constants6.RE_SUBNET_STRING.exec(address);
|
|
666
|
-
if (subnet) {
|
|
667
|
-
this.parsedSubnet = subnet[0].replace("/", "");
|
|
668
|
-
this.subnetMask = parseInt(this.parsedSubnet, 10);
|
|
669
|
-
this.subnet = `/${this.subnetMask}`;
|
|
670
|
-
if (Number.isNaN(this.subnetMask) || this.subnetMask < 0 || this.subnetMask > constants6.BITS) {
|
|
671
|
-
throw new address_error_1.AddressError("Invalid subnet mask.");
|
|
672
|
-
}
|
|
673
|
-
address = address.replace(constants6.RE_SUBNET_STRING, "");
|
|
674
|
-
} else if (/\//.test(address)) {
|
|
675
|
-
throw new address_error_1.AddressError("Invalid subnet mask.");
|
|
676
|
-
}
|
|
677
|
-
const zone = constants6.RE_ZONE_STRING.exec(address);
|
|
678
|
-
if (zone) {
|
|
679
|
-
this.zone = zone[0];
|
|
680
|
-
address = address.replace(constants6.RE_ZONE_STRING, "");
|
|
681
|
-
}
|
|
682
|
-
this.addressMinusSuffix = address;
|
|
683
|
-
this.parsedAddress = this.parse(this.addressMinusSuffix);
|
|
684
|
-
}
|
|
685
|
-
static isValid(address) {
|
|
686
|
-
try {
|
|
687
|
-
new _Address6(address);
|
|
688
|
-
return true;
|
|
689
|
-
} catch (e) {
|
|
690
|
-
return false;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Convert a BigInt to a v6 address object
|
|
695
|
-
* @memberof Address6
|
|
696
|
-
* @static
|
|
697
|
-
* @param {bigint} bigInt - a BigInt to convert
|
|
698
|
-
* @returns {Address6}
|
|
699
|
-
* @example
|
|
700
|
-
* var bigInt = BigInt('1000000000000');
|
|
701
|
-
* var address = Address6.fromBigInt(bigInt);
|
|
702
|
-
* address.correctForm(); // '::e8:d4a5:1000'
|
|
703
|
-
*/
|
|
704
|
-
static fromBigInt(bigInt) {
|
|
705
|
-
const hex = bigInt.toString(16).padStart(32, "0");
|
|
706
|
-
const groups = [];
|
|
707
|
-
let i;
|
|
708
|
-
for (i = 0; i < constants6.GROUPS; i++) {
|
|
709
|
-
groups.push(hex.slice(i * 4, (i + 1) * 4));
|
|
710
|
-
}
|
|
711
|
-
return new _Address6(groups.join(":"));
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Convert a URL (with optional port number) to an address object
|
|
715
|
-
* @memberof Address6
|
|
716
|
-
* @static
|
|
717
|
-
* @param {string} url - a URL with optional port number
|
|
718
|
-
* @example
|
|
719
|
-
* var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/');
|
|
720
|
-
* addressAndPort.address.correctForm(); // 'ffff::'
|
|
721
|
-
* addressAndPort.port; // 8080
|
|
722
|
-
*/
|
|
723
|
-
static fromURL(url) {
|
|
724
|
-
let host;
|
|
725
|
-
let port = null;
|
|
726
|
-
let result;
|
|
727
|
-
if (url.indexOf("[") !== -1 && url.indexOf("]:") !== -1) {
|
|
728
|
-
result = constants6.RE_URL_WITH_PORT.exec(url);
|
|
729
|
-
if (result === null) {
|
|
730
|
-
return {
|
|
731
|
-
error: "failed to parse address with port",
|
|
732
|
-
address: null,
|
|
733
|
-
port: null
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
host = result[1];
|
|
737
|
-
port = result[2];
|
|
738
|
-
} else if (url.indexOf("/") !== -1) {
|
|
739
|
-
url = url.replace(/^[a-z0-9]+:\/\//, "");
|
|
740
|
-
result = constants6.RE_URL.exec(url);
|
|
741
|
-
if (result === null) {
|
|
742
|
-
return {
|
|
743
|
-
error: "failed to parse address from URL",
|
|
744
|
-
address: null,
|
|
745
|
-
port: null
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
host = result[1];
|
|
749
|
-
} else {
|
|
750
|
-
host = url;
|
|
751
|
-
}
|
|
752
|
-
if (port) {
|
|
753
|
-
port = parseInt(port, 10);
|
|
754
|
-
if (port < 0 || port > 65536) {
|
|
755
|
-
port = null;
|
|
756
|
-
}
|
|
757
|
-
} else {
|
|
758
|
-
port = null;
|
|
759
|
-
}
|
|
760
|
-
return {
|
|
761
|
-
address: new _Address6(host),
|
|
762
|
-
port
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Create an IPv6-mapped address given an IPv4 address
|
|
767
|
-
* @memberof Address6
|
|
768
|
-
* @static
|
|
769
|
-
* @param {string} address - An IPv4 address string
|
|
770
|
-
* @returns {Address6}
|
|
771
|
-
* @example
|
|
772
|
-
* var address = Address6.fromAddress4('192.168.0.1');
|
|
773
|
-
* address.correctForm(); // '::ffff:c0a8:1'
|
|
774
|
-
* address.to4in6(); // '::ffff:192.168.0.1'
|
|
775
|
-
*/
|
|
776
|
-
static fromAddress4(address) {
|
|
777
|
-
const address4 = new ipv4_1.Address4(address);
|
|
778
|
-
const mask6 = constants6.BITS - (constants4.BITS - address4.subnetMask);
|
|
779
|
-
return new _Address6(`::ffff:${address4.correctForm()}/${mask6}`);
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* Return an address from ip6.arpa form
|
|
783
|
-
* @memberof Address6
|
|
784
|
-
* @static
|
|
785
|
-
* @param {string} arpaFormAddress - an 'ip6.arpa' form address
|
|
786
|
-
* @returns {Adress6}
|
|
787
|
-
* @example
|
|
788
|
-
* var address = Address6.fromArpa(e.f.f.f.3.c.2.6.f.f.f.e.6.6.8.e.1.0.6.7.9.4.e.c.0.0.0.0.1.0.0.2.ip6.arpa.)
|
|
789
|
-
* address.correctForm(); // '2001:0:ce49:7601:e866:efff:62c3:fffe'
|
|
790
|
-
*/
|
|
791
|
-
static fromArpa(arpaFormAddress) {
|
|
792
|
-
let address = arpaFormAddress.replace(/(\.ip6\.arpa)?\.$/, "");
|
|
793
|
-
const semicolonAmount = 7;
|
|
794
|
-
if (address.length !== 63) {
|
|
795
|
-
throw new address_error_1.AddressError("Invalid 'ip6.arpa' form.");
|
|
796
|
-
}
|
|
797
|
-
const parts = address.split(".").reverse();
|
|
798
|
-
for (let i = semicolonAmount; i > 0; i--) {
|
|
799
|
-
const insertIndex = i * 4;
|
|
800
|
-
parts.splice(insertIndex, 0, ":");
|
|
801
|
-
}
|
|
802
|
-
address = parts.join("");
|
|
803
|
-
return new _Address6(address);
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Return the Microsoft UNC transcription of the address
|
|
807
|
-
* @memberof Address6
|
|
808
|
-
* @instance
|
|
809
|
-
* @returns {String} the Microsoft UNC transcription of the address
|
|
810
|
-
*/
|
|
811
|
-
microsoftTranscription() {
|
|
812
|
-
return `${this.correctForm().replace(/:/g, "-")}.ipv6-literal.net`;
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Return the first n bits of the address, defaulting to the subnet mask
|
|
816
|
-
* @memberof Address6
|
|
817
|
-
* @instance
|
|
818
|
-
* @param {number} [mask=subnet] - the number of bits to mask
|
|
819
|
-
* @returns {String} the first n bits of the address as a string
|
|
820
|
-
*/
|
|
821
|
-
mask(mask = this.subnetMask) {
|
|
822
|
-
return this.getBitsBase2(0, mask);
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Return the number of possible subnets of a given size in the address
|
|
826
|
-
* @memberof Address6
|
|
827
|
-
* @instance
|
|
828
|
-
* @param {number} [subnetSize=128] - the subnet size
|
|
829
|
-
* @returns {String}
|
|
830
|
-
*/
|
|
831
|
-
// TODO: probably useful to have a numeric version of this too
|
|
832
|
-
possibleSubnets(subnetSize = 128) {
|
|
833
|
-
const availableBits = constants6.BITS - this.subnetMask;
|
|
834
|
-
const subnetBits = Math.abs(subnetSize - constants6.BITS);
|
|
835
|
-
const subnetPowers = availableBits - subnetBits;
|
|
836
|
-
if (subnetPowers < 0) {
|
|
837
|
-
return "0";
|
|
838
|
-
}
|
|
839
|
-
return addCommas((BigInt("2") ** BigInt(subnetPowers)).toString(10));
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Helper function getting start address.
|
|
843
|
-
* @memberof Address6
|
|
844
|
-
* @instance
|
|
845
|
-
* @returns {bigint}
|
|
846
|
-
*/
|
|
847
|
-
_startAddress() {
|
|
848
|
-
return BigInt(`0b${this.mask() + "0".repeat(constants6.BITS - this.subnetMask)}`);
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* The first address in the range given by this address' subnet
|
|
852
|
-
* Often referred to as the Network Address.
|
|
853
|
-
* @memberof Address6
|
|
854
|
-
* @instance
|
|
855
|
-
* @returns {Address6}
|
|
856
|
-
*/
|
|
857
|
-
startAddress() {
|
|
858
|
-
return _Address6.fromBigInt(this._startAddress());
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* The first host address in the range given by this address's subnet ie
|
|
862
|
-
* the first address after the Network Address
|
|
863
|
-
* @memberof Address6
|
|
864
|
-
* @instance
|
|
865
|
-
* @returns {Address6}
|
|
866
|
-
*/
|
|
867
|
-
startAddressExclusive() {
|
|
868
|
-
const adjust = BigInt("1");
|
|
869
|
-
return _Address6.fromBigInt(this._startAddress() + adjust);
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Helper function getting end address.
|
|
873
|
-
* @memberof Address6
|
|
874
|
-
* @instance
|
|
875
|
-
* @returns {bigint}
|
|
876
|
-
*/
|
|
877
|
-
_endAddress() {
|
|
878
|
-
return BigInt(`0b${this.mask() + "1".repeat(constants6.BITS - this.subnetMask)}`);
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* The last address in the range given by this address' subnet
|
|
882
|
-
* Often referred to as the Broadcast
|
|
883
|
-
* @memberof Address6
|
|
884
|
-
* @instance
|
|
885
|
-
* @returns {Address6}
|
|
886
|
-
*/
|
|
887
|
-
endAddress() {
|
|
888
|
-
return _Address6.fromBigInt(this._endAddress());
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* The last host address in the range given by this address's subnet ie
|
|
892
|
-
* the last address prior to the Broadcast Address
|
|
893
|
-
* @memberof Address6
|
|
894
|
-
* @instance
|
|
895
|
-
* @returns {Address6}
|
|
896
|
-
*/
|
|
897
|
-
endAddressExclusive() {
|
|
898
|
-
const adjust = BigInt("1");
|
|
899
|
-
return _Address6.fromBigInt(this._endAddress() - adjust);
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Return the scope of the address
|
|
903
|
-
* @memberof Address6
|
|
904
|
-
* @instance
|
|
905
|
-
* @returns {String}
|
|
906
|
-
*/
|
|
907
|
-
getScope() {
|
|
908
|
-
let scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)];
|
|
909
|
-
if (this.getType() === "Global unicast" && scope !== "Link local") {
|
|
910
|
-
scope = "Global";
|
|
911
|
-
}
|
|
912
|
-
return scope || "Unknown";
|
|
913
|
-
}
|
|
914
|
-
/**
|
|
915
|
-
* Return the type of the address
|
|
916
|
-
* @memberof Address6
|
|
917
|
-
* @instance
|
|
918
|
-
* @returns {String}
|
|
919
|
-
*/
|
|
920
|
-
getType() {
|
|
921
|
-
for (const subnet of Object.keys(constants6.TYPES)) {
|
|
922
|
-
if (this.isInSubnet(new _Address6(subnet))) {
|
|
923
|
-
return constants6.TYPES[subnet];
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return "Global unicast";
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Return the bits in the given range as a BigInt
|
|
930
|
-
* @memberof Address6
|
|
931
|
-
* @instance
|
|
932
|
-
* @returns {bigint}
|
|
933
|
-
*/
|
|
934
|
-
getBits(start, end) {
|
|
935
|
-
return BigInt(`0b${this.getBitsBase2(start, end)}`);
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Return the bits in the given range as a base-2 string
|
|
939
|
-
* @memberof Address6
|
|
940
|
-
* @instance
|
|
941
|
-
* @returns {String}
|
|
942
|
-
*/
|
|
943
|
-
getBitsBase2(start, end) {
|
|
944
|
-
return this.binaryZeroPad().slice(start, end);
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Return the bits in the given range as a base-16 string
|
|
948
|
-
* @memberof Address6
|
|
949
|
-
* @instance
|
|
950
|
-
* @returns {String}
|
|
951
|
-
*/
|
|
952
|
-
getBitsBase16(start, end) {
|
|
953
|
-
const length = end - start;
|
|
954
|
-
if (length % 4 !== 0) {
|
|
955
|
-
throw new Error("Length of bits to retrieve must be divisible by four");
|
|
956
|
-
}
|
|
957
|
-
return this.getBits(start, end).toString(16).padStart(length / 4, "0");
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* Return the bits that are set past the subnet mask length
|
|
961
|
-
* @memberof Address6
|
|
962
|
-
* @instance
|
|
963
|
-
* @returns {String}
|
|
964
|
-
*/
|
|
965
|
-
getBitsPastSubnet() {
|
|
966
|
-
return this.getBitsBase2(this.subnetMask, constants6.BITS);
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Return the reversed ip6.arpa form of the address
|
|
970
|
-
* @memberof Address6
|
|
971
|
-
* @param {Object} options
|
|
972
|
-
* @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix
|
|
973
|
-
* @instance
|
|
974
|
-
* @returns {String}
|
|
975
|
-
*/
|
|
976
|
-
reverseForm(options) {
|
|
977
|
-
if (!options) {
|
|
978
|
-
options = {};
|
|
979
|
-
}
|
|
980
|
-
const characters = Math.floor(this.subnetMask / 4);
|
|
981
|
-
const reversed = this.canonicalForm().replace(/:/g, "").split("").slice(0, characters).reverse().join(".");
|
|
982
|
-
if (characters > 0) {
|
|
983
|
-
if (options.omitSuffix) {
|
|
984
|
-
return reversed;
|
|
985
|
-
}
|
|
986
|
-
return `${reversed}.ip6.arpa.`;
|
|
987
|
-
}
|
|
988
|
-
if (options.omitSuffix) {
|
|
989
|
-
return "";
|
|
990
|
-
}
|
|
991
|
-
return "ip6.arpa.";
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Return the correct form of the address
|
|
995
|
-
* @memberof Address6
|
|
996
|
-
* @instance
|
|
997
|
-
* @returns {String}
|
|
998
|
-
*/
|
|
999
|
-
correctForm() {
|
|
1000
|
-
let i;
|
|
1001
|
-
let groups = [];
|
|
1002
|
-
let zeroCounter = 0;
|
|
1003
|
-
const zeroes = [];
|
|
1004
|
-
for (i = 0; i < this.parsedAddress.length; i++) {
|
|
1005
|
-
const value = parseInt(this.parsedAddress[i], 16);
|
|
1006
|
-
if (value === 0) {
|
|
1007
|
-
zeroCounter++;
|
|
1008
|
-
}
|
|
1009
|
-
if (value !== 0 && zeroCounter > 0) {
|
|
1010
|
-
if (zeroCounter > 1) {
|
|
1011
|
-
zeroes.push([i - zeroCounter, i - 1]);
|
|
1012
|
-
}
|
|
1013
|
-
zeroCounter = 0;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
if (zeroCounter > 1) {
|
|
1017
|
-
zeroes.push([this.parsedAddress.length - zeroCounter, this.parsedAddress.length - 1]);
|
|
1018
|
-
}
|
|
1019
|
-
const zeroLengths = zeroes.map((n) => n[1] - n[0] + 1);
|
|
1020
|
-
if (zeroes.length > 0) {
|
|
1021
|
-
const index = zeroLengths.indexOf(Math.max(...zeroLengths));
|
|
1022
|
-
groups = compact(this.parsedAddress, zeroes[index]);
|
|
1023
|
-
} else {
|
|
1024
|
-
groups = this.parsedAddress;
|
|
1025
|
-
}
|
|
1026
|
-
for (i = 0; i < groups.length; i++) {
|
|
1027
|
-
if (groups[i] !== "compact") {
|
|
1028
|
-
groups[i] = parseInt(groups[i], 16).toString(16);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
let correct = groups.join(":");
|
|
1032
|
-
correct = correct.replace(/^compact$/, "::");
|
|
1033
|
-
correct = correct.replace(/(^compact)|(compact$)/, ":");
|
|
1034
|
-
correct = correct.replace(/compact/, "");
|
|
1035
|
-
return correct;
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Return a zero-padded base-2 string representation of the address
|
|
1039
|
-
* @memberof Address6
|
|
1040
|
-
* @instance
|
|
1041
|
-
* @returns {String}
|
|
1042
|
-
* @example
|
|
1043
|
-
* var address = new Address6('2001:4860:4001:803::1011');
|
|
1044
|
-
* address.binaryZeroPad();
|
|
1045
|
-
* // '0010000000000001010010000110000001000000000000010000100000000011
|
|
1046
|
-
* // 0000000000000000000000000000000000000000000000000001000000010001'
|
|
1047
|
-
*/
|
|
1048
|
-
binaryZeroPad() {
|
|
1049
|
-
return this.bigInt().toString(2).padStart(constants6.BITS, "0");
|
|
1050
|
-
}
|
|
1051
|
-
// TODO: Improve the semantics of this helper function
|
|
1052
|
-
parse4in6(address) {
|
|
1053
|
-
const groups = address.split(":");
|
|
1054
|
-
const lastGroup = groups.slice(-1)[0];
|
|
1055
|
-
const address4 = lastGroup.match(constants4.RE_ADDRESS);
|
|
1056
|
-
if (address4) {
|
|
1057
|
-
this.parsedAddress4 = address4[0];
|
|
1058
|
-
this.address4 = new ipv4_1.Address4(this.parsedAddress4);
|
|
1059
|
-
for (let i = 0; i < this.address4.groups; i++) {
|
|
1060
|
-
if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) {
|
|
1061
|
-
throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", address.replace(constants4.RE_ADDRESS, this.address4.parsedAddress.map(spanLeadingZeroes4).join(".")));
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
this.v4 = true;
|
|
1065
|
-
groups[groups.length - 1] = this.address4.toGroup6();
|
|
1066
|
-
address = groups.join(":");
|
|
1067
|
-
}
|
|
1068
|
-
return address;
|
|
1069
|
-
}
|
|
1070
|
-
// TODO: Make private?
|
|
1071
|
-
parse(address) {
|
|
1072
|
-
address = this.parse4in6(address);
|
|
1073
|
-
const badCharacters = address.match(constants6.RE_BAD_CHARACTERS);
|
|
1074
|
-
if (badCharacters) {
|
|
1075
|
-
throw new address_error_1.AddressError(`Bad character${badCharacters.length > 1 ? "s" : ""} detected in address: ${badCharacters.join("")}`, address.replace(constants6.RE_BAD_CHARACTERS, '<span class="parse-error">$1</span>'));
|
|
1076
|
-
}
|
|
1077
|
-
const badAddress = address.match(constants6.RE_BAD_ADDRESS);
|
|
1078
|
-
if (badAddress) {
|
|
1079
|
-
throw new address_error_1.AddressError(`Address failed regex: ${badAddress.join("")}`, address.replace(constants6.RE_BAD_ADDRESS, '<span class="parse-error">$1</span>'));
|
|
1080
|
-
}
|
|
1081
|
-
let groups = [];
|
|
1082
|
-
const halves = address.split("::");
|
|
1083
|
-
if (halves.length === 2) {
|
|
1084
|
-
let first = halves[0].split(":");
|
|
1085
|
-
let last = halves[1].split(":");
|
|
1086
|
-
if (first.length === 1 && first[0] === "") {
|
|
1087
|
-
first = [];
|
|
1088
|
-
}
|
|
1089
|
-
if (last.length === 1 && last[0] === "") {
|
|
1090
|
-
last = [];
|
|
1091
|
-
}
|
|
1092
|
-
const remaining = this.groups - (first.length + last.length);
|
|
1093
|
-
if (!remaining) {
|
|
1094
|
-
throw new address_error_1.AddressError("Error parsing groups");
|
|
1095
|
-
}
|
|
1096
|
-
this.elidedGroups = remaining;
|
|
1097
|
-
this.elisionBegin = first.length;
|
|
1098
|
-
this.elisionEnd = first.length + this.elidedGroups;
|
|
1099
|
-
groups = groups.concat(first);
|
|
1100
|
-
for (let i = 0; i < remaining; i++) {
|
|
1101
|
-
groups.push("0");
|
|
1102
|
-
}
|
|
1103
|
-
groups = groups.concat(last);
|
|
1104
|
-
} else if (halves.length === 1) {
|
|
1105
|
-
groups = address.split(":");
|
|
1106
|
-
this.elidedGroups = 0;
|
|
1107
|
-
} else {
|
|
1108
|
-
throw new address_error_1.AddressError("Too many :: groups found");
|
|
1109
|
-
}
|
|
1110
|
-
groups = groups.map((group) => parseInt(group, 16).toString(16));
|
|
1111
|
-
if (groups.length !== this.groups) {
|
|
1112
|
-
throw new address_error_1.AddressError("Incorrect number of groups found");
|
|
1113
|
-
}
|
|
1114
|
-
return groups;
|
|
1115
|
-
}
|
|
1116
|
-
/**
|
|
1117
|
-
* Return the canonical form of the address
|
|
1118
|
-
* @memberof Address6
|
|
1119
|
-
* @instance
|
|
1120
|
-
* @returns {String}
|
|
1121
|
-
*/
|
|
1122
|
-
canonicalForm() {
|
|
1123
|
-
return this.parsedAddress.map(paddedHex).join(":");
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Return the decimal form of the address
|
|
1127
|
-
* @memberof Address6
|
|
1128
|
-
* @instance
|
|
1129
|
-
* @returns {String}
|
|
1130
|
-
*/
|
|
1131
|
-
decimal() {
|
|
1132
|
-
return this.parsedAddress.map((n) => parseInt(n, 16).toString(10).padStart(5, "0")).join(":");
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Return the address as a BigInt
|
|
1136
|
-
* @memberof Address6
|
|
1137
|
-
* @instance
|
|
1138
|
-
* @returns {bigint}
|
|
1139
|
-
*/
|
|
1140
|
-
bigInt() {
|
|
1141
|
-
return BigInt(`0x${this.parsedAddress.map(paddedHex).join("")}`);
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Return the last two groups of this address as an IPv4 address string
|
|
1145
|
-
* @memberof Address6
|
|
1146
|
-
* @instance
|
|
1147
|
-
* @returns {Address4}
|
|
1148
|
-
* @example
|
|
1149
|
-
* var address = new Address6('2001:4860:4001::1825:bf11');
|
|
1150
|
-
* address.to4().correctForm(); // '24.37.191.17'
|
|
1151
|
-
*/
|
|
1152
|
-
to4() {
|
|
1153
|
-
const binary = this.binaryZeroPad().split("");
|
|
1154
|
-
return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join("")}`).toString(16));
|
|
1155
|
-
}
|
|
1156
|
-
/**
|
|
1157
|
-
* Return the v4-in-v6 form of the address
|
|
1158
|
-
* @memberof Address6
|
|
1159
|
-
* @instance
|
|
1160
|
-
* @returns {String}
|
|
1161
|
-
*/
|
|
1162
|
-
to4in6() {
|
|
1163
|
-
const address4 = this.to4();
|
|
1164
|
-
const address6 = new _Address6(this.parsedAddress.slice(0, 6).join(":"), 6);
|
|
1165
|
-
const correct = address6.correctForm();
|
|
1166
|
-
let infix = "";
|
|
1167
|
-
if (!/:$/.test(correct)) {
|
|
1168
|
-
infix = ":";
|
|
1169
|
-
}
|
|
1170
|
-
return correct + infix + address4.address;
|
|
1171
|
-
}
|
|
1172
|
-
/**
|
|
1173
|
-
* Return an object containing the Teredo properties of the address
|
|
1174
|
-
* @memberof Address6
|
|
1175
|
-
* @instance
|
|
1176
|
-
* @returns {Object}
|
|
1177
|
-
*/
|
|
1178
|
-
inspectTeredo() {
|
|
1179
|
-
const prefix = this.getBitsBase16(0, 32);
|
|
1180
|
-
const bitsForUdpPort = this.getBits(80, 96);
|
|
1181
|
-
const udpPort = (bitsForUdpPort ^ BigInt("0xffff")).toString();
|
|
1182
|
-
const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64));
|
|
1183
|
-
const bitsForClient4 = this.getBits(96, 128);
|
|
1184
|
-
const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt("0xffffffff")).toString(16));
|
|
1185
|
-
const flagsBase2 = this.getBitsBase2(64, 80);
|
|
1186
|
-
const coneNat = (0, common_1.testBit)(flagsBase2, 15);
|
|
1187
|
-
const reserved = (0, common_1.testBit)(flagsBase2, 14);
|
|
1188
|
-
const groupIndividual = (0, common_1.testBit)(flagsBase2, 8);
|
|
1189
|
-
const universalLocal = (0, common_1.testBit)(flagsBase2, 9);
|
|
1190
|
-
const nonce = BigInt(`0b${flagsBase2.slice(2, 6) + flagsBase2.slice(8, 16)}`).toString(10);
|
|
1191
|
-
return {
|
|
1192
|
-
prefix: `${prefix.slice(0, 4)}:${prefix.slice(4, 8)}`,
|
|
1193
|
-
server4: server4.address,
|
|
1194
|
-
client4: client4.address,
|
|
1195
|
-
flags: flagsBase2,
|
|
1196
|
-
coneNat,
|
|
1197
|
-
microsoft: {
|
|
1198
|
-
reserved,
|
|
1199
|
-
universalLocal,
|
|
1200
|
-
groupIndividual,
|
|
1201
|
-
nonce
|
|
1202
|
-
},
|
|
1203
|
-
udpPort
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
/**
|
|
1207
|
-
* Return an object containing the 6to4 properties of the address
|
|
1208
|
-
* @memberof Address6
|
|
1209
|
-
* @instance
|
|
1210
|
-
* @returns {Object}
|
|
1211
|
-
*/
|
|
1212
|
-
inspect6to4() {
|
|
1213
|
-
const prefix = this.getBitsBase16(0, 16);
|
|
1214
|
-
const gateway = ipv4_1.Address4.fromHex(this.getBitsBase16(16, 48));
|
|
1215
|
-
return {
|
|
1216
|
-
prefix: prefix.slice(0, 4),
|
|
1217
|
-
gateway: gateway.address
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Return a v6 6to4 address from a v6 v4inv6 address
|
|
1222
|
-
* @memberof Address6
|
|
1223
|
-
* @instance
|
|
1224
|
-
* @returns {Address6}
|
|
1225
|
-
*/
|
|
1226
|
-
to6to4() {
|
|
1227
|
-
if (!this.is4()) {
|
|
1228
|
-
return null;
|
|
1229
|
-
}
|
|
1230
|
-
const addr6to4 = [
|
|
1231
|
-
"2002",
|
|
1232
|
-
this.getBitsBase16(96, 112),
|
|
1233
|
-
this.getBitsBase16(112, 128),
|
|
1234
|
-
"",
|
|
1235
|
-
"/16"
|
|
1236
|
-
].join(":");
|
|
1237
|
-
return new _Address6(addr6to4);
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Return a byte array
|
|
1241
|
-
* @memberof Address6
|
|
1242
|
-
* @instance
|
|
1243
|
-
* @returns {Array}
|
|
1244
|
-
*/
|
|
1245
|
-
toByteArray() {
|
|
1246
|
-
const valueWithoutPadding = this.bigInt().toString(16);
|
|
1247
|
-
const leadingPad = "0".repeat(valueWithoutPadding.length % 2);
|
|
1248
|
-
const value = `${leadingPad}${valueWithoutPadding}`;
|
|
1249
|
-
const bytes = [];
|
|
1250
|
-
for (let i = 0, length = value.length; i < length; i += 2) {
|
|
1251
|
-
bytes.push(parseInt(value.substring(i, i + 2), 16));
|
|
1252
|
-
}
|
|
1253
|
-
return bytes;
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Return an unsigned byte array
|
|
1257
|
-
* @memberof Address6
|
|
1258
|
-
* @instance
|
|
1259
|
-
* @returns {Array}
|
|
1260
|
-
*/
|
|
1261
|
-
toUnsignedByteArray() {
|
|
1262
|
-
return this.toByteArray().map(unsignByte);
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Convert a byte array to an Address6 object
|
|
1266
|
-
* @memberof Address6
|
|
1267
|
-
* @static
|
|
1268
|
-
* @returns {Address6}
|
|
1269
|
-
*/
|
|
1270
|
-
static fromByteArray(bytes) {
|
|
1271
|
-
return this.fromUnsignedByteArray(bytes.map(unsignByte));
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Convert an unsigned byte array to an Address6 object
|
|
1275
|
-
* @memberof Address6
|
|
1276
|
-
* @static
|
|
1277
|
-
* @returns {Address6}
|
|
1278
|
-
*/
|
|
1279
|
-
static fromUnsignedByteArray(bytes) {
|
|
1280
|
-
const BYTE_MAX = BigInt("256");
|
|
1281
|
-
let result = BigInt("0");
|
|
1282
|
-
let multiplier = BigInt("1");
|
|
1283
|
-
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
1284
|
-
result += multiplier * BigInt(bytes[i].toString(10));
|
|
1285
|
-
multiplier *= BYTE_MAX;
|
|
1286
|
-
}
|
|
1287
|
-
return _Address6.fromBigInt(result);
|
|
1288
|
-
}
|
|
1289
|
-
/**
|
|
1290
|
-
* Returns true if the address is in the canonical form, false otherwise
|
|
1291
|
-
* @memberof Address6
|
|
1292
|
-
* @instance
|
|
1293
|
-
* @returns {boolean}
|
|
1294
|
-
*/
|
|
1295
|
-
isCanonical() {
|
|
1296
|
-
return this.addressMinusSuffix === this.canonicalForm();
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Returns true if the address is a link local address, false otherwise
|
|
1300
|
-
* @memberof Address6
|
|
1301
|
-
* @instance
|
|
1302
|
-
* @returns {boolean}
|
|
1303
|
-
*/
|
|
1304
|
-
isLinkLocal() {
|
|
1305
|
-
if (this.getBitsBase2(0, 64) === "1111111010000000000000000000000000000000000000000000000000000000") {
|
|
1306
|
-
return true;
|
|
1307
|
-
}
|
|
1308
|
-
return false;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Returns true if the address is a multicast address, false otherwise
|
|
1312
|
-
* @memberof Address6
|
|
1313
|
-
* @instance
|
|
1314
|
-
* @returns {boolean}
|
|
1315
|
-
*/
|
|
1316
|
-
isMulticast() {
|
|
1317
|
-
return this.getType() === "Multicast";
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Returns true if the address is a v4-in-v6 address, false otherwise
|
|
1321
|
-
* @memberof Address6
|
|
1322
|
-
* @instance
|
|
1323
|
-
* @returns {boolean}
|
|
1324
|
-
*/
|
|
1325
|
-
is4() {
|
|
1326
|
-
return this.v4;
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Returns true if the address is a Teredo address, false otherwise
|
|
1330
|
-
* @memberof Address6
|
|
1331
|
-
* @instance
|
|
1332
|
-
* @returns {boolean}
|
|
1333
|
-
*/
|
|
1334
|
-
isTeredo() {
|
|
1335
|
-
return this.isInSubnet(new _Address6("2001::/32"));
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* Returns true if the address is a 6to4 address, false otherwise
|
|
1339
|
-
* @memberof Address6
|
|
1340
|
-
* @instance
|
|
1341
|
-
* @returns {boolean}
|
|
1342
|
-
*/
|
|
1343
|
-
is6to4() {
|
|
1344
|
-
return this.isInSubnet(new _Address6("2002::/16"));
|
|
1345
|
-
}
|
|
1346
|
-
/**
|
|
1347
|
-
* Returns true if the address is a loopback address, false otherwise
|
|
1348
|
-
* @memberof Address6
|
|
1349
|
-
* @instance
|
|
1350
|
-
* @returns {boolean}
|
|
1351
|
-
*/
|
|
1352
|
-
isLoopback() {
|
|
1353
|
-
return this.getType() === "Loopback";
|
|
1354
|
-
}
|
|
1355
|
-
// #endregion
|
|
1356
|
-
// #region HTML
|
|
1357
|
-
/**
|
|
1358
|
-
* @returns {String} the address in link form with a default port of 80
|
|
1359
|
-
*/
|
|
1360
|
-
href(optionalPort) {
|
|
1361
|
-
if (optionalPort === void 0) {
|
|
1362
|
-
optionalPort = "";
|
|
1363
|
-
} else {
|
|
1364
|
-
optionalPort = `:${optionalPort}`;
|
|
1365
|
-
}
|
|
1366
|
-
return `http://[${this.correctForm()}]${optionalPort}/`;
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* @returns {String} a link suitable for conveying the address via a URL hash
|
|
1370
|
-
*/
|
|
1371
|
-
link(options) {
|
|
1372
|
-
if (!options) {
|
|
1373
|
-
options = {};
|
|
1374
|
-
}
|
|
1375
|
-
if (options.className === void 0) {
|
|
1376
|
-
options.className = "";
|
|
1377
|
-
}
|
|
1378
|
-
if (options.prefix === void 0) {
|
|
1379
|
-
options.prefix = "/#address=";
|
|
1380
|
-
}
|
|
1381
|
-
if (options.v4 === void 0) {
|
|
1382
|
-
options.v4 = false;
|
|
1383
|
-
}
|
|
1384
|
-
let formFunction = this.correctForm;
|
|
1385
|
-
if (options.v4) {
|
|
1386
|
-
formFunction = this.to4in6;
|
|
1387
|
-
}
|
|
1388
|
-
const form = formFunction.call(this);
|
|
1389
|
-
if (options.className) {
|
|
1390
|
-
return `<a href="${options.prefix}${form}" class="${options.className}">${form}</a>`;
|
|
1391
|
-
}
|
|
1392
|
-
return `<a href="${options.prefix}${form}">${form}</a>`;
|
|
1393
|
-
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Groups an address
|
|
1396
|
-
* @returns {String}
|
|
1397
|
-
*/
|
|
1398
|
-
group() {
|
|
1399
|
-
if (this.elidedGroups === 0) {
|
|
1400
|
-
return helpers.simpleGroup(this.address).join(":");
|
|
1401
|
-
}
|
|
1402
|
-
assert(typeof this.elidedGroups === "number");
|
|
1403
|
-
assert(typeof this.elisionBegin === "number");
|
|
1404
|
-
const output = [];
|
|
1405
|
-
const [left, right] = this.address.split("::");
|
|
1406
|
-
if (left.length) {
|
|
1407
|
-
output.push(...helpers.simpleGroup(left));
|
|
1408
|
-
} else {
|
|
1409
|
-
output.push("");
|
|
1410
|
-
}
|
|
1411
|
-
const classes = ["hover-group"];
|
|
1412
|
-
for (let i = this.elisionBegin; i < this.elisionBegin + this.elidedGroups; i++) {
|
|
1413
|
-
classes.push(`group-${i}`);
|
|
1414
|
-
}
|
|
1415
|
-
output.push(`<span class="${classes.join(" ")}"></span>`);
|
|
1416
|
-
if (right.length) {
|
|
1417
|
-
output.push(...helpers.simpleGroup(right, this.elisionEnd));
|
|
1418
|
-
} else {
|
|
1419
|
-
output.push("");
|
|
1420
|
-
}
|
|
1421
|
-
if (this.is4()) {
|
|
1422
|
-
assert(this.address4 instanceof ipv4_1.Address4);
|
|
1423
|
-
output.pop();
|
|
1424
|
-
output.push(this.address4.groupForV6());
|
|
1425
|
-
}
|
|
1426
|
-
return output.join(":");
|
|
1427
|
-
}
|
|
1428
|
-
// #endregion
|
|
1429
|
-
// #region Regular expressions
|
|
1430
|
-
/**
|
|
1431
|
-
* Generate a regular expression string that can be used to find or validate
|
|
1432
|
-
* all variations of this address
|
|
1433
|
-
* @memberof Address6
|
|
1434
|
-
* @instance
|
|
1435
|
-
* @param {boolean} substringSearch
|
|
1436
|
-
* @returns {string}
|
|
1437
|
-
*/
|
|
1438
|
-
regularExpressionString(substringSearch = false) {
|
|
1439
|
-
let output = [];
|
|
1440
|
-
const address6 = new _Address6(this.correctForm());
|
|
1441
|
-
if (address6.elidedGroups === 0) {
|
|
1442
|
-
output.push((0, regular_expressions_1.simpleRegularExpression)(address6.parsedAddress));
|
|
1443
|
-
} else if (address6.elidedGroups === constants6.GROUPS) {
|
|
1444
|
-
output.push((0, regular_expressions_1.possibleElisions)(constants6.GROUPS));
|
|
1445
|
-
} else {
|
|
1446
|
-
const halves = address6.address.split("::");
|
|
1447
|
-
if (halves[0].length) {
|
|
1448
|
-
output.push((0, regular_expressions_1.simpleRegularExpression)(halves[0].split(":")));
|
|
1449
|
-
}
|
|
1450
|
-
assert(typeof address6.elidedGroups === "number");
|
|
1451
|
-
output.push((0, regular_expressions_1.possibleElisions)(address6.elidedGroups, halves[0].length !== 0, halves[1].length !== 0));
|
|
1452
|
-
if (halves[1].length) {
|
|
1453
|
-
output.push((0, regular_expressions_1.simpleRegularExpression)(halves[1].split(":")));
|
|
1454
|
-
}
|
|
1455
|
-
output = [output.join(":")];
|
|
1456
|
-
}
|
|
1457
|
-
if (!substringSearch) {
|
|
1458
|
-
output = [
|
|
1459
|
-
"(?=^|",
|
|
1460
|
-
regular_expressions_1.ADDRESS_BOUNDARY,
|
|
1461
|
-
"|[^\\w\\:])(",
|
|
1462
|
-
...output,
|
|
1463
|
-
")(?=[^\\w\\:]|",
|
|
1464
|
-
regular_expressions_1.ADDRESS_BOUNDARY,
|
|
1465
|
-
"|$)"
|
|
1466
|
-
];
|
|
1467
|
-
}
|
|
1468
|
-
return output.join("");
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Generate a regular expression that can be used to find or validate all
|
|
1472
|
-
* variations of this address.
|
|
1473
|
-
* @memberof Address6
|
|
1474
|
-
* @instance
|
|
1475
|
-
* @param {boolean} substringSearch
|
|
1476
|
-
* @returns {RegExp}
|
|
1477
|
-
*/
|
|
1478
|
-
regularExpression(substringSearch = false) {
|
|
1479
|
-
return new RegExp(this.regularExpressionString(substringSearch), "i");
|
|
1480
|
-
}
|
|
1481
|
-
};
|
|
1482
|
-
exports.Address6 = Address62;
|
|
1483
|
-
}
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
// node_modules/ip-address/dist/ip-address.js
|
|
1487
|
-
var require_ip_address = __commonJS({
|
|
1488
|
-
"node_modules/ip-address/dist/ip-address.js"(exports) {
|
|
1489
|
-
"use strict";
|
|
1490
|
-
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
1491
|
-
if (k2 === void 0) k2 = k;
|
|
1492
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
1493
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
1494
|
-
desc = { enumerable: true, get: function() {
|
|
1495
|
-
return m[k];
|
|
1496
|
-
} };
|
|
1497
|
-
}
|
|
1498
|
-
Object.defineProperty(o, k2, desc);
|
|
1499
|
-
}) : (function(o, m, k, k2) {
|
|
1500
|
-
if (k2 === void 0) k2 = k;
|
|
1501
|
-
o[k2] = m[k];
|
|
1502
|
-
}));
|
|
1503
|
-
var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
1504
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
1505
|
-
}) : function(o, v) {
|
|
1506
|
-
o["default"] = v;
|
|
1507
|
-
});
|
|
1508
|
-
var __importStar = exports && exports.__importStar || function(mod) {
|
|
1509
|
-
if (mod && mod.__esModule) return mod;
|
|
1510
|
-
var result = {};
|
|
1511
|
-
if (mod != null) {
|
|
1512
|
-
for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
1513
|
-
}
|
|
1514
|
-
__setModuleDefault(result, mod);
|
|
1515
|
-
return result;
|
|
1516
|
-
};
|
|
1517
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1518
|
-
exports.v6 = exports.AddressError = exports.Address6 = exports.Address4 = void 0;
|
|
1519
|
-
var ipv4_1 = require_ipv4();
|
|
1520
|
-
Object.defineProperty(exports, "Address4", { enumerable: true, get: function() {
|
|
1521
|
-
return ipv4_1.Address4;
|
|
1522
|
-
} });
|
|
1523
|
-
var ipv6_1 = require_ipv6();
|
|
1524
|
-
Object.defineProperty(exports, "Address6", { enumerable: true, get: function() {
|
|
1525
|
-
return ipv6_1.Address6;
|
|
1526
|
-
} });
|
|
1527
|
-
var address_error_1 = require_address_error();
|
|
1528
|
-
Object.defineProperty(exports, "AddressError", { enumerable: true, get: function() {
|
|
1529
|
-
return address_error_1.AddressError;
|
|
1530
|
-
} });
|
|
1531
|
-
var helpers = __importStar(require_helpers());
|
|
1532
|
-
exports.v6 = { helpers };
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
2
|
+
CRAWLIO_PORT_FILE,
|
|
3
|
+
PKG_VERSION,
|
|
4
|
+
TIMEOUTS,
|
|
5
|
+
WS_HEARTBEAT_INTERVAL,
|
|
6
|
+
WS_HOST,
|
|
7
|
+
WS_PORT,
|
|
8
|
+
WS_RECONNECT_GRACE,
|
|
9
|
+
WS_STALE_THRESHOLD
|
|
10
|
+
} from "./chunk-JRZS5IP6.js";
|
|
1535
11
|
|
|
1536
12
|
// src/mcp-server/index.ts
|
|
1537
|
-
import { randomBytes } from "crypto";
|
|
13
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1538
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1539
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1540
16
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
@@ -1542,904 +18,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
1542
18
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
1543
19
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1544
20
|
import { ZodError } from "zod";
|
|
1545
|
-
|
|
1546
|
-
// node_modules/express-rate-limit/dist/index.mjs
|
|
1547
|
-
var import_ip_address = __toESM(require_ip_address(), 1);
|
|
1548
|
-
import { isIPv6 } from "net";
|
|
1549
|
-
import { isIPv6 as isIPv62 } from "net";
|
|
1550
|
-
import { Buffer } from "buffer";
|
|
1551
|
-
import { createHash } from "crypto";
|
|
1552
|
-
import { isIP } from "net";
|
|
1553
|
-
function ipKeyGenerator(ip, ipv6Subnet = 56) {
|
|
1554
|
-
if (ipv6Subnet && isIPv6(ip)) {
|
|
1555
|
-
return `${new import_ip_address.Address6(`${ip}/${ipv6Subnet}`).startAddress().correctForm()}/${ipv6Subnet}`;
|
|
1556
|
-
}
|
|
1557
|
-
return ip;
|
|
1558
|
-
}
|
|
1559
|
-
var MemoryStore = class {
|
|
1560
|
-
constructor(validations2) {
|
|
1561
|
-
this.validations = validations2;
|
|
1562
|
-
this.previous = /* @__PURE__ */ new Map();
|
|
1563
|
-
this.current = /* @__PURE__ */ new Map();
|
|
1564
|
-
this.localKeys = true;
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Method that initializes the store.
|
|
1568
|
-
*
|
|
1569
|
-
* @param options {Options} - The options used to setup the middleware.
|
|
1570
|
-
*/
|
|
1571
|
-
init(options) {
|
|
1572
|
-
this.windowMs = options.windowMs;
|
|
1573
|
-
this.validations?.windowMs(this.windowMs);
|
|
1574
|
-
if (this.interval) clearInterval(this.interval);
|
|
1575
|
-
this.interval = setInterval(() => {
|
|
1576
|
-
this.clearExpired();
|
|
1577
|
-
}, this.windowMs);
|
|
1578
|
-
this.interval.unref?.();
|
|
1579
|
-
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Method to fetch a client's hit count and reset time.
|
|
1582
|
-
*
|
|
1583
|
-
* @param key {string} - The identifier for a client.
|
|
1584
|
-
*
|
|
1585
|
-
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
|
|
1586
|
-
*
|
|
1587
|
-
* @public
|
|
1588
|
-
*/
|
|
1589
|
-
async get(key) {
|
|
1590
|
-
return this.current.get(key) ?? this.previous.get(key);
|
|
1591
|
-
}
|
|
1592
|
-
/**
|
|
1593
|
-
* Method to increment a client's hit counter.
|
|
1594
|
-
*
|
|
1595
|
-
* @param key {string} - The identifier for a client.
|
|
1596
|
-
*
|
|
1597
|
-
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
|
|
1598
|
-
*
|
|
1599
|
-
* @public
|
|
1600
|
-
*/
|
|
1601
|
-
async increment(key) {
|
|
1602
|
-
const client = this.getClient(key);
|
|
1603
|
-
const now = Date.now();
|
|
1604
|
-
if (client.resetTime.getTime() <= now) {
|
|
1605
|
-
this.resetClient(client, now);
|
|
1606
|
-
}
|
|
1607
|
-
client.totalHits++;
|
|
1608
|
-
return client;
|
|
1609
|
-
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Method to decrement a client's hit counter.
|
|
1612
|
-
*
|
|
1613
|
-
* @param key {string} - The identifier for a client.
|
|
1614
|
-
*
|
|
1615
|
-
* @public
|
|
1616
|
-
*/
|
|
1617
|
-
async decrement(key) {
|
|
1618
|
-
const client = this.getClient(key);
|
|
1619
|
-
if (client.totalHits > 0) client.totalHits--;
|
|
1620
|
-
}
|
|
1621
|
-
/**
|
|
1622
|
-
* Method to reset a client's hit counter.
|
|
1623
|
-
*
|
|
1624
|
-
* @param key {string} - The identifier for a client.
|
|
1625
|
-
*
|
|
1626
|
-
* @public
|
|
1627
|
-
*/
|
|
1628
|
-
async resetKey(key) {
|
|
1629
|
-
this.current.delete(key);
|
|
1630
|
-
this.previous.delete(key);
|
|
1631
|
-
}
|
|
1632
|
-
/**
|
|
1633
|
-
* Method to reset everyone's hit counter.
|
|
1634
|
-
*
|
|
1635
|
-
* @public
|
|
1636
|
-
*/
|
|
1637
|
-
async resetAll() {
|
|
1638
|
-
this.current.clear();
|
|
1639
|
-
this.previous.clear();
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Method to stop the timer (if currently running) and prevent any memory
|
|
1643
|
-
* leaks.
|
|
1644
|
-
*
|
|
1645
|
-
* @public
|
|
1646
|
-
*/
|
|
1647
|
-
shutdown() {
|
|
1648
|
-
clearInterval(this.interval);
|
|
1649
|
-
void this.resetAll();
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Recycles a client by setting its hit count to zero, and reset time to
|
|
1653
|
-
* `windowMs` milliseconds from now.
|
|
1654
|
-
*
|
|
1655
|
-
* NOT to be confused with `#resetKey()`, which removes a client from both the
|
|
1656
|
-
* `current` and `previous` maps.
|
|
1657
|
-
*
|
|
1658
|
-
* @param client {Client} - The client to recycle.
|
|
1659
|
-
* @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
|
|
1660
|
-
*
|
|
1661
|
-
* @return {Client} - The modified client that was passed in, to allow for chaining.
|
|
1662
|
-
*/
|
|
1663
|
-
resetClient(client, now = Date.now()) {
|
|
1664
|
-
client.totalHits = 0;
|
|
1665
|
-
client.resetTime.setTime(now + this.windowMs);
|
|
1666
|
-
return client;
|
|
1667
|
-
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Retrieves or creates a client, given a key. Also ensures that the client being
|
|
1670
|
-
* returned is in the `current` map.
|
|
1671
|
-
*
|
|
1672
|
-
* @param key {string} - The key under which the client is (or is to be) stored.
|
|
1673
|
-
*
|
|
1674
|
-
* @returns {Client} - The requested client.
|
|
1675
|
-
*/
|
|
1676
|
-
getClient(key) {
|
|
1677
|
-
if (this.current.has(key)) return this.current.get(key);
|
|
1678
|
-
let client;
|
|
1679
|
-
if (this.previous.has(key)) {
|
|
1680
|
-
client = this.previous.get(key);
|
|
1681
|
-
this.previous.delete(key);
|
|
1682
|
-
} else {
|
|
1683
|
-
client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
|
|
1684
|
-
this.resetClient(client);
|
|
1685
|
-
}
|
|
1686
|
-
this.current.set(key, client);
|
|
1687
|
-
return client;
|
|
1688
|
-
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Move current clients to previous, create a new map for current.
|
|
1691
|
-
*
|
|
1692
|
-
* This function is called every `windowMs`.
|
|
1693
|
-
*/
|
|
1694
|
-
clearExpired() {
|
|
1695
|
-
this.previous = this.current;
|
|
1696
|
-
this.current = /* @__PURE__ */ new Map();
|
|
1697
|
-
}
|
|
1698
|
-
};
|
|
1699
|
-
var SUPPORTED_DRAFT_VERSIONS = [
|
|
1700
|
-
"draft-6",
|
|
1701
|
-
"draft-7",
|
|
1702
|
-
"draft-8"
|
|
1703
|
-
];
|
|
1704
|
-
var getResetSeconds = (windowMs, resetTime) => {
|
|
1705
|
-
let resetSeconds;
|
|
1706
|
-
if (resetTime) {
|
|
1707
|
-
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
1708
|
-
resetSeconds = Math.max(0, deltaSeconds);
|
|
1709
|
-
} else {
|
|
1710
|
-
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
1711
|
-
}
|
|
1712
|
-
return resetSeconds;
|
|
1713
|
-
};
|
|
1714
|
-
var getPartitionKey = (key) => {
|
|
1715
|
-
const hash = createHash("sha256");
|
|
1716
|
-
hash.update(key);
|
|
1717
|
-
const partitionKey = hash.digest("hex").slice(0, 12);
|
|
1718
|
-
return Buffer.from(partitionKey).toString("base64");
|
|
1719
|
-
};
|
|
1720
|
-
var setLegacyHeaders = (response, info) => {
|
|
1721
|
-
if (response.headersSent) return;
|
|
1722
|
-
response.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
1723
|
-
response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
1724
|
-
if (info.resetTime instanceof Date) {
|
|
1725
|
-
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
1726
|
-
response.setHeader(
|
|
1727
|
-
"X-RateLimit-Reset",
|
|
1728
|
-
Math.ceil(info.resetTime.getTime() / 1e3).toString()
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
var setDraft6Headers = (response, info, windowMs) => {
|
|
1733
|
-
if (response.headersSent) return;
|
|
1734
|
-
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
1735
|
-
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
1736
|
-
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
1737
|
-
response.setHeader("RateLimit-Limit", info.limit.toString());
|
|
1738
|
-
response.setHeader("RateLimit-Remaining", info.remaining.toString());
|
|
1739
|
-
if (typeof resetSeconds === "number")
|
|
1740
|
-
response.setHeader("RateLimit-Reset", resetSeconds.toString());
|
|
1741
|
-
};
|
|
1742
|
-
var setDraft7Headers = (response, info, windowMs) => {
|
|
1743
|
-
if (response.headersSent) return;
|
|
1744
|
-
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
1745
|
-
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
1746
|
-
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
1747
|
-
response.setHeader(
|
|
1748
|
-
"RateLimit",
|
|
1749
|
-
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
1750
|
-
);
|
|
1751
|
-
};
|
|
1752
|
-
var setDraft8Headers = (response, info, windowMs, name, key) => {
|
|
1753
|
-
if (response.headersSent) return;
|
|
1754
|
-
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
1755
|
-
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
1756
|
-
const partitionKey = getPartitionKey(key);
|
|
1757
|
-
const header = `r=${info.remaining}; t=${resetSeconds}`;
|
|
1758
|
-
const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
|
|
1759
|
-
response.append("RateLimit", `"${name}"; ${header}`);
|
|
1760
|
-
response.append("RateLimit-Policy", `"${name}"; ${policy}`);
|
|
1761
|
-
};
|
|
1762
|
-
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
1763
|
-
if (response.headersSent) return;
|
|
1764
|
-
const resetSeconds = getResetSeconds(windowMs, info.resetTime);
|
|
1765
|
-
response.setHeader("Retry-After", resetSeconds.toString());
|
|
1766
|
-
};
|
|
1767
|
-
var omitUndefinedProperties = (passedOptions) => {
|
|
1768
|
-
const omittedOptions = {};
|
|
1769
|
-
for (const k of Object.keys(passedOptions)) {
|
|
1770
|
-
const key = k;
|
|
1771
|
-
if (passedOptions[key] !== void 0) {
|
|
1772
|
-
omittedOptions[key] = passedOptions[key];
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
return omittedOptions;
|
|
1776
|
-
};
|
|
1777
|
-
var ValidationError = class extends Error {
|
|
1778
|
-
/**
|
|
1779
|
-
* The code must be a string, in snake case and all capital, that starts with
|
|
1780
|
-
* the substring `ERR_ERL_`.
|
|
1781
|
-
*
|
|
1782
|
-
* The message must be a string, starting with an uppercase character,
|
|
1783
|
-
* describing the issue in detail.
|
|
1784
|
-
*/
|
|
1785
|
-
constructor(code, message) {
|
|
1786
|
-
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
1787
|
-
super(`${message} See ${url} for more information.`);
|
|
1788
|
-
this.name = this.constructor.name;
|
|
1789
|
-
this.code = code;
|
|
1790
|
-
this.help = url;
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
var ChangeWarning = class extends ValidationError {
|
|
1794
|
-
};
|
|
1795
|
-
var usedStores = /* @__PURE__ */ new Set();
|
|
1796
|
-
var singleCountKeys = /* @__PURE__ */ new WeakMap();
|
|
1797
|
-
var validations = {
|
|
1798
|
-
enabled: {
|
|
1799
|
-
default: true
|
|
1800
|
-
},
|
|
1801
|
-
// Should be EnabledValidations type, but that's a circular reference
|
|
1802
|
-
disable() {
|
|
1803
|
-
for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
|
|
1804
|
-
},
|
|
1805
|
-
/**
|
|
1806
|
-
* Checks whether the IP address is valid, and that it does not have a port
|
|
1807
|
-
* number in it.
|
|
1808
|
-
*
|
|
1809
|
-
* See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
|
|
1810
|
-
*
|
|
1811
|
-
* @param ip {string | undefined} - The IP address provided by Express as request.ip.
|
|
1812
|
-
*
|
|
1813
|
-
* @returns {void}
|
|
1814
|
-
*/
|
|
1815
|
-
ip(ip) {
|
|
1816
|
-
if (ip === void 0) {
|
|
1817
|
-
throw new ValidationError(
|
|
1818
|
-
"ERR_ERL_UNDEFINED_IP_ADDRESS",
|
|
1819
|
-
`An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
if (!isIP(ip)) {
|
|
1823
|
-
throw new ValidationError(
|
|
1824
|
-
"ERR_ERL_INVALID_IP_ADDRESS",
|
|
1825
|
-
`An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
|
|
1826
|
-
);
|
|
1827
|
-
}
|
|
1828
|
-
},
|
|
1829
|
-
/**
|
|
1830
|
-
* Makes sure the trust proxy setting is not set to `true`.
|
|
1831
|
-
*
|
|
1832
|
-
* See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
|
|
1833
|
-
*
|
|
1834
|
-
* @param request {Request} - The Express request object.
|
|
1835
|
-
*
|
|
1836
|
-
* @returns {void}
|
|
1837
|
-
*/
|
|
1838
|
-
trustProxy(request) {
|
|
1839
|
-
if (request.app.get("trust proxy") === true) {
|
|
1840
|
-
throw new ValidationError(
|
|
1841
|
-
"ERR_ERL_PERMISSIVE_TRUST_PROXY",
|
|
1842
|
-
`The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
|
|
1843
|
-
);
|
|
1844
|
-
}
|
|
1845
|
-
},
|
|
1846
|
-
/**
|
|
1847
|
-
* Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
|
|
1848
|
-
* header is present.
|
|
1849
|
-
*
|
|
1850
|
-
* See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
|
|
1851
|
-
*
|
|
1852
|
-
* @param request {Request} - The Express request object.
|
|
1853
|
-
*
|
|
1854
|
-
* @returns {void}
|
|
1855
|
-
*/
|
|
1856
|
-
xForwardedForHeader(request) {
|
|
1857
|
-
if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
|
|
1858
|
-
throw new ValidationError(
|
|
1859
|
-
"ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
|
|
1860
|
-
`The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.`
|
|
1861
|
-
);
|
|
1862
|
-
}
|
|
1863
|
-
},
|
|
1864
|
-
/**
|
|
1865
|
-
* Alert the user if the Forwarded header is set (standardized version of X-Forwarded-For - not supported by express as of version 5.1.0)
|
|
1866
|
-
*
|
|
1867
|
-
* @param request {Request} - The Express request object.
|
|
1868
|
-
*
|
|
1869
|
-
* @returns {void}
|
|
1870
|
-
*/
|
|
1871
|
-
forwardedHeader(request) {
|
|
1872
|
-
if (request.headers.forwarded && request.ip === request.socket?.remoteAddress) {
|
|
1873
|
-
throw new ValidationError(
|
|
1874
|
-
"ERR_ERL_FORWARDED_HEADER",
|
|
1875
|
-
`The 'Forwarded' header (standardized X-Forwarded-For) is set but currently being ignored. Add a custom keyGenerator to use a value from this header.`
|
|
1876
|
-
);
|
|
1877
|
-
}
|
|
1878
|
-
},
|
|
1879
|
-
/**
|
|
1880
|
-
* Ensures totalHits value from store is a positive integer.
|
|
1881
|
-
*
|
|
1882
|
-
* @param hits {any} - The `totalHits` returned by the store.
|
|
1883
|
-
*/
|
|
1884
|
-
positiveHits(hits) {
|
|
1885
|
-
if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
|
1886
|
-
throw new ValidationError(
|
|
1887
|
-
"ERR_ERL_INVALID_HITS",
|
|
1888
|
-
`The totalHits value returned from the store must be a positive integer, got ${hits}`
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
},
|
|
1892
|
-
/**
|
|
1893
|
-
* Ensures a single store instance is not used with multiple express-rate-limit instances
|
|
1894
|
-
*/
|
|
1895
|
-
unsharedStore(store) {
|
|
1896
|
-
if (usedStores.has(store)) {
|
|
1897
|
-
const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)";
|
|
1898
|
-
throw new ValidationError(
|
|
1899
|
-
"ERR_ERL_STORE_REUSE",
|
|
1900
|
-
`A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`
|
|
1901
|
-
);
|
|
1902
|
-
}
|
|
1903
|
-
usedStores.add(store);
|
|
1904
|
-
},
|
|
1905
|
-
/**
|
|
1906
|
-
* Ensures a given key is incremented only once per request.
|
|
1907
|
-
*
|
|
1908
|
-
* @param request {Request} - The Express request object.
|
|
1909
|
-
* @param store {Store} - The store class.
|
|
1910
|
-
* @param key {string} - The key used to store the client's hit count.
|
|
1911
|
-
*
|
|
1912
|
-
* @returns {void}
|
|
1913
|
-
*/
|
|
1914
|
-
singleCount(request, store, key) {
|
|
1915
|
-
let storeKeys = singleCountKeys.get(request);
|
|
1916
|
-
if (!storeKeys) {
|
|
1917
|
-
storeKeys = /* @__PURE__ */ new Map();
|
|
1918
|
-
singleCountKeys.set(request, storeKeys);
|
|
1919
|
-
}
|
|
1920
|
-
const storeKey = store.localKeys ? store : store.constructor.name;
|
|
1921
|
-
let keys = storeKeys.get(storeKey);
|
|
1922
|
-
if (!keys) {
|
|
1923
|
-
keys = [];
|
|
1924
|
-
storeKeys.set(storeKey, keys);
|
|
1925
|
-
}
|
|
1926
|
-
const prefixedKey = `${store.prefix ?? ""}${key}`;
|
|
1927
|
-
if (keys.includes(prefixedKey)) {
|
|
1928
|
-
throw new ValidationError(
|
|
1929
|
-
"ERR_ERL_DOUBLE_COUNT",
|
|
1930
|
-
`The hit count for ${key} was incremented more than once for a single request.`
|
|
1931
|
-
);
|
|
1932
|
-
}
|
|
1933
|
-
keys.push(prefixedKey);
|
|
1934
|
-
},
|
|
1935
|
-
/**
|
|
1936
|
-
* Warns the user that the behaviour for `max: 0` / `limit: 0` is
|
|
1937
|
-
* changing in the next major release.
|
|
1938
|
-
*
|
|
1939
|
-
* @param limit {number} - The maximum number of hits per client.
|
|
1940
|
-
*
|
|
1941
|
-
* @returns {void}
|
|
1942
|
-
*/
|
|
1943
|
-
limit(limit) {
|
|
1944
|
-
if (limit === 0) {
|
|
1945
|
-
throw new ChangeWarning(
|
|
1946
|
-
"WRN_ERL_MAX_ZERO",
|
|
1947
|
-
"Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7"
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
},
|
|
1951
|
-
/**
|
|
1952
|
-
* Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
|
|
1953
|
-
* and will be removed in the next major release.
|
|
1954
|
-
*
|
|
1955
|
-
* @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
|
|
1956
|
-
*
|
|
1957
|
-
* @returns {void}
|
|
1958
|
-
*/
|
|
1959
|
-
draftPolliHeaders(draft_polli_ratelimit_headers) {
|
|
1960
|
-
if (draft_polli_ratelimit_headers) {
|
|
1961
|
-
throw new ChangeWarning(
|
|
1962
|
-
"WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
|
1963
|
-
`The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
|
1964
|
-
);
|
|
1965
|
-
}
|
|
1966
|
-
},
|
|
1967
|
-
/**
|
|
1968
|
-
* Warns the user that the `onLimitReached` option is deprecated and
|
|
1969
|
-
* will be removed in the next major release.
|
|
1970
|
-
*
|
|
1971
|
-
* @param onLimitReached {any | undefined} - The maximum number of hits per client.
|
|
1972
|
-
*
|
|
1973
|
-
* @returns {void}
|
|
1974
|
-
*/
|
|
1975
|
-
onLimitReached(onLimitReached) {
|
|
1976
|
-
if (onLimitReached) {
|
|
1977
|
-
throw new ChangeWarning(
|
|
1978
|
-
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
1979
|
-
"The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7."
|
|
1980
|
-
);
|
|
1981
|
-
}
|
|
1982
|
-
},
|
|
1983
|
-
/**
|
|
1984
|
-
* Warns the user when an invalid/unsupported version of the draft spec is passed.
|
|
1985
|
-
*
|
|
1986
|
-
* @param version {any | undefined} - The version passed by the user.
|
|
1987
|
-
*
|
|
1988
|
-
* @returns {void}
|
|
1989
|
-
*/
|
|
1990
|
-
headersDraftVersion(version) {
|
|
1991
|
-
if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
|
|
1992
|
-
!SUPPORTED_DRAFT_VERSIONS.includes(version)) {
|
|
1993
|
-
const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
|
|
1994
|
-
throw new ValidationError(
|
|
1995
|
-
"ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
|
|
1996
|
-
`standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
|
|
1997
|
-
);
|
|
1998
|
-
}
|
|
1999
|
-
},
|
|
2000
|
-
/**
|
|
2001
|
-
* Warns the user when the selected headers option requires a reset time but
|
|
2002
|
-
* the store does not provide one.
|
|
2003
|
-
*
|
|
2004
|
-
* @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
|
|
2005
|
-
*
|
|
2006
|
-
* @returns {void}
|
|
2007
|
-
*/
|
|
2008
|
-
headersResetTime(resetTime) {
|
|
2009
|
-
if (!resetTime) {
|
|
2010
|
-
throw new ValidationError(
|
|
2011
|
-
"ERR_ERL_HEADERS_NO_RESET",
|
|
2012
|
-
`standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
|
|
2013
|
-
);
|
|
2014
|
-
}
|
|
2015
|
-
},
|
|
2016
|
-
knownOptions(passedOptions) {
|
|
2017
|
-
if (!passedOptions) return;
|
|
2018
|
-
const optionsMap = {
|
|
2019
|
-
windowMs: true,
|
|
2020
|
-
limit: true,
|
|
2021
|
-
message: true,
|
|
2022
|
-
statusCode: true,
|
|
2023
|
-
legacyHeaders: true,
|
|
2024
|
-
standardHeaders: true,
|
|
2025
|
-
identifier: true,
|
|
2026
|
-
requestPropertyName: true,
|
|
2027
|
-
skipFailedRequests: true,
|
|
2028
|
-
skipSuccessfulRequests: true,
|
|
2029
|
-
keyGenerator: true,
|
|
2030
|
-
ipv6Subnet: true,
|
|
2031
|
-
handler: true,
|
|
2032
|
-
skip: true,
|
|
2033
|
-
requestWasSuccessful: true,
|
|
2034
|
-
store: true,
|
|
2035
|
-
validate: true,
|
|
2036
|
-
headers: true,
|
|
2037
|
-
max: true,
|
|
2038
|
-
passOnStoreError: true
|
|
2039
|
-
};
|
|
2040
|
-
const validOptions = Object.keys(optionsMap).concat(
|
|
2041
|
-
"draft_polli_ratelimit_headers",
|
|
2042
|
-
// not a valid option anymore, but we have a more specific check for this one, so don't warn for it here
|
|
2043
|
-
// from express-slow-down - https://github.com/express-rate-limit/express-slow-down/blob/main/source/types.ts#L65
|
|
2044
|
-
"delayAfter",
|
|
2045
|
-
"delayMs",
|
|
2046
|
-
"maxDelayMs"
|
|
2047
|
-
);
|
|
2048
|
-
for (const key of Object.keys(passedOptions)) {
|
|
2049
|
-
if (!validOptions.includes(key)) {
|
|
2050
|
-
throw new ValidationError(
|
|
2051
|
-
"ERR_ERL_UNKNOWN_OPTION",
|
|
2052
|
-
`Unexpected configuration option: ${key}`
|
|
2053
|
-
// todo: suggest a valid option with a short levenstein distance?
|
|
2054
|
-
);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
},
|
|
2058
|
-
/**
|
|
2059
|
-
* Checks the options.validate setting to ensure that only recognized
|
|
2060
|
-
* validations are enabled or disabled.
|
|
2061
|
-
*
|
|
2062
|
-
* If any unrecognized values are found, an error is logged that
|
|
2063
|
-
* includes the list of supported validations.
|
|
2064
|
-
*/
|
|
2065
|
-
validationsConfig() {
|
|
2066
|
-
const supportedValidations = Object.keys(this).filter(
|
|
2067
|
-
(k) => !["enabled", "disable"].includes(k)
|
|
2068
|
-
);
|
|
2069
|
-
supportedValidations.push("default");
|
|
2070
|
-
for (const key of Object.keys(this.enabled)) {
|
|
2071
|
-
if (!supportedValidations.includes(key)) {
|
|
2072
|
-
throw new ValidationError(
|
|
2073
|
-
"ERR_ERL_UNKNOWN_VALIDATION",
|
|
2074
|
-
`options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
|
|
2075
|
-
", "
|
|
2076
|
-
)}.`
|
|
2077
|
-
);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
},
|
|
2081
|
-
/**
|
|
2082
|
-
* Checks to see if the instance was created inside of a request handler,
|
|
2083
|
-
* which would prevent it from working correctly, with the default memory
|
|
2084
|
-
* store (or any other store with localKeys.)
|
|
2085
|
-
*/
|
|
2086
|
-
creationStack(store) {
|
|
2087
|
-
const { stack } = new Error(
|
|
2088
|
-
"express-rate-limit validation check (set options.validate.creationStack=false to disable)"
|
|
2089
|
-
);
|
|
2090
|
-
if (stack?.includes("Layer.handle [as handle_request]") || // express v4
|
|
2091
|
-
stack?.includes("Layer.handleRequest")) {
|
|
2092
|
-
if (!store.localKeys) {
|
|
2093
|
-
throw new ValidationError(
|
|
2094
|
-
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
2095
|
-
"express-rate-limit instance should *usually* be created at app initialization, not when responding to a request."
|
|
2096
|
-
);
|
|
2097
|
-
}
|
|
2098
|
-
throw new ValidationError(
|
|
2099
|
-
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
2100
|
-
"express-rate-limit instance should be created at app initialization, not when responding to a request."
|
|
2101
|
-
);
|
|
2102
|
-
}
|
|
2103
|
-
},
|
|
2104
|
-
ipv6Subnet(ipv6Subnet) {
|
|
2105
|
-
if (ipv6Subnet === false) {
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
|
|
2109
|
-
throw new ValidationError(
|
|
2110
|
-
"ERR_ERL_IPV6_SUBNET",
|
|
2111
|
-
`Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
|
|
2112
|
-
);
|
|
2113
|
-
}
|
|
2114
|
-
},
|
|
2115
|
-
ipv6SubnetOrKeyGenerator(options) {
|
|
2116
|
-
if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
|
|
2117
|
-
throw new ValidationError(
|
|
2118
|
-
"ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
|
|
2119
|
-
`Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
|
|
2120
|
-
);
|
|
2121
|
-
}
|
|
2122
|
-
},
|
|
2123
|
-
keyGeneratorIpFallback(keyGenerator) {
|
|
2124
|
-
if (!keyGenerator) {
|
|
2125
|
-
return;
|
|
2126
|
-
}
|
|
2127
|
-
const src = keyGenerator.toString();
|
|
2128
|
-
if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
|
|
2129
|
-
throw new ValidationError(
|
|
2130
|
-
"ERR_ERL_KEY_GEN_IPV6",
|
|
2131
|
-
"Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits."
|
|
2132
|
-
);
|
|
2133
|
-
}
|
|
2134
|
-
},
|
|
2135
|
-
/**
|
|
2136
|
-
* Checks to see if the window duration is greater than 2^32 - 1. This is only
|
|
2137
|
-
* called by the default MemoryStore, since it uses Node's setInterval method.
|
|
2138
|
-
*
|
|
2139
|
-
* See https://nodejs.org/api/timers.html#setintervalcallback-delay-args.
|
|
2140
|
-
*/
|
|
2141
|
-
windowMs(windowMs) {
|
|
2142
|
-
const SET_TIMEOUT_MAX = 2 ** 31 - 1;
|
|
2143
|
-
if (typeof windowMs !== "number" || Number.isNaN(windowMs) || windowMs < 1 || windowMs > SET_TIMEOUT_MAX) {
|
|
2144
|
-
throw new ValidationError(
|
|
2145
|
-
"ERR_ERL_WINDOW_MS",
|
|
2146
|
-
`Invalid windowMs value: ${windowMs}${typeof windowMs !== "number" ? ` (${typeof windowMs})` : ""}, must be a number between 1 and ${SET_TIMEOUT_MAX} when using the default MemoryStore`
|
|
2147
|
-
);
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
};
|
|
2151
|
-
var getValidations = (_enabled) => {
|
|
2152
|
-
let enabled;
|
|
2153
|
-
if (typeof _enabled === "boolean") {
|
|
2154
|
-
enabled = {
|
|
2155
|
-
default: _enabled
|
|
2156
|
-
};
|
|
2157
|
-
} else {
|
|
2158
|
-
enabled = {
|
|
2159
|
-
default: true,
|
|
2160
|
-
..._enabled
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
const wrappedValidations = { enabled };
|
|
2164
|
-
for (const [name, validation] of Object.entries(validations)) {
|
|
2165
|
-
if (typeof validation === "function")
|
|
2166
|
-
wrappedValidations[name] = (...args) => {
|
|
2167
|
-
if (!(enabled[name] ?? enabled.default)) {
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
try {
|
|
2171
|
-
;
|
|
2172
|
-
validation.apply(
|
|
2173
|
-
wrappedValidations,
|
|
2174
|
-
args
|
|
2175
|
-
);
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
if (error instanceof ChangeWarning) console.warn(error);
|
|
2178
|
-
else console.error(error);
|
|
2179
|
-
}
|
|
2180
|
-
};
|
|
2181
|
-
}
|
|
2182
|
-
return wrappedValidations;
|
|
2183
|
-
};
|
|
2184
|
-
var isLegacyStore = (store) => (
|
|
2185
|
-
// Check that `incr` exists but `increment` does not - store authors might want
|
|
2186
|
-
// to keep both around for backwards compatibility.
|
|
2187
|
-
typeof store.incr === "function" && typeof store.increment !== "function"
|
|
2188
|
-
);
|
|
2189
|
-
var promisifyStore = (passedStore) => {
|
|
2190
|
-
if (!isLegacyStore(passedStore)) {
|
|
2191
|
-
return passedStore;
|
|
2192
|
-
}
|
|
2193
|
-
const legacyStore = passedStore;
|
|
2194
|
-
class PromisifiedStore {
|
|
2195
|
-
async increment(key) {
|
|
2196
|
-
return new Promise((resolve2, reject) => {
|
|
2197
|
-
legacyStore.incr(
|
|
2198
|
-
key,
|
|
2199
|
-
(error, totalHits, resetTime) => {
|
|
2200
|
-
if (error) reject(error);
|
|
2201
|
-
resolve2({ totalHits, resetTime });
|
|
2202
|
-
}
|
|
2203
|
-
);
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2206
|
-
async decrement(key) {
|
|
2207
|
-
return legacyStore.decrement(key);
|
|
2208
|
-
}
|
|
2209
|
-
async resetKey(key) {
|
|
2210
|
-
return legacyStore.resetKey(key);
|
|
2211
|
-
}
|
|
2212
|
-
/* istanbul ignore next */
|
|
2213
|
-
async resetAll() {
|
|
2214
|
-
if (typeof legacyStore.resetAll === "function")
|
|
2215
|
-
return legacyStore.resetAll();
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
return new PromisifiedStore();
|
|
2219
|
-
};
|
|
2220
|
-
var getOptionsFromConfig = (config) => {
|
|
2221
|
-
const { validations: validations2, ...directlyPassableEntries } = config;
|
|
2222
|
-
return {
|
|
2223
|
-
...directlyPassableEntries,
|
|
2224
|
-
validate: validations2.enabled
|
|
2225
|
-
};
|
|
2226
|
-
};
|
|
2227
|
-
var parseOptions = (passedOptions) => {
|
|
2228
|
-
const notUndefinedOptions = omitUndefinedProperties(passedOptions);
|
|
2229
|
-
const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
|
2230
|
-
validations2.validationsConfig();
|
|
2231
|
-
validations2.knownOptions(passedOptions);
|
|
2232
|
-
validations2.draftPolliHeaders(
|
|
2233
|
-
// @ts-expect-error see the note above.
|
|
2234
|
-
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
2235
|
-
);
|
|
2236
|
-
validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
2237
|
-
if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
|
|
2238
|
-
validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
|
|
2239
|
-
}
|
|
2240
|
-
validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
|
|
2241
|
-
validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
|
|
2242
|
-
let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
|
2243
|
-
if (standardHeaders === true) standardHeaders = "draft-6";
|
|
2244
|
-
const config = {
|
|
2245
|
-
windowMs: 60 * 1e3,
|
|
2246
|
-
limit: passedOptions.max ?? 5,
|
|
2247
|
-
// `max` is deprecated, but support it anyways.
|
|
2248
|
-
message: "Too many requests, please try again later.",
|
|
2249
|
-
statusCode: 429,
|
|
2250
|
-
legacyHeaders: passedOptions.headers ?? true,
|
|
2251
|
-
identifier(request, _response) {
|
|
2252
|
-
let duration = "";
|
|
2253
|
-
const property = config.requestPropertyName;
|
|
2254
|
-
const { limit } = request[property];
|
|
2255
|
-
const seconds = config.windowMs / 1e3;
|
|
2256
|
-
const minutes = config.windowMs / (1e3 * 60);
|
|
2257
|
-
const hours = config.windowMs / (1e3 * 60 * 60);
|
|
2258
|
-
const days = config.windowMs / (1e3 * 60 * 60 * 24);
|
|
2259
|
-
if (seconds < 60) duration = `${seconds}sec`;
|
|
2260
|
-
else if (minutes < 60) duration = `${minutes}min`;
|
|
2261
|
-
else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
|
|
2262
|
-
else duration = `${days}day${days > 1 ? "s" : ""}`;
|
|
2263
|
-
return `${limit}-in-${duration}`;
|
|
2264
|
-
},
|
|
2265
|
-
requestPropertyName: "rateLimit",
|
|
2266
|
-
skipFailedRequests: false,
|
|
2267
|
-
skipSuccessfulRequests: false,
|
|
2268
|
-
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
|
2269
|
-
skip: (_request, _response) => false,
|
|
2270
|
-
async keyGenerator(request, response) {
|
|
2271
|
-
validations2.ip(request.ip);
|
|
2272
|
-
validations2.trustProxy(request);
|
|
2273
|
-
validations2.xForwardedForHeader(request);
|
|
2274
|
-
validations2.forwardedHeader(request);
|
|
2275
|
-
const ip = request.ip;
|
|
2276
|
-
let subnet = 56;
|
|
2277
|
-
if (isIPv62(ip)) {
|
|
2278
|
-
subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
|
|
2279
|
-
if (typeof config.ipv6Subnet === "function")
|
|
2280
|
-
validations2.ipv6Subnet(subnet);
|
|
2281
|
-
}
|
|
2282
|
-
return ipKeyGenerator(ip, subnet);
|
|
2283
|
-
},
|
|
2284
|
-
ipv6Subnet: 56,
|
|
2285
|
-
async handler(request, response, _next, _optionsUsed) {
|
|
2286
|
-
response.status(config.statusCode);
|
|
2287
|
-
const message = typeof config.message === "function" ? await config.message(
|
|
2288
|
-
request,
|
|
2289
|
-
response
|
|
2290
|
-
) : config.message;
|
|
2291
|
-
if (!response.writableEnded) response.send(message);
|
|
2292
|
-
},
|
|
2293
|
-
passOnStoreError: false,
|
|
2294
|
-
// Allow the default options to be overridden by the passed options.
|
|
2295
|
-
...notUndefinedOptions,
|
|
2296
|
-
// `standardHeaders` is resolved into a draft version above, use that.
|
|
2297
|
-
standardHeaders,
|
|
2298
|
-
// Note that this field is declared after the user's options are spread in,
|
|
2299
|
-
// so that this field doesn't get overridden with an un-promisified store!
|
|
2300
|
-
store: promisifyStore(
|
|
2301
|
-
notUndefinedOptions.store ?? new MemoryStore(validations2)
|
|
2302
|
-
),
|
|
2303
|
-
// Print an error to the console if a few known misconfigurations are detected.
|
|
2304
|
-
validations: validations2
|
|
2305
|
-
};
|
|
2306
|
-
if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") {
|
|
2307
|
-
throw new TypeError(
|
|
2308
|
-
"An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
2311
|
-
return config;
|
|
2312
|
-
};
|
|
2313
|
-
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
|
2314
|
-
try {
|
|
2315
|
-
await Promise.resolve(fn(request, response, next)).catch(next);
|
|
2316
|
-
} catch (error) {
|
|
2317
|
-
next(error);
|
|
2318
|
-
}
|
|
2319
|
-
};
|
|
2320
|
-
var rateLimit = (passedOptions) => {
|
|
2321
|
-
const config = parseOptions(passedOptions ?? {});
|
|
2322
|
-
const options = getOptionsFromConfig(config);
|
|
2323
|
-
config.validations.creationStack(config.store);
|
|
2324
|
-
config.validations.unsharedStore(config.store);
|
|
2325
|
-
if (typeof config.store.init === "function") config.store.init(options);
|
|
2326
|
-
const middleware = handleAsyncErrors(
|
|
2327
|
-
async (request, response, next) => {
|
|
2328
|
-
const skip = await config.skip(request, response);
|
|
2329
|
-
if (skip) {
|
|
2330
|
-
next();
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
const augmentedRequest = request;
|
|
2334
|
-
const key = await config.keyGenerator(request, response);
|
|
2335
|
-
let totalHits = 0;
|
|
2336
|
-
let resetTime;
|
|
2337
|
-
try {
|
|
2338
|
-
const incrementResult = await config.store.increment(key);
|
|
2339
|
-
totalHits = incrementResult.totalHits;
|
|
2340
|
-
resetTime = incrementResult.resetTime;
|
|
2341
|
-
} catch (error) {
|
|
2342
|
-
if (config.passOnStoreError) {
|
|
2343
|
-
console.error(
|
|
2344
|
-
"express-rate-limit: error from store, allowing request without rate-limiting.",
|
|
2345
|
-
error
|
|
2346
|
-
);
|
|
2347
|
-
next();
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
throw error;
|
|
2351
|
-
}
|
|
2352
|
-
config.validations.positiveHits(totalHits);
|
|
2353
|
-
config.validations.singleCount(request, config.store, key);
|
|
2354
|
-
const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
|
|
2355
|
-
const limit = await retrieveLimit;
|
|
2356
|
-
config.validations.limit(limit);
|
|
2357
|
-
const info = {
|
|
2358
|
-
limit,
|
|
2359
|
-
used: totalHits,
|
|
2360
|
-
remaining: Math.max(limit - totalHits, 0),
|
|
2361
|
-
resetTime,
|
|
2362
|
-
key
|
|
2363
|
-
};
|
|
2364
|
-
Object.defineProperty(info, "current", {
|
|
2365
|
-
configurable: false,
|
|
2366
|
-
enumerable: false,
|
|
2367
|
-
value: totalHits
|
|
2368
|
-
});
|
|
2369
|
-
augmentedRequest[config.requestPropertyName] = info;
|
|
2370
|
-
if (config.legacyHeaders && !response.headersSent) {
|
|
2371
|
-
setLegacyHeaders(response, info);
|
|
2372
|
-
}
|
|
2373
|
-
if (config.standardHeaders && !response.headersSent) {
|
|
2374
|
-
switch (config.standardHeaders) {
|
|
2375
|
-
case "draft-6": {
|
|
2376
|
-
setDraft6Headers(response, info, config.windowMs);
|
|
2377
|
-
break;
|
|
2378
|
-
}
|
|
2379
|
-
case "draft-7": {
|
|
2380
|
-
config.validations.headersResetTime(info.resetTime);
|
|
2381
|
-
setDraft7Headers(response, info, config.windowMs);
|
|
2382
|
-
break;
|
|
2383
|
-
}
|
|
2384
|
-
case "draft-8": {
|
|
2385
|
-
const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
|
|
2386
|
-
const name = await retrieveName;
|
|
2387
|
-
config.validations.headersResetTime(info.resetTime);
|
|
2388
|
-
setDraft8Headers(response, info, config.windowMs, name, key);
|
|
2389
|
-
break;
|
|
2390
|
-
}
|
|
2391
|
-
default: {
|
|
2392
|
-
config.validations.headersDraftVersion(config.standardHeaders);
|
|
2393
|
-
break;
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
2398
|
-
let decremented = false;
|
|
2399
|
-
const decrementKey = async () => {
|
|
2400
|
-
if (!decremented) {
|
|
2401
|
-
await config.store.decrement(key);
|
|
2402
|
-
decremented = true;
|
|
2403
|
-
}
|
|
2404
|
-
};
|
|
2405
|
-
if (config.skipFailedRequests) {
|
|
2406
|
-
response.on("finish", async () => {
|
|
2407
|
-
if (!await config.requestWasSuccessful(request, response))
|
|
2408
|
-
await decrementKey();
|
|
2409
|
-
});
|
|
2410
|
-
response.on("close", async () => {
|
|
2411
|
-
if (!response.writableEnded) await decrementKey();
|
|
2412
|
-
});
|
|
2413
|
-
response.on("error", async () => {
|
|
2414
|
-
await decrementKey();
|
|
2415
|
-
});
|
|
2416
|
-
}
|
|
2417
|
-
if (config.skipSuccessfulRequests) {
|
|
2418
|
-
response.on("finish", async () => {
|
|
2419
|
-
if (await config.requestWasSuccessful(request, response))
|
|
2420
|
-
await decrementKey();
|
|
2421
|
-
});
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
config.validations.disable();
|
|
2425
|
-
if (totalHits > limit) {
|
|
2426
|
-
if (config.legacyHeaders || config.standardHeaders) {
|
|
2427
|
-
setRetryAfterHeader(response, info, config.windowMs);
|
|
2428
|
-
}
|
|
2429
|
-
config.handler(request, response, next, options);
|
|
2430
|
-
return;
|
|
2431
|
-
}
|
|
2432
|
-
next();
|
|
2433
|
-
}
|
|
2434
|
-
);
|
|
2435
|
-
const getThrowFn = () => {
|
|
2436
|
-
throw new Error("The current store does not support the get/getKey method");
|
|
2437
|
-
};
|
|
2438
|
-
middleware.resetKey = config.store.resetKey.bind(config.store);
|
|
2439
|
-
middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
|
|
2440
|
-
return middleware;
|
|
2441
|
-
};
|
|
2442
|
-
var rate_limit_default = rateLimit;
|
|
21
|
+
import rateLimit from "express-rate-limit";
|
|
2443
22
|
|
|
2444
23
|
// src/mcp-server/websocket-bridge.ts
|
|
2445
24
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -2447,33 +26,6 @@ import { createServer } from "http";
|
|
|
2447
26
|
import { randomUUID } from "crypto";
|
|
2448
27
|
import { fileURLToPath } from "url";
|
|
2449
28
|
import { dirname, resolve } from "path";
|
|
2450
|
-
|
|
2451
|
-
// src/shared/constants.ts
|
|
2452
|
-
import { homedir } from "os";
|
|
2453
|
-
import { join } from "path";
|
|
2454
|
-
var WS_PORT = 9333;
|
|
2455
|
-
var WS_HOST = "127.0.0.1";
|
|
2456
|
-
var CRAWLIO_PORT_FILE = join(
|
|
2457
|
-
homedir(),
|
|
2458
|
-
"Library",
|
|
2459
|
-
"Logs",
|
|
2460
|
-
"Crawlio",
|
|
2461
|
-
"control.port"
|
|
2462
|
-
);
|
|
2463
|
-
var TIMEOUTS = {
|
|
2464
|
-
WS_COMMAND: 3e4,
|
|
2465
|
-
// 30s for most commands
|
|
2466
|
-
NETWORK_CAPTURE: 12e4,
|
|
2467
|
-
// 2min for network capture
|
|
2468
|
-
SCREENSHOT: 1e4,
|
|
2469
|
-
// 10s
|
|
2470
|
-
RECONNECT: 3e3,
|
|
2471
|
-
// 3s reconnect delay
|
|
2472
|
-
CODE_EXECUTE: 12e4
|
|
2473
|
-
// 2min for code-mode execute()
|
|
2474
|
-
};
|
|
2475
|
-
|
|
2476
|
-
// src/mcp-server/websocket-bridge.ts
|
|
2477
29
|
function resolveIndexPath() {
|
|
2478
30
|
if (process.argv[1] && process.argv[1].includes("dist")) {
|
|
2479
31
|
return process.argv[1];
|
|
@@ -2665,8 +217,8 @@ var MessageQueue = class {
|
|
|
2665
217
|
this.queue = [];
|
|
2666
218
|
}
|
|
2667
219
|
};
|
|
2668
|
-
var HEARTBEAT_INTERVAL =
|
|
2669
|
-
var STALE_THRESHOLD =
|
|
220
|
+
var HEARTBEAT_INTERVAL = WS_HEARTBEAT_INTERVAL;
|
|
221
|
+
var STALE_THRESHOLD = WS_STALE_THRESHOLD;
|
|
2670
222
|
var WebSocketBridge = class {
|
|
2671
223
|
wss = null;
|
|
2672
224
|
httpServer = null;
|
|
@@ -2674,6 +226,7 @@ var WebSocketBridge = class {
|
|
|
2674
226
|
pending = /* @__PURE__ */ new Map();
|
|
2675
227
|
messageQueue = new MessageQueue();
|
|
2676
228
|
heartbeatTimer = null;
|
|
229
|
+
reconnectGraceTimer = null;
|
|
2677
230
|
lastPong = 0;
|
|
2678
231
|
lastPingSent = 0;
|
|
2679
232
|
latencyMs = 0;
|
|
@@ -2689,7 +242,7 @@ var WebSocketBridge = class {
|
|
|
2689
242
|
this.lastPingSent = Date.now();
|
|
2690
243
|
this.client.ping();
|
|
2691
244
|
if (this.lastPong > 0 && Date.now() - this.lastPong > STALE_THRESHOLD) {
|
|
2692
|
-
console.error(
|
|
245
|
+
console.error(`[Bridge] WebSocket stale \u2014 no pong in ${STALE_THRESHOLD / 1e3}s, closing`);
|
|
2693
246
|
this.client.terminate();
|
|
2694
247
|
}
|
|
2695
248
|
}
|
|
@@ -2742,12 +295,48 @@ var WebSocketBridge = class {
|
|
|
2742
295
|
res.end();
|
|
2743
296
|
return;
|
|
2744
297
|
}
|
|
2745
|
-
res.writeHead(426);
|
|
2746
|
-
res.end("Upgrade Required");
|
|
298
|
+
res.writeHead(426);
|
|
299
|
+
res.end("Upgrade Required");
|
|
300
|
+
});
|
|
301
|
+
this.wss = new WebSocketServer({
|
|
302
|
+
server: this.httpServer,
|
|
303
|
+
maxPayload: 10 * 1024 * 1024,
|
|
304
|
+
// 10 MB — prevents memory exhaustion
|
|
305
|
+
verifyClient: (info, callback) => {
|
|
306
|
+
const origin = info.origin;
|
|
307
|
+
if (!origin || origin === "null") {
|
|
308
|
+
callback(true);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (origin.startsWith("chrome-extension://")) {
|
|
312
|
+
callback(true);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const url = new URL(origin);
|
|
317
|
+
if (url.hostname === "127.0.0.1" || url.hostname === "localhost") {
|
|
318
|
+
callback(true);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
console.error(`[Bridge] WebSocket origin rejected: ${origin}`);
|
|
324
|
+
callback(false, 403, "Forbidden");
|
|
325
|
+
}
|
|
2747
326
|
});
|
|
2748
|
-
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
2749
327
|
this.wss.on("connection", (ws) => {
|
|
328
|
+
if (this.client && this.client.readyState === WebSocket.OPEN) {
|
|
329
|
+
console.error("[Bridge] Evicting stale client \u2014 new connection");
|
|
330
|
+
this.client.close(1e3, "Replaced by new connection");
|
|
331
|
+
}
|
|
2750
332
|
console.error(`[Bridge] Extension connected`);
|
|
333
|
+
if (this.reconnectGraceTimer) {
|
|
334
|
+
clearTimeout(this.reconnectGraceTimer);
|
|
335
|
+
this.reconnectGraceTimer = null;
|
|
336
|
+
if (this.pending.size > 0) {
|
|
337
|
+
console.error(`[Bridge] Reconnect within grace period \u2014 ${this.pending.size} pending commands preserved`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
2751
340
|
this.client = ws;
|
|
2752
341
|
this.reconnectCount++;
|
|
2753
342
|
this.connectTime = Date.now();
|
|
@@ -2789,6 +378,11 @@ var WebSocketBridge = class {
|
|
|
2789
378
|
}
|
|
2790
379
|
ws.on("message", (raw) => {
|
|
2791
380
|
try {
|
|
381
|
+
const len = typeof raw === "string" ? Buffer.byteLength(raw) : raw.length;
|
|
382
|
+
if (len > 5 * 1024 * 1024) {
|
|
383
|
+
console.error(`[Bridge] Oversized message dropped: ${(len / 1024 / 1024).toFixed(1)} MB`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
2792
386
|
const msg = JSON.parse(raw.toString());
|
|
2793
387
|
if (msg.type === "refresh_port") {
|
|
2794
388
|
this.onPortRefreshRequested?.();
|
|
@@ -2800,20 +394,36 @@ var WebSocketBridge = class {
|
|
|
2800
394
|
}
|
|
2801
395
|
});
|
|
2802
396
|
ws.on("error", (err) => {
|
|
2803
|
-
|
|
397
|
+
if (this.client === ws) {
|
|
398
|
+
console.error("[Bridge] WebSocket error:", err.message);
|
|
399
|
+
}
|
|
2804
400
|
});
|
|
2805
401
|
ws.on("close", () => {
|
|
402
|
+
if (this.client !== ws) {
|
|
403
|
+
console.error("[Bridge] Stale client closed (already replaced)");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
2806
406
|
console.error("[Bridge] Extension disconnected");
|
|
2807
407
|
this.stopHeartbeat();
|
|
2808
408
|
this.client = null;
|
|
2809
409
|
this.connectTime = 0;
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
410
|
+
if (this.pending.size > 0) {
|
|
411
|
+
console.error(`[Bridge] ${this.pending.size} pending commands \u2014 waiting ${WS_RECONNECT_GRACE / 1e3}s for reconnect`);
|
|
412
|
+
this.reconnectGraceTimer = setTimeout(() => {
|
|
413
|
+
this.reconnectGraceTimer = null;
|
|
414
|
+
for (const [id, req] of this.pending) {
|
|
415
|
+
clearTimeout(req.timer);
|
|
416
|
+
req.reject(new Error("Extension disconnected"));
|
|
417
|
+
this.pending.delete(id);
|
|
418
|
+
}
|
|
419
|
+
}, WS_RECONNECT_GRACE);
|
|
2814
420
|
}
|
|
2815
421
|
});
|
|
2816
422
|
});
|
|
423
|
+
this.wss.on("wsClientError", (err, socket) => {
|
|
424
|
+
console.error("[Bridge] WebSocket handshake error:", err.message);
|
|
425
|
+
socket.destroy();
|
|
426
|
+
});
|
|
2817
427
|
try {
|
|
2818
428
|
const probe = await fetch(`http://${WS_HOST}:${WS_PORT}/health`, { signal: AbortSignal.timeout(500) });
|
|
2819
429
|
const body = await probe.json();
|
|
@@ -2855,7 +465,8 @@ var WebSocketBridge = class {
|
|
|
2855
465
|
const serialized = JSON.stringify(fullCommand);
|
|
2856
466
|
if (!this.isConnected) {
|
|
2857
467
|
console.error(`[Bridge] Queuing command (offline): ${command.type} (queue depth: ${this.messageQueue.depth + 1})`);
|
|
2858
|
-
|
|
468
|
+
const queueTimeout = Math.max(timeout, 45e3);
|
|
469
|
+
return this.messageQueue.enqueue(serialized, queueTimeout);
|
|
2859
470
|
}
|
|
2860
471
|
return new Promise((resolve2, reject) => {
|
|
2861
472
|
const timer = setTimeout(() => {
|
|
@@ -2874,8 +485,8 @@ var WebSocketBridge = class {
|
|
|
2874
485
|
}
|
|
2875
486
|
handleMessage(msg) {
|
|
2876
487
|
if (msg.type === "open_crawlio_app") {
|
|
2877
|
-
import("child_process").then(({ execFile }) => {
|
|
2878
|
-
|
|
488
|
+
import("child_process").then(({ execFile: execFile2 }) => {
|
|
489
|
+
execFile2("open", ["-a", "Crawlio"], () => {
|
|
2879
490
|
});
|
|
2880
491
|
});
|
|
2881
492
|
return;
|
|
@@ -2908,6 +519,10 @@ var WebSocketBridge = class {
|
|
|
2908
519
|
}
|
|
2909
520
|
async stop() {
|
|
2910
521
|
this.stopHeartbeat();
|
|
522
|
+
if (this.reconnectGraceTimer) {
|
|
523
|
+
clearTimeout(this.reconnectGraceTimer);
|
|
524
|
+
this.reconnectGraceTimer = null;
|
|
525
|
+
}
|
|
2911
526
|
this.messageQueue.clear();
|
|
2912
527
|
this.client?.close();
|
|
2913
528
|
this.wss?.close();
|
|
@@ -3083,6 +698,476 @@ var CrawlioClient = class {
|
|
|
3083
698
|
|
|
3084
699
|
// src/mcp-server/tools.ts
|
|
3085
700
|
import { z } from "zod";
|
|
701
|
+
import { execFile } from "child_process";
|
|
702
|
+
import { writeFile, unlink } from "fs/promises";
|
|
703
|
+
import { tmpdir } from "os";
|
|
704
|
+
import { join as join2, dirname as dirname3 } from "path";
|
|
705
|
+
import { randomBytes } from "crypto";
|
|
706
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
707
|
+
import { promisify } from "util";
|
|
708
|
+
|
|
709
|
+
// src/mcp-server/response-shapers.ts
|
|
710
|
+
function truncateUrl(url, max = 120) {
|
|
711
|
+
if (url.length <= max) return url;
|
|
712
|
+
return url.slice(0, max - 3) + "...";
|
|
713
|
+
}
|
|
714
|
+
function shapeListTabs(data) {
|
|
715
|
+
const raw = Array.isArray(data) ? { tabs: data, connectedTabId: null } : data;
|
|
716
|
+
return {
|
|
717
|
+
connectedTabId: raw.connectedTabId ?? null,
|
|
718
|
+
tabs: raw.tabs.map((t) => ({
|
|
719
|
+
id: t.tabId,
|
|
720
|
+
title: t.title,
|
|
721
|
+
url: truncateUrl(t.url),
|
|
722
|
+
...t.active ? { active: true } : {}
|
|
723
|
+
}))
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function shapeConnectTab(data) {
|
|
727
|
+
const shaped = {
|
|
728
|
+
tabId: data.tabId,
|
|
729
|
+
url: typeof data.url === "string" ? truncateUrl(data.url) : data.url,
|
|
730
|
+
title: data.title
|
|
731
|
+
};
|
|
732
|
+
const ds = data.domainState;
|
|
733
|
+
if (ds?.required) {
|
|
734
|
+
const failed = ds.required.filter((d) => !d.success);
|
|
735
|
+
if (failed.length > 0) {
|
|
736
|
+
shaped.ok = false;
|
|
737
|
+
shaped.failedDomains = failed.map((d) => d.domain);
|
|
738
|
+
} else {
|
|
739
|
+
shaped.ok = true;
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
shaped.ok = true;
|
|
743
|
+
}
|
|
744
|
+
return shaped;
|
|
745
|
+
}
|
|
746
|
+
function summarizeDom(node) {
|
|
747
|
+
const stats = { nodeCount: 0, forms: 0, links: 0, images: 0, inputs: 0 };
|
|
748
|
+
const walk = (n) => {
|
|
749
|
+
stats.nodeCount++;
|
|
750
|
+
const tag = n.tag?.toLowerCase();
|
|
751
|
+
if (tag === "form") stats.forms++;
|
|
752
|
+
else if (tag === "a") stats.links++;
|
|
753
|
+
else if (tag === "img") stats.images++;
|
|
754
|
+
else if (tag === "input" || tag === "textarea" || tag === "select") stats.inputs++;
|
|
755
|
+
if (n.children) n.children.forEach(walk);
|
|
756
|
+
};
|
|
757
|
+
walk(node);
|
|
758
|
+
return stats;
|
|
759
|
+
}
|
|
760
|
+
function shapeCapturePage(data) {
|
|
761
|
+
const shaped = {
|
|
762
|
+
url: truncateUrl(data.url),
|
|
763
|
+
title: data.title,
|
|
764
|
+
capturedAt: data.capturedAt
|
|
765
|
+
};
|
|
766
|
+
if (data.framework) {
|
|
767
|
+
shaped.framework = data.framework;
|
|
768
|
+
}
|
|
769
|
+
if (data.networkRequests) {
|
|
770
|
+
const entries = data.networkRequests;
|
|
771
|
+
const failed = entries.filter((e) => e.status >= 400 || e.status === 0);
|
|
772
|
+
const byType = {};
|
|
773
|
+
for (const e of entries) {
|
|
774
|
+
const t = e.resourceType || "other";
|
|
775
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
776
|
+
}
|
|
777
|
+
shaped.network = {
|
|
778
|
+
total: entries.length,
|
|
779
|
+
failed: failed.length,
|
|
780
|
+
byType,
|
|
781
|
+
errors: failed.slice(0, 10).map((e) => ({
|
|
782
|
+
url: truncateUrl(e.url, 80),
|
|
783
|
+
status: e.status,
|
|
784
|
+
method: e.method
|
|
785
|
+
}))
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (data.consoleLogs) {
|
|
789
|
+
const entries = data.consoleLogs;
|
|
790
|
+
const errors = entries.filter((e) => e.level === "error");
|
|
791
|
+
const warnings = entries.filter((e) => e.level === "warning");
|
|
792
|
+
shaped.console = {
|
|
793
|
+
total: entries.length,
|
|
794
|
+
errors: errors.slice(0, 10).map((e) => ({ level: e.level, text: e.text.slice(0, 200) })),
|
|
795
|
+
warnings: warnings.length,
|
|
796
|
+
info: entries.filter((e) => e.level === "info").length,
|
|
797
|
+
debug: entries.filter((e) => e.level === "debug").length
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (data.cookies) {
|
|
801
|
+
shaped.cookies = {
|
|
802
|
+
total: data.cookies.length,
|
|
803
|
+
names: data.cookies.map((c) => c.name)
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (data.domSnapshot) {
|
|
807
|
+
shaped.dom = summarizeDom(data.domSnapshot);
|
|
808
|
+
}
|
|
809
|
+
return shaped;
|
|
810
|
+
}
|
|
811
|
+
function shapeConsoleLogs(entries) {
|
|
812
|
+
const errors = entries.filter((e) => e.level === "error");
|
|
813
|
+
const warnings = entries.filter((e) => e.level === "warning");
|
|
814
|
+
return {
|
|
815
|
+
total: entries.length,
|
|
816
|
+
errors: errors.map((e) => ({
|
|
817
|
+
text: e.text.slice(0, 300),
|
|
818
|
+
url: e.url ? truncateUrl(e.url, 80) : void 0,
|
|
819
|
+
lineNumber: e.lineNumber
|
|
820
|
+
})),
|
|
821
|
+
warnings: warnings.slice(0, 10).map((e) => ({
|
|
822
|
+
text: e.text.slice(0, 200)
|
|
823
|
+
})),
|
|
824
|
+
info: entries.filter((e) => e.level === "info").length,
|
|
825
|
+
debug: entries.filter((e) => e.level === "debug").length
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function shapeNetworkLog(entries) {
|
|
829
|
+
const failed = entries.filter((e) => e.status >= 400 || e.status === 0);
|
|
830
|
+
const byStatus = {};
|
|
831
|
+
const byType = {};
|
|
832
|
+
for (const e of entries) {
|
|
833
|
+
const bucket = e.status === 0 ? "0xx" : `${Math.floor(e.status / 100)}xx`;
|
|
834
|
+
byStatus[bucket] = (byStatus[bucket] || 0) + 1;
|
|
835
|
+
const t = e.resourceType || "other";
|
|
836
|
+
byType[t] = (byType[t] || 0) + 1;
|
|
837
|
+
}
|
|
838
|
+
const slowest = [...entries].sort((a, b) => b.durationMs - a.durationMs).slice(0, 5).map((e) => ({
|
|
839
|
+
url: truncateUrl(e.url, 80),
|
|
840
|
+
durationMs: e.durationMs,
|
|
841
|
+
status: e.status
|
|
842
|
+
}));
|
|
843
|
+
return {
|
|
844
|
+
total: entries.length,
|
|
845
|
+
failed: failed.map((e) => ({
|
|
846
|
+
url: truncateUrl(e.url, 80),
|
|
847
|
+
status: e.status,
|
|
848
|
+
method: e.method
|
|
849
|
+
})),
|
|
850
|
+
byStatus,
|
|
851
|
+
byType,
|
|
852
|
+
slowest
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function shapeCookies(data) {
|
|
856
|
+
return {
|
|
857
|
+
total: data.cookies.length,
|
|
858
|
+
fallbackUsed: data.fallbackUsed ?? false,
|
|
859
|
+
cookies: data.cookies.map((c) => ({
|
|
860
|
+
name: c.name,
|
|
861
|
+
domain: c.domain,
|
|
862
|
+
httpOnly: c.httpOnly,
|
|
863
|
+
secure: c.secure,
|
|
864
|
+
sameSite: c.sameSite
|
|
865
|
+
}))
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function shapeInteraction(data) {
|
|
869
|
+
if (!data || typeof data !== "object") return data;
|
|
870
|
+
const d = data;
|
|
871
|
+
const shaped = {};
|
|
872
|
+
const keep = [
|
|
873
|
+
"action",
|
|
874
|
+
"selector",
|
|
875
|
+
"ref",
|
|
876
|
+
"url",
|
|
877
|
+
"title",
|
|
878
|
+
"text",
|
|
879
|
+
"key",
|
|
880
|
+
"message",
|
|
881
|
+
"snapshot",
|
|
882
|
+
"previousValue",
|
|
883
|
+
"newValue",
|
|
884
|
+
"filesCount",
|
|
885
|
+
"success",
|
|
886
|
+
"filled",
|
|
887
|
+
"submitted"
|
|
888
|
+
];
|
|
889
|
+
for (const k of keep) {
|
|
890
|
+
if (d[k] !== void 0) shaped[k] = d[k];
|
|
891
|
+
}
|
|
892
|
+
if (typeof shaped.url === "string") {
|
|
893
|
+
shaped.url = truncateUrl(shaped.url);
|
|
894
|
+
}
|
|
895
|
+
return shaped;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/mcp-server/recording-compiler.ts
|
|
899
|
+
function compileRecording(session, options) {
|
|
900
|
+
const name = sanitizeSkillName(options.name);
|
|
901
|
+
const totalInteractions = session.pages.reduce((sum, p) => sum + p.interactions.length, 0);
|
|
902
|
+
const description = options.description ?? `Replay of a recorded browser session (${session.pages.length} pages, ${totalInteractions} interactions).`;
|
|
903
|
+
const lines = [];
|
|
904
|
+
lines.push("---");
|
|
905
|
+
lines.push(`name: ${name}`);
|
|
906
|
+
lines.push(`description: ${description}`);
|
|
907
|
+
lines.push("allowed-tools: mcp__crawlio-browser__search, mcp__crawlio-browser__execute, mcp__crawlio-browser__connect_tab");
|
|
908
|
+
lines.push("---");
|
|
909
|
+
lines.push("");
|
|
910
|
+
const title = options.name.trim() || "Unnamed Skill";
|
|
911
|
+
lines.push(`# ${title}`);
|
|
912
|
+
lines.push("");
|
|
913
|
+
lines.push(`Replay of a recorded browser session (${session.pages.length} pages, ${totalInteractions} interactions).`);
|
|
914
|
+
lines.push("");
|
|
915
|
+
lines.push("## Prerequisites");
|
|
916
|
+
lines.push("");
|
|
917
|
+
lines.push("Connect to a browser tab before running:");
|
|
918
|
+
lines.push("");
|
|
919
|
+
lines.push("```");
|
|
920
|
+
lines.push(`connect_tab({ url: "${escapeString(session.metadata.initialUrl)}" })`);
|
|
921
|
+
lines.push("```");
|
|
922
|
+
lines.push("");
|
|
923
|
+
for (let i = 0; i < session.pages.length; i++) {
|
|
924
|
+
const page = session.pages[i];
|
|
925
|
+
const pageLabel = page.title || page.url;
|
|
926
|
+
lines.push(`## Page ${i + 1}: ${pageLabel}`);
|
|
927
|
+
lines.push("");
|
|
928
|
+
lines.push("```js");
|
|
929
|
+
for (const interaction of page.interactions) {
|
|
930
|
+
lines.push(compileInteraction(interaction));
|
|
931
|
+
}
|
|
932
|
+
lines.push("```");
|
|
933
|
+
lines.push("");
|
|
934
|
+
if (i < session.pages.length - 1) {
|
|
935
|
+
lines.push("```js");
|
|
936
|
+
lines.push("await sleep(1000)");
|
|
937
|
+
lines.push("await smart.snapshot()");
|
|
938
|
+
lines.push("```");
|
|
939
|
+
lines.push("");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
lines.push("## Session Info");
|
|
943
|
+
lines.push("");
|
|
944
|
+
lines.push(`- **Recorded**: ${session.startedAt}`);
|
|
945
|
+
lines.push(`- **Duration**: ${session.duration}s`);
|
|
946
|
+
lines.push(`- **Stop reason**: ${session.metadata.stopReason}`);
|
|
947
|
+
lines.push("");
|
|
948
|
+
return {
|
|
949
|
+
skillMarkdown: lines.join("\n"),
|
|
950
|
+
name,
|
|
951
|
+
pageCount: session.pages.length,
|
|
952
|
+
interactionCount: totalInteractions
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
var STRIP_KEYS = /* @__PURE__ */ new Set(["type", "id", "_internal"]);
|
|
956
|
+
function filterArgs(args) {
|
|
957
|
+
const filtered = {};
|
|
958
|
+
for (const [k, v] of Object.entries(args)) {
|
|
959
|
+
if (!STRIP_KEYS.has(k)) filtered[k] = v;
|
|
960
|
+
}
|
|
961
|
+
return filtered;
|
|
962
|
+
}
|
|
963
|
+
function resolveSelector(args) {
|
|
964
|
+
const sel = args.selector ?? args.ref;
|
|
965
|
+
return typeof sel === "string" ? sel : void 0;
|
|
966
|
+
}
|
|
967
|
+
function compileInteraction(interaction) {
|
|
968
|
+
const args = filterArgs(interaction.args);
|
|
969
|
+
const tool = interaction.tool;
|
|
970
|
+
switch (tool) {
|
|
971
|
+
case "browser_navigate": {
|
|
972
|
+
const url = args.url ?? "";
|
|
973
|
+
return `await smart.navigate(${JSON.stringify(url)})`;
|
|
974
|
+
}
|
|
975
|
+
case "browser_click": {
|
|
976
|
+
const sel = resolveSelector(args) ?? "";
|
|
977
|
+
return `await smart.click(${JSON.stringify(sel)})`;
|
|
978
|
+
}
|
|
979
|
+
case "browser_type": {
|
|
980
|
+
const sel = resolveSelector(args) ?? "";
|
|
981
|
+
const text = args.text ?? "";
|
|
982
|
+
if (args.clearFirst) {
|
|
983
|
+
return `await smart.type(${JSON.stringify(sel)}, ${JSON.stringify(text)}, { clearFirst: true })`;
|
|
984
|
+
}
|
|
985
|
+
return `await smart.type(${JSON.stringify(sel)}, ${JSON.stringify(text)})`;
|
|
986
|
+
}
|
|
987
|
+
case "browser_evaluate": {
|
|
988
|
+
const expr = args.expression ?? "";
|
|
989
|
+
return `await smart.evaluate(${JSON.stringify(expr)})`;
|
|
990
|
+
}
|
|
991
|
+
case "browser_press_key": {
|
|
992
|
+
const bridgeArgs = { type: "browser_press_key", ...args };
|
|
993
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
994
|
+
}
|
|
995
|
+
case "browser_hover": {
|
|
996
|
+
const sel = resolveSelector(args);
|
|
997
|
+
const bridgeArgs = { type: "browser_hover", ...args };
|
|
998
|
+
if (sel) {
|
|
999
|
+
return `await smart.waitFor(${JSON.stringify(sel)})
|
|
1000
|
+
await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1001
|
+
}
|
|
1002
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1003
|
+
}
|
|
1004
|
+
case "browser_select_option": {
|
|
1005
|
+
const sel = resolveSelector(args);
|
|
1006
|
+
const bridgeArgs = { type: "browser_select_option", ...args };
|
|
1007
|
+
if (sel) {
|
|
1008
|
+
return `await smart.waitFor(${JSON.stringify(sel)})
|
|
1009
|
+
await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1010
|
+
}
|
|
1011
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1012
|
+
}
|
|
1013
|
+
case "browser_scroll": {
|
|
1014
|
+
const bridgeArgs = { type: "browser_scroll", ...args };
|
|
1015
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1016
|
+
}
|
|
1017
|
+
case "browser_double_click": {
|
|
1018
|
+
const sel = resolveSelector(args);
|
|
1019
|
+
const bridgeArgs = { type: "browser_double_click", ...args };
|
|
1020
|
+
if (sel) {
|
|
1021
|
+
return `await smart.waitFor(${JSON.stringify(sel)})
|
|
1022
|
+
await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1023
|
+
}
|
|
1024
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1025
|
+
}
|
|
1026
|
+
case "browser_drag": {
|
|
1027
|
+
const bridgeArgs = { type: "browser_drag", ...args };
|
|
1028
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1029
|
+
}
|
|
1030
|
+
case "browser_fill_form": {
|
|
1031
|
+
const bridgeArgs = { type: "browser_fill_form", ...args };
|
|
1032
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1033
|
+
}
|
|
1034
|
+
case "browser_file_upload": {
|
|
1035
|
+
const sel = resolveSelector(args);
|
|
1036
|
+
const bridgeArgs = { type: "browser_file_upload", ...args };
|
|
1037
|
+
if (sel) {
|
|
1038
|
+
return `await smart.waitFor(${JSON.stringify(sel)})
|
|
1039
|
+
await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1040
|
+
}
|
|
1041
|
+
return `await bridge.send(${JSON.stringify(bridgeArgs)})`;
|
|
1042
|
+
}
|
|
1043
|
+
// User interaction tools (captured from manual browser events)
|
|
1044
|
+
case "user_click": {
|
|
1045
|
+
const sel = args.selector ?? "";
|
|
1046
|
+
return `await smart.click(${JSON.stringify(sel)})`;
|
|
1047
|
+
}
|
|
1048
|
+
case "user_type": {
|
|
1049
|
+
const sel = args.selector ?? "";
|
|
1050
|
+
const text = args.text ?? "";
|
|
1051
|
+
return `await smart.type(${JSON.stringify(sel)}, ${JSON.stringify(text)})`;
|
|
1052
|
+
}
|
|
1053
|
+
case "user_keypress": {
|
|
1054
|
+
const key = args.key ?? "";
|
|
1055
|
+
return `await bridge.send(${JSON.stringify({ type: "browser_press_key", key })})`;
|
|
1056
|
+
}
|
|
1057
|
+
default:
|
|
1058
|
+
return `// Unknown tool: ${tool}`;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function sanitizeSkillName(raw) {
|
|
1062
|
+
let name = raw.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
1063
|
+
if (name.length > 50) name = name.slice(0, 50).replace(/-+$/, "");
|
|
1064
|
+
if (!name) name = "unnamed-skill";
|
|
1065
|
+
return name;
|
|
1066
|
+
}
|
|
1067
|
+
function escapeString(s) {
|
|
1068
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/mcp-server/semantic-search.ts
|
|
1072
|
+
import { readFileSync } from "fs";
|
|
1073
|
+
import { join, dirname as dirname2 } from "path";
|
|
1074
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1075
|
+
function cosineSimilarity(a, b) {
|
|
1076
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
1077
|
+
let dot = 0;
|
|
1078
|
+
let normA = 0;
|
|
1079
|
+
let normB = 0;
|
|
1080
|
+
for (let i = 0; i < a.length; i++) {
|
|
1081
|
+
dot += a[i] * b[i];
|
|
1082
|
+
normA += a[i] * a[i];
|
|
1083
|
+
normB += b[i] * b[i];
|
|
1084
|
+
}
|
|
1085
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1086
|
+
if (denom === 0) return 0;
|
|
1087
|
+
return dot / denom;
|
|
1088
|
+
}
|
|
1089
|
+
function loadEmbeddings() {
|
|
1090
|
+
const map = /* @__PURE__ */ new Map();
|
|
1091
|
+
try {
|
|
1092
|
+
const thisDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1093
|
+
const assetPath = join(thisDir, "tool-embeddings.json");
|
|
1094
|
+
const raw = readFileSync(assetPath, "utf-8");
|
|
1095
|
+
const asset = JSON.parse(raw);
|
|
1096
|
+
if (!asset.embeddings || typeof asset.embeddings !== "object") return map;
|
|
1097
|
+
for (const [name, vec] of Object.entries(asset.embeddings)) {
|
|
1098
|
+
if (Array.isArray(vec) && vec.length > 0) {
|
|
1099
|
+
map.set(name, new Float32Array(vec));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
} catch {
|
|
1103
|
+
}
|
|
1104
|
+
return map;
|
|
1105
|
+
}
|
|
1106
|
+
function semanticSearch(queryEmbedding, embeddings, limit) {
|
|
1107
|
+
const results = [];
|
|
1108
|
+
for (const [name, vec] of embeddings) {
|
|
1109
|
+
const score = cosineSimilarity(queryEmbedding, vec);
|
|
1110
|
+
results.push({ name, score });
|
|
1111
|
+
}
|
|
1112
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1113
|
+
}
|
|
1114
|
+
function buildQueryEmbedding(query, catalog, embeddings) {
|
|
1115
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1116
|
+
if (tokens.length === 0 || embeddings.size === 0) return null;
|
|
1117
|
+
const matchedVectors = [];
|
|
1118
|
+
for (const token of tokens) {
|
|
1119
|
+
let bestName = "";
|
|
1120
|
+
let bestScore = 0;
|
|
1121
|
+
for (const entry of catalog) {
|
|
1122
|
+
if (!embeddings.has(entry.name)) continue;
|
|
1123
|
+
let score = 0;
|
|
1124
|
+
const nameLower = entry.name.toLowerCase();
|
|
1125
|
+
const descLower = entry.description.toLowerCase();
|
|
1126
|
+
if (nameLower === token) score = 10;
|
|
1127
|
+
else if (nameLower.includes(token)) score = 5;
|
|
1128
|
+
if (descLower.includes(token)) score += 2;
|
|
1129
|
+
if (score > bestScore) {
|
|
1130
|
+
bestScore = score;
|
|
1131
|
+
bestName = entry.name;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (bestName && bestScore > 0) {
|
|
1135
|
+
const vec = embeddings.get(bestName);
|
|
1136
|
+
if (vec) matchedVectors.push(vec);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (matchedVectors.length === 0) return null;
|
|
1140
|
+
const dim = matchedVectors[0].length;
|
|
1141
|
+
const avg = new Float32Array(dim);
|
|
1142
|
+
for (const vec of matchedVectors) {
|
|
1143
|
+
for (let i = 0; i < dim; i++) {
|
|
1144
|
+
avg[i] += vec[i];
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const count = matchedVectors.length;
|
|
1148
|
+
for (let i = 0; i < dim; i++) {
|
|
1149
|
+
avg[i] /= count;
|
|
1150
|
+
}
|
|
1151
|
+
return avg;
|
|
1152
|
+
}
|
|
1153
|
+
function normalizeScores(scores) {
|
|
1154
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
1155
|
+
if (scores.size === 0) return normalized;
|
|
1156
|
+
let min = Infinity;
|
|
1157
|
+
let max = -Infinity;
|
|
1158
|
+
for (const s of scores.values()) {
|
|
1159
|
+
if (s < min) min = s;
|
|
1160
|
+
if (s > max) max = s;
|
|
1161
|
+
}
|
|
1162
|
+
const range = max - min;
|
|
1163
|
+
for (const [name, score] of scores) {
|
|
1164
|
+
normalized.set(name, range === 0 ? max > 0 ? 1 : 0 : (score - min) / range);
|
|
1165
|
+
}
|
|
1166
|
+
return normalized;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/mcp-server/tools.ts
|
|
1170
|
+
var execFileAsync = promisify(execFile);
|
|
3086
1171
|
var urlSchema = z.string().url().refine(
|
|
3087
1172
|
(url) => {
|
|
3088
1173
|
try {
|
|
@@ -3110,10 +1195,12 @@ var PERMISSION_EXEMPT_TOOLS = /* @__PURE__ */ new Set([
|
|
|
3110
1195
|
"check_permissions",
|
|
3111
1196
|
"request_permissions",
|
|
3112
1197
|
"get_connection_status",
|
|
1198
|
+
"get_recording_status",
|
|
3113
1199
|
"connect_tab",
|
|
3114
1200
|
"list_tabs",
|
|
3115
1201
|
"search",
|
|
3116
|
-
"execute"
|
|
1202
|
+
"execute",
|
|
1203
|
+
"compile_recording"
|
|
3117
1204
|
]);
|
|
3118
1205
|
async function ensurePermission(bridge2, toolName) {
|
|
3119
1206
|
try {
|
|
@@ -3189,6 +1276,7 @@ var TOOL_TIMEOUTS = {
|
|
|
3189
1276
|
get_dialog: 5e3,
|
|
3190
1277
|
handle_dialog: 5e3,
|
|
3191
1278
|
get_response_body: 5e3,
|
|
1279
|
+
replay_request: 3e4,
|
|
3192
1280
|
set_viewport: 5e3,
|
|
3193
1281
|
set_user_agent: 5e3,
|
|
3194
1282
|
emulate_device: 1e4,
|
|
@@ -3236,10 +1324,15 @@ var TOOL_TIMEOUTS = {
|
|
|
3236
1324
|
take_heap_snapshot: 3e4,
|
|
3237
1325
|
highlight_element: 1e4,
|
|
3238
1326
|
show_layout_shifts: 5e3,
|
|
3239
|
-
show_paint_rects: 5e3
|
|
1327
|
+
show_paint_rects: 5e3,
|
|
1328
|
+
start_recording: 1e4,
|
|
1329
|
+
stop_recording: 1e4,
|
|
1330
|
+
get_recording_status: 5e3,
|
|
1331
|
+
compile_recording: 5e3,
|
|
1332
|
+
ocr_screenshot: 3e4
|
|
3240
1333
|
};
|
|
3241
1334
|
function toolSuccess(content) {
|
|
3242
|
-
return { content: [{ type: "text", text: JSON.stringify(content ?? {}
|
|
1335
|
+
return { content: [{ type: "text", text: JSON.stringify(content ?? {}) }], isError: false };
|
|
3243
1336
|
}
|
|
3244
1337
|
function toolError(message) {
|
|
3245
1338
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -3249,6 +1342,7 @@ var smartObjectCache = null;
|
|
|
3249
1342
|
async function checkActionability(bridge2, selector) {
|
|
3250
1343
|
const result = await bridge2.send({
|
|
3251
1344
|
type: "browser_evaluate",
|
|
1345
|
+
_internal: true,
|
|
3252
1346
|
expression: `(() => {
|
|
3253
1347
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
3254
1348
|
if (!el) return { actionable: false, reason: 'Element not found' };
|
|
@@ -3344,7 +1438,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3344
1438
|
if (parsed.url) params.url = parsed.url;
|
|
3345
1439
|
if (parsed.tabId) params.tabId = parsed.tabId;
|
|
3346
1440
|
const data = await bridge2.send(params, TOOL_TIMEOUTS.connect_tab);
|
|
3347
|
-
return toolSuccess(data);
|
|
1441
|
+
return toolSuccess(shapeConnectTab(data));
|
|
3348
1442
|
}
|
|
3349
1443
|
},
|
|
3350
1444
|
{
|
|
@@ -3362,7 +1456,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3362
1456
|
inputSchema: { type: "object", properties: {} },
|
|
3363
1457
|
handler: async () => {
|
|
3364
1458
|
const data = await bridge2.send({ type: "list_tabs" }, TOOL_TIMEOUTS.list_tabs);
|
|
3365
|
-
return toolSuccess(data);
|
|
1459
|
+
return toolSuccess(shapeListTabs(data));
|
|
3366
1460
|
}
|
|
3367
1461
|
},
|
|
3368
1462
|
{
|
|
@@ -3418,7 +1512,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3418
1512
|
console.error("[Tools] Failed to send to Crawlio:", e);
|
|
3419
1513
|
}
|
|
3420
1514
|
}
|
|
3421
|
-
return toolSuccess(data);
|
|
1515
|
+
return toolSuccess(shapeCapturePage(data));
|
|
3422
1516
|
}
|
|
3423
1517
|
},
|
|
3424
1518
|
{
|
|
@@ -3445,7 +1539,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3445
1539
|
inputSchema: { type: "object", properties: {} },
|
|
3446
1540
|
handler: async () => {
|
|
3447
1541
|
const data = await bridge2.send({ type: "stop_network_capture" }, TOOL_TIMEOUTS.stop_network_capture);
|
|
3448
|
-
return toolSuccess(data);
|
|
1542
|
+
return toolSuccess(shapeNetworkLog(data));
|
|
3449
1543
|
}
|
|
3450
1544
|
},
|
|
3451
1545
|
{
|
|
@@ -3454,7 +1548,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3454
1548
|
inputSchema: { type: "object", properties: {} },
|
|
3455
1549
|
handler: async () => {
|
|
3456
1550
|
const data = await bridge2.send({ type: "get_console_logs" }, TOOL_TIMEOUTS.get_console_logs);
|
|
3457
|
-
return toolSuccess(data);
|
|
1551
|
+
return toolSuccess(shapeConsoleLogs(data));
|
|
3458
1552
|
}
|
|
3459
1553
|
},
|
|
3460
1554
|
{
|
|
@@ -3464,7 +1558,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3464
1558
|
handler: async () => {
|
|
3465
1559
|
try {
|
|
3466
1560
|
const data = await bridge2.send({ type: "get_cookies" }, TOOL_TIMEOUTS.get_cookies);
|
|
3467
|
-
return toolSuccess(data);
|
|
1561
|
+
return toolSuccess(shapeCookies(data));
|
|
3468
1562
|
} catch (e) {
|
|
3469
1563
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3470
1564
|
return toolError(`get_cookies failed: ${msg}. CDP domain: Network. Suggestion: use reconnect_tab to re-establish the CDP connection.`);
|
|
@@ -3538,6 +1632,11 @@ function createTools(bridge2, crawlio2) {
|
|
|
3538
1632
|
handler: async (args) => {
|
|
3539
1633
|
const schema = z.object({ url: urlSchema.optional() });
|
|
3540
1634
|
const parsed = schema.parse(args);
|
|
1635
|
+
try {
|
|
1636
|
+
const bridgeData = await bridge2.send({ type: "get_enrichment", url: parsed.url }, 5e3);
|
|
1637
|
+
if (bridgeData) return toolSuccess(bridgeData);
|
|
1638
|
+
} catch {
|
|
1639
|
+
}
|
|
3541
1640
|
const data = await crawlio2.getEnrichment(parsed.url);
|
|
3542
1641
|
return toolSuccess(data);
|
|
3543
1642
|
}
|
|
@@ -3683,7 +1782,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3683
1782
|
button: parsed.button,
|
|
3684
1783
|
modifiers: parsed.modifiers
|
|
3685
1784
|
}, TOOL_TIMEOUTS.browser_click);
|
|
3686
|
-
return toolSuccess(result);
|
|
1785
|
+
return toolSuccess(shapeInteraction(result));
|
|
3687
1786
|
}
|
|
3688
1787
|
)
|
|
3689
1788
|
},
|
|
@@ -3736,7 +1835,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3736
1835
|
submit: parsed.submit,
|
|
3737
1836
|
modifiers: parsed.modifiers
|
|
3738
1837
|
}, TOOL_TIMEOUTS.browser_type);
|
|
3739
|
-
return toolSuccess(result);
|
|
1838
|
+
return toolSuccess(shapeInteraction(result));
|
|
3740
1839
|
}
|
|
3741
1840
|
)
|
|
3742
1841
|
},
|
|
@@ -3771,7 +1870,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3771
1870
|
key: parsed.key,
|
|
3772
1871
|
modifiers: parsed.modifiers
|
|
3773
1872
|
}, TOOL_TIMEOUTS.browser_press_key);
|
|
3774
|
-
return toolSuccess(result);
|
|
1873
|
+
return toolSuccess(shapeInteraction(result));
|
|
3775
1874
|
}
|
|
3776
1875
|
},
|
|
3777
1876
|
{
|
|
@@ -3807,7 +1906,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3807
1906
|
selector: parsed.selector,
|
|
3808
1907
|
modifiers: parsed.modifiers
|
|
3809
1908
|
}, TOOL_TIMEOUTS.browser_hover);
|
|
3810
|
-
return toolSuccess(result);
|
|
1909
|
+
return toolSuccess(shapeInteraction(result));
|
|
3811
1910
|
}
|
|
3812
1911
|
},
|
|
3813
1912
|
{
|
|
@@ -3838,7 +1937,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3838
1937
|
selector: parsed.selector,
|
|
3839
1938
|
value: parsed.value
|
|
3840
1939
|
}, TOOL_TIMEOUTS.browser_select_option);
|
|
3841
|
-
return toolSuccess(result);
|
|
1940
|
+
return toolSuccess(shapeInteraction(result));
|
|
3842
1941
|
}
|
|
3843
1942
|
)
|
|
3844
1943
|
},
|
|
@@ -3894,7 +1993,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
3894
1993
|
type: "browser_fill_form",
|
|
3895
1994
|
fields: parsed.fields
|
|
3896
1995
|
}, TOOL_TIMEOUTS.browser_fill_form ?? 15e3);
|
|
3897
|
-
return toolSuccess(result);
|
|
1996
|
+
return toolSuccess(shapeInteraction(result));
|
|
3898
1997
|
}
|
|
3899
1998
|
},
|
|
3900
1999
|
{
|
|
@@ -4316,6 +2415,41 @@ function createTools(bridge2, crawlio2) {
|
|
|
4316
2415
|
return toolSuccess(data);
|
|
4317
2416
|
}
|
|
4318
2417
|
},
|
|
2418
|
+
// Network replay (PiecesOS Heist Ph3)
|
|
2419
|
+
{
|
|
2420
|
+
name: "replay_request",
|
|
2421
|
+
description: "Re-fire a previously captured network request with optional modifications. Requires active network capture. Specify the request by URL. Optionally override headers, body, or method. Returns the new response status, headers, and body. Useful for API testing, auth token replay, and form submission testing.",
|
|
2422
|
+
inputSchema: {
|
|
2423
|
+
type: "object",
|
|
2424
|
+
properties: {
|
|
2425
|
+
url: { type: "string", description: "URL of the captured request to replay" },
|
|
2426
|
+
method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], description: "Override HTTP method" },
|
|
2427
|
+
headers: { type: "object", description: "Override or add headers (merged with original captured headers)" },
|
|
2428
|
+
body: { type: "string", description: "Override request body (for POST/PUT)" },
|
|
2429
|
+
followRedirects: { type: "boolean", description: "Follow 3xx redirects (default: true)" }
|
|
2430
|
+
},
|
|
2431
|
+
required: ["url"]
|
|
2432
|
+
},
|
|
2433
|
+
handler: async (args) => {
|
|
2434
|
+
const schema = z.object({
|
|
2435
|
+
url: z.string().url(),
|
|
2436
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]).optional(),
|
|
2437
|
+
headers: z.record(z.string()).optional(),
|
|
2438
|
+
body: z.string().optional(),
|
|
2439
|
+
followRedirects: z.boolean().optional()
|
|
2440
|
+
});
|
|
2441
|
+
const parsed = schema.parse(args);
|
|
2442
|
+
const data = await bridge2.send({
|
|
2443
|
+
type: "replay_request",
|
|
2444
|
+
url: parsed.url,
|
|
2445
|
+
method: parsed.method,
|
|
2446
|
+
headers: parsed.headers,
|
|
2447
|
+
body: parsed.body,
|
|
2448
|
+
followRedirects: parsed.followRedirects
|
|
2449
|
+
}, TOOL_TIMEOUTS.replay_request);
|
|
2450
|
+
return toolSuccess(data);
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
4319
2453
|
// AC-8: Viewport & device emulation
|
|
4320
2454
|
{
|
|
4321
2455
|
name: "set_viewport",
|
|
@@ -4460,7 +2594,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
4460
2594
|
type: "browser_scroll",
|
|
4461
2595
|
...parsed
|
|
4462
2596
|
}, TOOL_TIMEOUTS.browser_scroll);
|
|
4463
|
-
return toolSuccess(result);
|
|
2597
|
+
return toolSuccess(shapeInteraction(result));
|
|
4464
2598
|
}
|
|
4465
2599
|
},
|
|
4466
2600
|
{
|
|
@@ -4484,7 +2618,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
4484
2618
|
ref: parsed.ref,
|
|
4485
2619
|
selector: parsed.selector
|
|
4486
2620
|
}, TOOL_TIMEOUTS.browser_double_click);
|
|
4487
|
-
return toolSuccess(result);
|
|
2621
|
+
return toolSuccess(shapeInteraction(result));
|
|
4488
2622
|
}
|
|
4489
2623
|
},
|
|
4490
2624
|
{
|
|
@@ -4517,7 +2651,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
4517
2651
|
to: parsed.to,
|
|
4518
2652
|
steps: parsed.steps
|
|
4519
2653
|
}, TOOL_TIMEOUTS.browser_drag);
|
|
4520
|
-
return toolSuccess(result);
|
|
2654
|
+
return toolSuccess(shapeInteraction(result));
|
|
4521
2655
|
}
|
|
4522
2656
|
},
|
|
4523
2657
|
{
|
|
@@ -4546,7 +2680,7 @@ function createTools(bridge2, crawlio2) {
|
|
|
4546
2680
|
selector: parsed.selector,
|
|
4547
2681
|
files: parsed.files
|
|
4548
2682
|
}, TOOL_TIMEOUTS.browser_file_upload);
|
|
4549
|
-
return toolSuccess(result);
|
|
2683
|
+
return toolSuccess(shapeInteraction(result));
|
|
4550
2684
|
}
|
|
4551
2685
|
},
|
|
4552
2686
|
// AC-13: Accessibility tree
|
|
@@ -5306,6 +3440,190 @@ function createTools(bridge2, crawlio2) {
|
|
|
5306
3440
|
}, TOOL_TIMEOUTS.show_paint_rects);
|
|
5307
3441
|
return toolSuccess(data);
|
|
5308
3442
|
}
|
|
3443
|
+
},
|
|
3444
|
+
// --- Session Recording tools ---
|
|
3445
|
+
{
|
|
3446
|
+
name: "start_recording",
|
|
3447
|
+
description: "Start recording a browser session. Captures all tool interactions, page navigations, network requests, and console logs organized by page. Recording auto-stops after maxDurationSec (default 600s) or maxInteractions (default 500). A tab must be connected first.",
|
|
3448
|
+
inputSchema: {
|
|
3449
|
+
type: "object",
|
|
3450
|
+
properties: {
|
|
3451
|
+
maxDurationSec: { type: "number", description: "Max recording duration in seconds (10-600, default: 600)" },
|
|
3452
|
+
maxInteractions: { type: "number", description: "Max interaction count before auto-stop (1-500, default: 500)" }
|
|
3453
|
+
}
|
|
3454
|
+
},
|
|
3455
|
+
handler: async (args) => {
|
|
3456
|
+
const schema = z.object({
|
|
3457
|
+
maxDurationSec: z.number().min(10).max(600).optional(),
|
|
3458
|
+
maxInteractions: z.number().min(1).max(500).optional()
|
|
3459
|
+
});
|
|
3460
|
+
const parsed = schema.parse(args);
|
|
3461
|
+
try {
|
|
3462
|
+
const data = await bridge2.send({
|
|
3463
|
+
type: "start_recording",
|
|
3464
|
+
maxDurationSec: parsed.maxDurationSec,
|
|
3465
|
+
maxInteractions: parsed.maxInteractions
|
|
3466
|
+
}, TOOL_TIMEOUTS.start_recording);
|
|
3467
|
+
return toolSuccess(data);
|
|
3468
|
+
} catch (e) {
|
|
3469
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3470
|
+
return toolError(`start_recording failed: ${msg}. Suggestion: ensure a tab is connected via connect_tab first.`);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
name: "stop_recording",
|
|
3476
|
+
description: "Stop the active recording session and return the complete session data. Returns a RecordingSession JSON with all pages, interactions (tool calls with args/results/timing), network requests, and console logs partitioned by page. If the recording was auto-stopped (duration/interaction limit/tab closed), returns that session.",
|
|
3477
|
+
inputSchema: {
|
|
3478
|
+
type: "object",
|
|
3479
|
+
properties: {}
|
|
3480
|
+
},
|
|
3481
|
+
handler: async () => {
|
|
3482
|
+
try {
|
|
3483
|
+
const data = await bridge2.send({ type: "stop_recording" }, TOOL_TIMEOUTS.stop_recording);
|
|
3484
|
+
return toolSuccess(data);
|
|
3485
|
+
} catch (e) {
|
|
3486
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3487
|
+
return toolError(`stop_recording failed: ${msg}`);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
},
|
|
3491
|
+
{
|
|
3492
|
+
name: "get_recording_status",
|
|
3493
|
+
description: "Check the current recording status. Returns whether a recording is active, session ID, duration, page count, interaction count, and current page URL.",
|
|
3494
|
+
inputSchema: {
|
|
3495
|
+
type: "object",
|
|
3496
|
+
properties: {}
|
|
3497
|
+
},
|
|
3498
|
+
handler: async () => {
|
|
3499
|
+
try {
|
|
3500
|
+
const data = await bridge2.send({ type: "get_recording_status" }, TOOL_TIMEOUTS.get_recording_status);
|
|
3501
|
+
return toolSuccess(data);
|
|
3502
|
+
} catch (e) {
|
|
3503
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3504
|
+
return toolError(`get_recording_status failed: ${msg}`);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
3508
|
+
{
|
|
3509
|
+
name: "compile_recording",
|
|
3510
|
+
description: "Compile a recording session into a replayable SKILL.md markdown file. Takes the session JSON from stop_recording output and produces executable skill markdown with smart.* and bridge.send() calls. Pure computation \u2014 no browser access required.",
|
|
3511
|
+
inputSchema: {
|
|
3512
|
+
type: "object",
|
|
3513
|
+
properties: {
|
|
3514
|
+
session: {
|
|
3515
|
+
type: "object",
|
|
3516
|
+
description: "The RecordingSession object returned by stop_recording"
|
|
3517
|
+
},
|
|
3518
|
+
name: {
|
|
3519
|
+
type: "string",
|
|
3520
|
+
description: "Name for the skill (will be kebab-cased)"
|
|
3521
|
+
},
|
|
3522
|
+
description: {
|
|
3523
|
+
type: "string",
|
|
3524
|
+
description: "Optional description for the skill frontmatter"
|
|
3525
|
+
}
|
|
3526
|
+
},
|
|
3527
|
+
required: ["session", "name"]
|
|
3528
|
+
},
|
|
3529
|
+
handler: async (args) => {
|
|
3530
|
+
const schema = z.object({
|
|
3531
|
+
session: z.object({
|
|
3532
|
+
id: z.string(),
|
|
3533
|
+
startedAt: z.string(),
|
|
3534
|
+
stoppedAt: z.string().optional(),
|
|
3535
|
+
duration: z.number(),
|
|
3536
|
+
pages: z.array(z.object({
|
|
3537
|
+
url: z.string(),
|
|
3538
|
+
title: z.string().optional(),
|
|
3539
|
+
enteredAt: z.string(),
|
|
3540
|
+
interactions: z.array(z.any())
|
|
3541
|
+
}).passthrough()),
|
|
3542
|
+
metadata: z.object({
|
|
3543
|
+
tabId: z.number(),
|
|
3544
|
+
initialUrl: z.string(),
|
|
3545
|
+
stopReason: z.string()
|
|
3546
|
+
}).passthrough()
|
|
3547
|
+
}).passthrough(),
|
|
3548
|
+
name: z.string().min(1),
|
|
3549
|
+
description: z.string().optional()
|
|
3550
|
+
});
|
|
3551
|
+
const parsed = schema.safeParse(args);
|
|
3552
|
+
if (!parsed.success) {
|
|
3553
|
+
return toolError(`Invalid arguments: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
|
|
3554
|
+
}
|
|
3555
|
+
try {
|
|
3556
|
+
const result = compileRecording(
|
|
3557
|
+
parsed.data.session,
|
|
3558
|
+
{ name: parsed.data.name, description: parsed.data.description }
|
|
3559
|
+
);
|
|
3560
|
+
return toolSuccess(result);
|
|
3561
|
+
} catch (e) {
|
|
3562
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3563
|
+
return toolError(`compile_recording failed: ${msg}`);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
},
|
|
3567
|
+
{
|
|
3568
|
+
name: "ocr_screenshot",
|
|
3569
|
+
description: "Extract text from the current page using macOS Vision.framework OCR. Takes a screenshot via CDP, runs native text recognition (VNRecognizeTextRequest), and returns recognized text with confidence scores and bounding regions. Works on canvas elements, images, anti-scraping sites, and any visual content invisible to DOM extraction.",
|
|
3570
|
+
inputSchema: {
|
|
3571
|
+
type: "object",
|
|
3572
|
+
properties: {
|
|
3573
|
+
fullPage: { type: "boolean", description: "Capture full scrollable page (default: viewport only)" },
|
|
3574
|
+
selector: { type: "string", description: "CSS selector \u2014 screenshot only this element" }
|
|
3575
|
+
}
|
|
3576
|
+
},
|
|
3577
|
+
handler: async (args) => {
|
|
3578
|
+
if (process.platform !== "darwin") {
|
|
3579
|
+
return toolError("ocr_screenshot requires macOS (Vision.framework). On other platforms, use browser_evaluate with a JS OCR library.");
|
|
3580
|
+
}
|
|
3581
|
+
const schema = z.object({
|
|
3582
|
+
fullPage: z.boolean().default(false),
|
|
3583
|
+
selector: selectorSchema.optional()
|
|
3584
|
+
});
|
|
3585
|
+
const parsed = schema.parse(args);
|
|
3586
|
+
const screenshotParams = { type: "take_screenshot" };
|
|
3587
|
+
if (parsed.fullPage) screenshotParams.fullPage = true;
|
|
3588
|
+
if (parsed.selector) screenshotParams.selector = parsed.selector;
|
|
3589
|
+
let tmpPath = "";
|
|
3590
|
+
try {
|
|
3591
|
+
const data = await bridge2.send(screenshotParams, TOOL_TIMEOUTS.ocr_screenshot);
|
|
3592
|
+
if (!data?.data) {
|
|
3593
|
+
return toolError("Screenshot capture failed \u2014 no image data returned");
|
|
3594
|
+
}
|
|
3595
|
+
tmpPath = join2(tmpdir(), `crawlio-ocr-${randomBytes(6).toString("hex")}.png`);
|
|
3596
|
+
await writeFile(tmpPath, Buffer.from(data.data, "base64"));
|
|
3597
|
+
const shimPath = join2(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "bin", "ocr-shim.swift");
|
|
3598
|
+
const { stdout, stderr } = await execFileAsync("swift", [shimPath, tmpPath], { timeout: 25e3 });
|
|
3599
|
+
if (!stdout.trim()) {
|
|
3600
|
+
return toolError(`OCR produced no output${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
3601
|
+
}
|
|
3602
|
+
const result = JSON.parse(stdout.trim());
|
|
3603
|
+
if (result.error) {
|
|
3604
|
+
return toolError(`OCR failed: ${result.error}`);
|
|
3605
|
+
}
|
|
3606
|
+
if (result.regions && result.regions.length > 20) {
|
|
3607
|
+
result.regions = result.regions.sort((a, b) => b.confidence - a.confidence).slice(0, 20);
|
|
3608
|
+
result.regionsLimited = true;
|
|
3609
|
+
}
|
|
3610
|
+
return toolSuccess(result);
|
|
3611
|
+
} catch (e) {
|
|
3612
|
+
const execErr = e;
|
|
3613
|
+
if (execErr?.stderr) {
|
|
3614
|
+
try {
|
|
3615
|
+
const parsed2 = JSON.parse(execErr.stderr.trim());
|
|
3616
|
+
if (parsed2.error) return toolError(`OCR failed: ${parsed2.error}`);
|
|
3617
|
+
} catch {
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3621
|
+
return toolError(`OCR failed: ${msg}`);
|
|
3622
|
+
} finally {
|
|
3623
|
+
if (tmpPath) unlink(tmpPath).catch(() => {
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
5309
3627
|
}
|
|
5310
3628
|
];
|
|
5311
3629
|
}
|
|
@@ -5514,17 +3832,36 @@ function buildCatalog(tools2) {
|
|
|
5514
3832
|
}));
|
|
5515
3833
|
return [...browserCatalog, ...crawlioHTTPCatalog];
|
|
5516
3834
|
}
|
|
5517
|
-
function searchCatalog(catalog, query, limit) {
|
|
3835
|
+
function searchCatalog(catalog, query, limit, embeddings) {
|
|
5518
3836
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
5519
3837
|
if (tokens.length === 0) return catalog.slice(0, limit);
|
|
5520
|
-
const
|
|
5521
|
-
|
|
3838
|
+
const keywordScores = /* @__PURE__ */ new Map();
|
|
3839
|
+
for (const entry of catalog) {
|
|
5522
3840
|
let score = 0;
|
|
5523
3841
|
for (const token of tokens) {
|
|
5524
3842
|
if (entry.name.toLowerCase() === token) score += 10;
|
|
5525
3843
|
else if (entry.name.toLowerCase().includes(token)) score += 5;
|
|
5526
3844
|
if (entry.description.toLowerCase().includes(token)) score += 2;
|
|
5527
3845
|
}
|
|
3846
|
+
keywordScores.set(entry.name, score);
|
|
3847
|
+
}
|
|
3848
|
+
const semanticScores = /* @__PURE__ */ new Map();
|
|
3849
|
+
if (embeddings && embeddings.size > 0) {
|
|
3850
|
+
const queryVec = buildQueryEmbedding(query, catalog, embeddings);
|
|
3851
|
+
if (queryVec) {
|
|
3852
|
+
const results = semanticSearch(queryVec, embeddings, embeddings.size);
|
|
3853
|
+
for (const r of results) {
|
|
3854
|
+
semanticScores.set(r.name, r.score);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const hasSemanticScores = semanticScores.size > 0;
|
|
3859
|
+
const normalizedKeyword = normalizeScores(keywordScores);
|
|
3860
|
+
const normalizedSemantic = hasSemanticScores ? normalizeScores(semanticScores) : /* @__PURE__ */ new Map();
|
|
3861
|
+
const scored = catalog.map((entry) => {
|
|
3862
|
+
const kw = normalizedKeyword.get(entry.name) ?? 0;
|
|
3863
|
+
const sem = normalizedSemantic.get(entry.name) ?? 0;
|
|
3864
|
+
const score = hasSemanticScores ? 0.6 * sem + 0.4 * kw : kw;
|
|
5528
3865
|
return { entry, score };
|
|
5529
3866
|
});
|
|
5530
3867
|
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.entry);
|
|
@@ -5532,6 +3869,7 @@ function searchCatalog(catalog, query, limit) {
|
|
|
5532
3869
|
function createCodeModeTools(bridge2, crawlio2) {
|
|
5533
3870
|
const allTools = createTools(bridge2, crawlio2);
|
|
5534
3871
|
const catalog = buildCatalog(allTools);
|
|
3872
|
+
const embeddings = loadEmbeddings();
|
|
5535
3873
|
const connectTab = allTools.find((t) => t.name === "connect_tab");
|
|
5536
3874
|
return [
|
|
5537
3875
|
// --- search: discover available commands ---
|
|
@@ -5552,7 +3890,7 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5552
3890
|
limit: z.number().int().min(1).max(50).default(10)
|
|
5553
3891
|
});
|
|
5554
3892
|
const parsed = schema.parse(args);
|
|
5555
|
-
const results = searchCatalog(catalog, parsed.query, parsed.limit);
|
|
3893
|
+
const results = searchCatalog(catalog, parsed.query, parsed.limit, embeddings);
|
|
5556
3894
|
return toolSuccess(results);
|
|
5557
3895
|
}
|
|
5558
3896
|
},
|
|
@@ -5579,6 +3917,8 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5579
3917
|
"- crawlio.postEnrichment(url, data) \u2014 shortcut for POST /enrichment/bundle",
|
|
5580
3918
|
"- sleep(ms) \u2014 async wait (max 30s)",
|
|
5581
3919
|
"- TIMEOUTS \u2014 per-command timeout constants",
|
|
3920
|
+
"- compileRecording(session, { name, description? }) \u2014 compile RecordingSession to SKILL.md",
|
|
3921
|
+
" Returns { skillMarkdown, name, pageCount, interactionCount }",
|
|
5582
3922
|
"- smart \u2014 auto-waiting wrappers and framework-specific data accessors:",
|
|
5583
3923
|
" smart.evaluate(expr) \u2014 raw JS evaluation via CDP",
|
|
5584
3924
|
" smart.click(selector, opts?) \u2014 poll + click + 500ms settle",
|
|
@@ -5616,12 +3956,19 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5616
3956
|
"",
|
|
5617
3957
|
"Example (smart \u2014 framework data):",
|
|
5618
3958
|
" const nextData = await smart.nextjs?.getData();",
|
|
5619
|
-
" return { page: nextData?.page, buildId: nextData?.buildId };"
|
|
3959
|
+
" return { page: nextData?.page, buildId: nextData?.buildId };",
|
|
3960
|
+
"",
|
|
3961
|
+
"Example (session recording + compile):",
|
|
3962
|
+
" const s = await bridge.send({ type: 'start_recording', maxDurationSec: 120 });",
|
|
3963
|
+
" // ... interact with page ...",
|
|
3964
|
+
" const session = await bridge.send({ type: 'stop_recording' });",
|
|
3965
|
+
" const skill = compileRecording(session, { name: 'my-flow' });",
|
|
3966
|
+
" return skill;"
|
|
5620
3967
|
].join("\n"),
|
|
5621
3968
|
inputSchema: {
|
|
5622
3969
|
type: "object",
|
|
5623
3970
|
properties: {
|
|
5624
|
-
code: { type: "string", description: "Async JavaScript function body. Has bridge, crawlio, sleep, TIMEOUTS, smart in scope. Must return a value." }
|
|
3971
|
+
code: { type: "string", description: "Async JavaScript function body. Has bridge, crawlio, sleep, TIMEOUTS, smart, compileRecording in scope. Must return a value." }
|
|
5625
3972
|
},
|
|
5626
3973
|
required: ["code"]
|
|
5627
3974
|
},
|
|
@@ -5645,7 +3992,7 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5645
3992
|
}
|
|
5646
3993
|
let fn;
|
|
5647
3994
|
try {
|
|
5648
|
-
fn = new AsyncFunction("bridge", "crawlio", "sleep", "TIMEOUTS", "smart", code);
|
|
3995
|
+
fn = new AsyncFunction("bridge", "crawlio", "sleep", "TIMEOUTS", "smart", "compileRecording", code);
|
|
5649
3996
|
} catch (syntaxError) {
|
|
5650
3997
|
return toolError(`Syntax error in code: ${syntaxError instanceof Error ? syntaxError.message : String(syntaxError)}`);
|
|
5651
3998
|
}
|
|
@@ -5653,7 +4000,7 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5653
4000
|
const timer = setTimeout(() => controller.abort(), 12e4);
|
|
5654
4001
|
try {
|
|
5655
4002
|
const result = await Promise.race([
|
|
5656
|
-
fn(bridge2, crawlio2, sleepFn, TOOL_TIMEOUTS, smart),
|
|
4003
|
+
fn(bridge2, crawlio2, sleepFn, TOOL_TIMEOUTS, smart, compileRecording),
|
|
5657
4004
|
new Promise((_, reject) => {
|
|
5658
4005
|
controller.signal.addEventListener(
|
|
5659
4006
|
"abort",
|
|
@@ -5678,7 +4025,7 @@ function createCodeModeTools(bridge2, crawlio2) {
|
|
|
5678
4025
|
// src/mcp-server/index.ts
|
|
5679
4026
|
var initMode = process.argv.includes("init") || process.argv.includes("--setup") || process.argv.includes("setup");
|
|
5680
4027
|
if (initMode) {
|
|
5681
|
-
const { runInit } = await import("./init-
|
|
4028
|
+
const { runInit } = await import("./init-4ITWQXXA.js");
|
|
5682
4029
|
await runInit(process.argv.slice(2));
|
|
5683
4030
|
process.exit(0);
|
|
5684
4031
|
}
|
|
@@ -5701,7 +4048,7 @@ if (!codeMode) {
|
|
|
5701
4048
|
}
|
|
5702
4049
|
function createMcpServer() {
|
|
5703
4050
|
const s = new Server(
|
|
5704
|
-
{ name: "crawlio-browser", version:
|
|
4051
|
+
{ name: "crawlio-browser", version: PKG_VERSION },
|
|
5705
4052
|
{ capabilities: { tools: {} } }
|
|
5706
4053
|
);
|
|
5707
4054
|
s.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -5712,7 +4059,7 @@ function createMcpServer() {
|
|
|
5712
4059
|
}))
|
|
5713
4060
|
}));
|
|
5714
4061
|
s.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5715
|
-
const requestId =
|
|
4062
|
+
const requestId = randomBytes2(4).toString("hex");
|
|
5716
4063
|
const toolName = request.params.name;
|
|
5717
4064
|
const tool = tools.find((t) => t.name === toolName);
|
|
5718
4065
|
if (!tool) {
|
|
@@ -5767,7 +4114,8 @@ async function main() {
|
|
|
5767
4114
|
const port = await crawlio.getPort();
|
|
5768
4115
|
bridge.push({ type: "set_crawlio_port", port });
|
|
5769
4116
|
console.error(`[MCP] Port refresh: pushed Crawlio port ${port}`);
|
|
5770
|
-
} catch {
|
|
4117
|
+
} catch (e) {
|
|
4118
|
+
console.error("[MCP] Port refresh failed:", e);
|
|
5771
4119
|
}
|
|
5772
4120
|
};
|
|
5773
4121
|
if (portalMode) {
|
|
@@ -5808,7 +4156,7 @@ async function startPortalServer(port) {
|
|
|
5808
4156
|
}
|
|
5809
4157
|
next();
|
|
5810
4158
|
});
|
|
5811
|
-
app.use(
|
|
4159
|
+
app.use(rateLimit({
|
|
5812
4160
|
windowMs: 6e4,
|
|
5813
4161
|
max: 120,
|
|
5814
4162
|
standardHeaders: true,
|
|
@@ -5850,7 +4198,7 @@ async function startPortalServer(port) {
|
|
|
5850
4198
|
app.get("/health", (_req, res) => {
|
|
5851
4199
|
res.json({
|
|
5852
4200
|
status: "ok",
|
|
5853
|
-
version:
|
|
4201
|
+
version: PKG_VERSION,
|
|
5854
4202
|
mode: codeMode ? "code" : "full",
|
|
5855
4203
|
transport: "portal",
|
|
5856
4204
|
bridgeConnected: bridge.isConnected,
|
|
@@ -5869,6 +4217,14 @@ main().catch((e) => {
|
|
|
5869
4217
|
console.error("[MCP] Fatal:", e);
|
|
5870
4218
|
process.exit(1);
|
|
5871
4219
|
});
|
|
4220
|
+
process.on("uncaughtException", (err) => {
|
|
4221
|
+
console.error("[MCP] CRASH \u2014 uncaughtException:", err.stack ?? err.message);
|
|
4222
|
+
process.exit(1);
|
|
4223
|
+
});
|
|
4224
|
+
process.on("unhandledRejection", (reason) => {
|
|
4225
|
+
console.error("[MCP] CRASH \u2014 unhandledRejection:", reason instanceof Error ? reason.stack ?? reason.message : reason);
|
|
4226
|
+
process.exit(1);
|
|
4227
|
+
});
|
|
5872
4228
|
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
5873
4229
|
process.on(sig, async () => {
|
|
5874
4230
|
console.error(`[MCP] ${sig} received, shutting down`);
|