• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microlinkhq / browserless / 38539476882

14 Oct 2026 09:02AM UTC coverage: 77.425% (+0.1%) from 77.306%
38539476882

push

github

web-flow
fix(function): pass the browser socket as an isolate egress exception (#994)

* fix(function): pass the browser socket as an isolate egress exception

The isolate refuses reserved addresses only when it is given an egress list.
The browser websocket is on loopback, so the run grants that exact ip:port
and nothing else.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(function): keep an explicit default browser port in the egress list

new URL() drops port 80 on ws and 443 on wss, so those sockets were not granted. Read the port from the raw authority, and still skip an endpoint that names no port.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(function): grant the browser socket inside the caller's dial policy

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(function): match the browser grant to the port that is dialed

A default port written as 080 was granted as the raw token, so a dial on port 80 missed the exception. Canonicalize that number.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

2138 of 2423 branches covered (88.24%)

Branch coverage included in aggregate %.

45 of 47 new or added lines in 1 file covered. (95.74%)

7736 of 10330 relevant lines covered (74.89%)

58183.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.82
/packages/function/src/function.js
1
'use strict'
14 ✔
2

14 ✔
3
const { SLOT } = require('isolated-function')
14 ✔
4
const net = require('node:net')
14 ✔
5

14 ✔
6
const template = require('./template')
14 ✔
7

14 ✔
8
// `new URL()` drops a scheme's default port, so `ws://127.0.0.1:80` has an
14 ✔
9
// empty `url.port`. The port is still in the raw authority. An endpoint that
14 ✔
10
// names no port stays ungranted.
14 ✔
11
const portOf = (endpoint, url) => {
14 ✔
12
  if (url.port !== '') return url.port
43 ✔
13
  const scheme = endpoint.indexOf('://')
11 ✔
14
  if (scheme === -1) return ''
16 !
15
  const authority = endpoint.slice(scheme + 3).split(/[/?#]/, 1)[0]
11 ✔
16
  const colon = authority.lastIndexOf(':')
11 ✔
17
  if (colon === -1) return ''
16 ✔
18
  const port = authority.slice(colon + 1)
5 ✔
19
  // `080` is port 80. The dial uses the number, so the grant has to as well.
5 ✔
20
  return /^\d+$/.test(port) ? String(Number(port)) : ''
43 ✔
21
}
43 ✔
22

14 ✔
23
const pair = (address, port) => {
14 ✔
24
  const value = String(port)
20 ✔
25
  return net.isIP(address) === 6 ? `[${address}]:${value}` : `${address}:${value}`
20 ✔
26
}
20 ✔
27

14 ✔
28
const egressOf = endpoint => {
14 ✔
29
  if (typeof endpoint !== 'string') return []
75 ✔
30
  let url
43 ✔
31
  try {
43 ✔
32
    url = new URL(endpoint)
43 ✔
33
  } catch {
56 !
NEW
34
    return []
×
NEW
35
  }
×
36
  const port = portOf(endpoint, url)
43 ✔
37
  if (port === '') return []
56 ✔
38
  const host =
36 ✔
39
    url.hostname.startsWith('[') && url.hostname.endsWith(']')
51 ✔
40
      ? url.hostname.slice(1, -1)
75 ✔
41
      : url.hostname
75 ✔
42
  if (net.isIP(host) === 0) return []
75 ✔
43
  return [net.isIP(host) === 6 ? `[${host}]:${port}` : `${host}:${port}`]
75 ✔
44
}
75 ✔
45

14 ✔
46
const [nodeMajor] = process.version.slice(1).split('.').map(Number)
14 ✔
47

14 ✔
48
/**
14 ✔
49
 * The template is built around `SLOT` rather than the user code, so every
14 ✔
50
 * function with the same page shape shares one bundled program and only the
14 ✔
51
 * slot is filled per call, instead of re-bundling the whole program.
14 ✔
52
 */
14 ✔
53
module.exports =
14 ✔
54
  isolatedFunction =>
14 ✔
55
    async ({
26 ✔
56
      url,
75 ✔
57
      code,
75 ✔
58
      vmOpts,
75 ✔
59
      browserWSEndpoint,
75 ✔
60
      extendPage,
75 ✔
61
      hostPage,
75 ✔
62
      needsNetwork = template.needsBrowser(code, { ...extendPage, ...hostPage }),
75 ✔
63
      source = template(SLOT, {
75 ✔
64
        usesPage: template.isUsingPage(code),
75 ✔
65
        needsBrowser: needsNetwork,
75 ✔
66
        extendPage,
75 ✔
67
        hostPage
75 ✔
68
      }),
75 ✔
69
      ...opts
75 ✔
70
    }) => {
75 ✔
71
      const permissions = needsNetwork && nodeMajor >= 25 ? ['net'] : []
75 !
72
      const { ssrf: policy, allow: vmOptsAllow = {}, ...vmOptsRest } = vmOpts ?? {}
75 ✔
73
      const granted = new Set(egressOf(browserWSEndpoint))
75 ✔
74
      const ssrf =
75 ✔
75
      typeof policy === 'function'
75 ✔
76
        ? (address, port) => granted.has(pair(address, port)) || policy(address, port) === true
75 ✔
77
        : undefined
75 ✔
78
      // `isolated-function` rejects `slot` unless the sentinel appears once.
75 ✔
79
      // A second copy means an extendPage method mentioned it; inline the user
75 ✔
80
      // code and take a full build so that call still runs.
75 ✔
81
      const copies = source.split(SLOT).length - 1
75 ✔
82
      const program =
75 ✔
83
      copies > 1
75 ✔
84
        ? template(code, {
75 ✔
85
          usesPage: template.isUsingPage(code),
1 ✔
86
          needsBrowser: needsNetwork,
1 ✔
87
          extendPage,
1 ✔
88
          hostPage
1 ✔
89
        })
1 ✔
90
        : source
75 ✔
91
      const host = template.exposedHost(hostPage, extendPage)
75 ✔
92
      const fn = isolatedFunction(program, {
75 ✔
93
        ...vmOptsRest,
75 ✔
94
        ...(ssrf && { ssrf }),
75 ✔
95
        ...(copies === 1 && { slot: code }),
75 ✔
96
        ...(host && { host }),
75 ✔
97
        allow: {
75 ✔
98
          ...vmOptsAllow,
75 ✔
99
          permissions: [...(vmOptsAllow.permissions || []), ...permissions]
75 ✔
100
        },
75 ✔
101
        throwError: false
75 ✔
102
      })
75 ✔
103
      return fn(url, browserWSEndpoint, opts)
75 ✔
104
    }
75 ✔
105

14 ✔
106
module.exports.isUsingPage = template.isUsingPage
14 ✔
107
module.exports.needsBrowser = template.needsBrowser
14 ✔
108
module.exports.buildTemplate = template
14 ✔
109
module.exports.inspect = template.inspect
14 ✔
110
module.exports.PAGE_NOT_FOUND = template.PAGE_NOT_FOUND
14 ✔
111
module.exports.RESOLVE_PAGE = template.RESOLVE_PAGE
14 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc