@open-mercato/web-research-browser 0.10.1-develop.8050.1.45ae874fe7 → 0.10.1-develop.8053.1.97a8e36d1b

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/web-research-browser",
3
- "version": "0.10.1-develop.8050.1.45ae874fe7",
3
+ "version": "0.10.1-develop.8053.1.97a8e36d1b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,12 +38,12 @@
38
38
  "@open-mercato/web-research": "^0.6.0"
39
39
  },
40
40
  "dependencies": {
41
- "@open-mercato/web-research-serp": "0.10.1-develop.8050.1.45ae874fe7",
41
+ "@open-mercato/web-research-serp": "0.10.1-develop.8053.1.97a8e36d1b",
42
42
  "playwright": "^1.40.0",
43
43
  "zod": "^4.6.6"
44
44
  },
45
45
  "devDependencies": {
46
- "@open-mercato/web-research": "0.10.1-develop.8050.1.45ae874fe7",
46
+ "@open-mercato/web-research": "0.10.1-develop.8053.1.97a8e36d1b",
47
47
  "@types/jest": "^30.0.0",
48
48
  "@types/node": "^26.0.1",
49
49
  "esbuild": "^0.28.1",
@@ -16,6 +16,7 @@ type Harness = {
16
16
  proxy: EgressProxy
17
17
  proxyPort: number
18
18
  dials: EgressDialTarget[]
19
+ upstreamSockets: Socket[]
19
20
  privateConnections: () => number
20
21
  publicRequests: Recorded[]
21
22
  publicSockets: Set<Socket>
@@ -31,6 +32,13 @@ async function listen(server: Server | ReturnType<typeof createTcpServer>): Prom
31
32
  return (server.address() as AddressInfo).port
32
33
  }
33
34
 
35
+ async function waitUntil(condition: () => boolean, timeoutMs = 5_000): Promise<void> {
36
+ const deadline = Date.now() + timeoutMs
37
+ while (!condition() && Date.now() < deadline) {
38
+ await new Promise<void>((resolve) => setTimeout(resolve, 1))
39
+ }
40
+ }
41
+
34
42
  function closeAll(): Promise<void[]> {
35
43
  const closing = [...servers, ...tcpServers].map(
36
44
  (server) =>
@@ -121,6 +129,7 @@ async function buildHarness(): Promise<Harness> {
121
129
  }
122
130
 
123
131
  const dials: EgressDialTarget[] = []
132
+ const upstreamSockets: Socket[] = []
124
133
  const portFor = (addresses: readonly string[]): number => {
125
134
  if (addresses.length === 1 && [PUBLIC_IP, PUBLIC_IPV6, PUBLIC_6TO4].includes(addresses[0])) return publicPort
126
135
  if (addresses.length === 1 && addresses[0] === RESET_IP) return resetPort
@@ -132,13 +141,16 @@ async function buildHarness(): Promise<Harness> {
132
141
  lookup,
133
142
  connect: (target) => {
134
143
  dials.push(target)
135
- return netConnect({ host: '127.0.0.1', port: portFor(target.addresses) })
144
+ const upstream = netConnect({ host: '127.0.0.1', port: portFor(target.addresses) })
145
+ upstreamSockets.push(upstream)
146
+ return upstream
136
147
  },
137
148
  })
138
149
  return {
139
150
  proxy,
140
151
  proxyPort: Number(new URL(proxy.url).port),
141
152
  dials,
153
+ upstreamSockets,
142
154
  privateConnections: () => privateHits,
143
155
  publicRequests,
144
156
  publicSockets,
@@ -382,11 +394,14 @@ describe('browser egress proxy (#7218)', () => {
382
394
  expect(tunnel.status).toBe(200)
383
395
  tunnel.socket.on('error', () => undefined)
384
396
  const clientClosed = new Promise<void>((resolve) => tunnel.socket.once('close', () => resolve()))
397
+ await waitUntil(() => harness.publicSockets.size === 1)
398
+ expect(harness.publicSockets.size).toBe(1)
385
399
 
386
400
  await harness.proxy.close()
401
+ expect(harness.upstreamSockets.map((socket) => socket.destroyed)).toEqual([true])
387
402
  await clientClosed
388
403
 
389
- await new Promise<void>((resolve) => setImmediate(resolve))
404
+ await waitUntil(() => harness.publicSockets.size === 0)
390
405
  expect(harness.publicSockets.size).toBe(0)
391
406
  await expect(
392
407
  new Promise((resolve, reject) => {
@@ -401,6 +416,59 @@ describe('browser egress proxy (#7218)', () => {
401
416
  })
402
417
  })
403
418
 
419
+ const REQUEST_FORMS = [
420
+ ['CONNECT', 'CONNECT vetting.test:443 HTTP/1.1\r\nHost: vetting.test:443\r\n\r\n'],
421
+ ['absolute-form', 'GET http://vetting.test/ HTTP/1.1\r\nHost: vetting.test\r\n\r\n'],
422
+ ]
423
+
424
+ const CLIENT_DEPARTURES: Array<[string, (client: Socket) => void]> = [
425
+ ['disconnect', (client) => client.destroy()],
426
+ ['reset', (client) => client.resetAndDestroy()],
427
+ ]
428
+
429
+ type LookupRecords = Awaited<ReturnType<LookupFn>>
430
+
431
+ type DeferredLookupProxy = {
432
+ proxy: EgressProxy
433
+ dials: EgressDialTarget[]
434
+ started: Promise<void>
435
+ answer: (records: LookupRecords) => void
436
+ }
437
+
438
+ function proxyRoundTrip(proxy: EgressProxy): Promise<void> {
439
+ return new Promise((resolve, reject) => {
440
+ const barrier = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
441
+ barrier.once('error', reject)
442
+ barrier.once('close', () => reject(new Error('[internal] barrier closed without a response')))
443
+ barrier.once('data', () => {
444
+ barrier.destroy()
445
+ resolve()
446
+ })
447
+ barrier.write('GET /barrier HTTP/1.1\r\nHost: barrier\r\nConnection: close\r\n\r\n')
448
+ })
449
+ }
450
+
451
+ async function startProxyWithDeferredLookup(): Promise<DeferredLookupProxy> {
452
+ const dials: EgressDialTarget[] = []
453
+ let resolveLookup: (records: LookupRecords) => void = () => undefined
454
+ let lookupStarted: () => void = () => undefined
455
+ const started = new Promise<void>((resolve) => {
456
+ lookupStarted = resolve
457
+ })
458
+ const proxy = await startEgressProxy({
459
+ lookup: () =>
460
+ new Promise((resolve) => {
461
+ resolveLookup = resolve
462
+ lookupStarted()
463
+ }),
464
+ connect: (dialTarget) => {
465
+ dials.push(dialTarget)
466
+ return netConnect({ host: '127.0.0.1', port: 9 })
467
+ },
468
+ })
469
+ return { proxy, dials, started, answer: (records) => resolveLookup(records) }
470
+ }
471
+
404
472
  describe('pinned dial and vetting limits (#7218)', () => {
405
473
  it('connects the default dial to the vetted address without resolving the hostname', async () => {
406
474
  const target = createTcpServer((socket) => socket.end())
@@ -457,38 +525,44 @@ describe('pinned dial and vetting limits (#7218)', () => {
457
525
  }
458
526
  })
459
527
 
460
- it('does not dial for a client that left while its destination was being vetted', async () => {
461
- const dials: EgressDialTarget[] = []
462
- let answer: (records: Array<{ address: string; family: number }>) => void = () => undefined
463
- let lookupStarted: () => void = () => undefined
464
- const started = new Promise<void>((resolve) => {
465
- lookupStarted = resolve
466
- })
467
- const proxy = await startEgressProxy({
468
- lookup: () =>
469
- new Promise((resolve) => {
470
- answer = resolve
471
- lookupStarted()
472
- }),
473
- connect: (dialTarget) => {
474
- dials.push(dialTarget)
475
- return netConnect({ host: '127.0.0.1', port: 9 })
476
- },
528
+ describe.each(CLIENT_DEPARTURES)('when the client leaves by %s', (_departure, leave) => {
529
+ it.each(REQUEST_FORMS)('does not dial for a client that left during vetting (%s)', async (_form, request) => {
530
+ const { proxy, dials, started, answer } = await startProxyWithDeferredLookup()
531
+ try {
532
+ const client = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
533
+ client.on('error', () => undefined)
534
+ client.write(request)
535
+ await started
536
+ const clientClosed = new Promise<void>((resolve) => client.once('close', () => resolve()))
537
+ leave(client)
538
+ await clientClosed
539
+ await proxyRoundTrip(proxy)
540
+ answer([{ address: PUBLIC_IP, family: 4 }])
541
+ await new Promise((resolve) => setTimeout(resolve, 20))
542
+
543
+ expect(dials).toEqual([])
544
+ } finally {
545
+ await proxy.close()
546
+ }
477
547
  })
548
+ })
549
+
550
+ it.each(REQUEST_FORMS)('does not dial a request still being vetted when the proxy closes (%s)', async (_form, request) => {
551
+ const { proxy, dials, started, answer } = await startProxyWithDeferredLookup()
552
+ const client = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
553
+ client.on('error', () => undefined)
478
554
  try {
479
- const client = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
480
- client.on('error', () => undefined)
481
- client.write('CONNECT leaving.test:443 HTTP/1.1\r\nHost: leaving.test:443\r\n\r\n')
555
+ client.write(request)
482
556
  await started
483
- const clientClosed = new Promise<void>((resolve) => client.once('close', () => resolve()))
484
- client.destroy()
485
- await clientClosed
486
- await new Promise((resolve) => setTimeout(resolve, 20))
557
+
558
+ const closing = proxy.close()
487
559
  answer([{ address: PUBLIC_IP, family: 4 }])
560
+ await closing
488
561
  await new Promise((resolve) => setTimeout(resolve, 20))
489
562
 
490
563
  expect(dials).toEqual([])
491
564
  } finally {
565
+ client.destroy()
492
566
  await proxy.close()
493
567
  }
494
568
  })