From 96f12d8428912f3ac81f4207c5b58343a537562c Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Tue, 24 Mar 2026 15:20:56 -0700 Subject: [PATCH 1/2] extract tunnel domain from base url --- src/sdk/devbox.ts | 4 +++- tests/objects/devbox.test.ts | 18 ++++++++++++++++++ .../smoketests/object-oriented/devbox.test.ts | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index df25963c9..a338b423e 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -838,7 +838,9 @@ export class Devbox { if (!tunnel) { throw new RunloopError('No tunnel has been enabled for this devbox. Call net.enableTunnel() first.'); } - return `https://${port}-${tunnel.tunnel_key}.tunnel.runloop.ai`; + const apiHost = new URL(this.client.baseURL).hostname; + const baseDomain = apiHost.split('.').slice(-2).join('.'); + return `https://${port}-${tunnel.tunnel_key}.tunnel.${baseDomain}`; } /** diff --git a/tests/objects/devbox.test.ts b/tests/objects/devbox.test.ts index 4a9fadfa9..94a9d6739 100644 --- a/tests/objects/devbox.test.ts +++ b/tests/objects/devbox.test.ts @@ -11,6 +11,7 @@ describe('Devbox (New API)', () => { beforeEach(() => { // Create mock client instance with proper structure mockClient = { + baseURL: 'https://api.runloop.ai', devboxes: { createAndAwaitRunning: jest.fn(), retrieve: jest.fn(), @@ -303,6 +304,23 @@ describe('Devbox (New API)', () => { expect(url).toBe('https://3000-mykey456.tunnel.runloop.ai'); }); + it('should derive tunnel domain from client baseURL', async () => { + mockClient.baseURL = 'https://api.runloop.pro'; + const mockTunnel = { + tunnel_key: 'abc123xyz', + auth_mode: 'open' as const, + create_time_ms: Date.now(), + }; + const dataWithTunnel = { ...mockDevboxData, tunnel: mockTunnel }; + mockClient.devboxes.retrieve.mockResolvedValue(dataWithTunnel); + + const url = await devbox.getTunnelUrl(8080); + + expect(url).toBe('https://8080-abc123xyz.tunnel.runloop.pro'); + + mockClient.baseURL = 'https://api.runloop.ai'; + }); + it('should throw RunloopError when no tunnel has been enabled', async () => { const dataWithoutTunnel = { ...mockDevboxData, tunnel: null }; mockClient.devboxes.retrieve.mockResolvedValue(dataWithoutTunnel); diff --git a/tests/smoketests/object-oriented/devbox.test.ts b/tests/smoketests/object-oriented/devbox.test.ts index 603128f30..8d8598cbd 100644 --- a/tests/smoketests/object-oriented/devbox.test.ts +++ b/tests/smoketests/object-oriented/devbox.test.ts @@ -413,10 +413,10 @@ describe('smoketest: object-oriented devbox', () => { expect(tunnel.tunnel_key).toBeTruthy(); const url = await devbox.getTunnelUrl(8080); - expect(url).toBe(`https://8080-${tunnel.tunnel_key}.tunnel.runloop.ai`); + expect(url).toContain(`https://8080-${tunnel.tunnel_key}.tunnel.runloop.`); const url3000 = await devbox.getTunnelUrl(3000); - expect(url3000).toBe(`https://3000-${tunnel.tunnel_key}.tunnel.runloop.ai`); + expect(url3000).toContain(`https://3000-${tunnel.tunnel_key}.tunnel.runloop.`); } finally { await devbox.shutdown(); } From 775c494e8d28d3641271baa08837f52d6c7a8165 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Tue, 24 Mar 2026 15:33:49 -0700 Subject: [PATCH 2/2] address comments --- src/sdk/devbox.ts | 2 +- tests/objects/devbox.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index a338b423e..e34a938d3 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -839,7 +839,7 @@ export class Devbox { throw new RunloopError('No tunnel has been enabled for this devbox. Call net.enableTunnel() first.'); } const apiHost = new URL(this.client.baseURL).hostname; - const baseDomain = apiHost.split('.').slice(-2).join('.'); + const baseDomain = apiHost.startsWith('api.') ? apiHost.slice(4) : apiHost; return `https://${port}-${tunnel.tunnel_key}.tunnel.${baseDomain}`; } diff --git a/tests/objects/devbox.test.ts b/tests/objects/devbox.test.ts index 94a9d6739..2cb930c97 100644 --- a/tests/objects/devbox.test.ts +++ b/tests/objects/devbox.test.ts @@ -305,7 +305,6 @@ describe('Devbox (New API)', () => { }); it('should derive tunnel domain from client baseURL', async () => { - mockClient.baseURL = 'https://api.runloop.pro'; const mockTunnel = { tunnel_key: 'abc123xyz', auth_mode: 'open' as const, @@ -314,9 +313,11 @@ describe('Devbox (New API)', () => { const dataWithTunnel = { ...mockDevboxData, tunnel: mockTunnel }; mockClient.devboxes.retrieve.mockResolvedValue(dataWithTunnel); - const url = await devbox.getTunnelUrl(8080); + mockClient.baseURL = 'https://api.runloop.pro'; + expect(await devbox.getTunnelUrl(8080)).toBe('https://8080-abc123xyz.tunnel.runloop.pro'); - expect(url).toBe('https://8080-abc123xyz.tunnel.runloop.pro'); + mockClient.baseURL = 'http://127.0.0.1:8080'; + expect(await devbox.getTunnelUrl(8080)).toBe('https://8080-abc123xyz.tunnel.127.0.0.1'); mockClient.baseURL = 'https://api.runloop.ai'; });