fix: source per-user creds and region from BrowserStackConfig (remote-MCP safety)#295
Merged
gaurav-singh-9227 merged 2 commits intoMay 15, 2026
Conversation
….env Two runtime paths were reading BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY directly from process.env. In remote (multi-tenant) mode the per-user creds flow through the BrowserStackConfig object passed to each tool, not env vars, so these reads silently return undefined (401) or — worse — return a stale value left over from container startup, leaking another user's credentials. Changes: 1. triggerEspressoBuild (appautomate-utils/native-execution/appautomate.ts) Now takes a BrowserStackConfig parameter and derives auth via getBrowserStackAuth(config). Matches the existing pattern in the sibling triggerXcuiBuild. Caller in src/tools/appautomate.ts updated to thread config through. Throws a clear error when config is missing creds rather than silently base64-encoding "undefined:undefined". 2. getMavenCommandForWindows (sdk-utils/bstack/commands.ts) Now takes username/accessKey parameters, matching the sibling getMavenCommandForUnix. Previously the Windows branch read process.env while the Unix branch correctly took params. In remote mode the Windows variant emitted "undefined" into the Maven command shown to the user. Tests: - tests/tools/triggerEspressoBuild.test.ts — verifies creds come from config, not process.env (sets process.env to a sentinel and asserts the Authorization header decodes to the config values). Also covers the missing-creds error path. - tests/tools/sdk-utils-commands.test.ts — verifies getSDKPrefixCommand (nodejs / java unix / java windows) renders the passed username/accessKey and never the literal "undefined" or "process.env". Out of scope (separate follow-ups): - Module-level user-data globals in src/lib/inmemory-store.ts, src/tools/sdk-utils/percy-web/fetchPercyToken.ts, src/lib/tm-base-url.ts. All consumer tools are already on the remote wrapper's INITIALLY_DISABLED_TOOLS list; refactoring to request-scoped state is a larger change. - Code-template emitters under src/tools/*/appium-sdk/languages/*.ts and the WebdriverIO config snippets in sdk-utils/bstack/constants.ts and sdk-utils/percy-automate/constants.ts contain the literal text "process.env.BROWSERSTACK_USERNAME" — these are emitted as code for the user to paste into their own integration; they are not runtime reads and are intentionally left as-is. The prettier reformat on update-testcase.ts is an unrelated drive-by cleanup applied by `npm run format` (part of the build gate).
… leak The module-level `cachedBaseUrl` is process-shared. In remote (multi-tenant) mode, the first user to call `getTMBaseURL` populates it with their region's URL, and every subsequent user — including users on a different region's BrowserStack account — receives that same URL. Their requests then hit the wrong region's Test Management backend. Guard both the read and write of `cachedBaseUrl` behind `!appConfig.REMOTE_MCP`. In stdio (single-tenant) mode behavior is unchanged: discover once, cache, reuse. In remote mode the cache stays empty and each caller re-discovers their own region. Per-credentials caching (instead of skipping the cache entirely) would be faster but requires a larger change; the immediate priority is correctness. Adds tests/lib/tm-base-url.test.ts with two regression tests: one verifying the stdio mode still caches, one verifying remote mode re-discovers across calls with different region responses.
gaurav-singh-9227
approved these changes
May 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three correctness bugs surfaced when this package runs as a library inside the remote MCP wrapper (multi-tenant Node process). Each one either silently fails or risks cross-user state leakage in that deployment; all are no-ops in local stdio mode.
triggerEspressoBuildread creds fromprocess.envat request time → silent 401 or wrong user's creds in remote mode.getMavenCommandForWindowsread creds fromprocess.envand emitted them into a Maven command shown to the user → user sees the literal stringundefined(or a stale value) in remote mode.getTMBaseURL's module-levelcachedBaseUrlis process-shared → in a multi-region remote deployment, the first user's region URL is served to every subsequent user.Why this matters
@browserstack/mcp-serveris consumed as a library by the remote MCP wrapper, a long-lived multi-tenant HTTP service. A single Node process serves many concurrent users. The wrapper builds a per-requestBrowserStackConfigfrom each user's OAuth-decoded session and passes it tonew BrowserStackMcpServer(config)— it does not mutateprocess.env.BROWSERSTACK_USERNAMEper request, and the module-level cache cannot be made user-aware without code changes here.Sibling code paths already do the right thing (
triggerXcuiBuildacceptsconfig;getMavenCommandForUnixacceptsusername/accessKeyparams;device-cache.tsalready gates writes onconfig.REMOTE_MCP). This PR brings the inconsistent siblings into line.Changes
1.
triggerEspressoBuild(appautomate-utils/native-execution/appautomate.ts)config: BrowserStackConfigand derives auth viagetBrowserStackAuth(config). MatchestriggerXcuiBuild.src/tools/appautomate.tsthreadsconfigthrough (already in scope from the tool registration).undefined:undefined.2.
getMavenCommandForWindows(sdk-utils/bstack/commands.ts)username/accessKeyparameters, matchinggetMavenCommandForUnix.process.envwhile the Unix branch took params — an inconsistency that made the Windows variant emit literalundefinedinto the Maven command shown to the user in remote mode.3.
getTMBaseURL(src/lib/tm-base-url.ts)appConfigfromsrc/config.jsand guards both the read and write ofcachedBaseUrlon!appConfig.REMOTE_MCP.device-cache.ts(shouldSendStartedEventalready checksconfig.REMOTE_MCP).Tests
tests/tools/triggerEspressoBuild.test.ts(new) — setsprocess.env.BROWSERSTACK_USERNAMEto a sentinel, callstriggerEspressoBuildwith a different value inconfig, decodes theAuthorizationheader, and asserts the config value wins. Covers the missing-creds error path.tests/tools/sdk-utils-commands.test.ts(new) — verifiesgetSDKPrefixCommand(nodejs / java-unix / java-windows) renders the passed username/accessKey and never the literalundefinedorprocess.env. The Windows case is a direct regression test.tests/lib/tm-base-url.test.ts(new) — two regression tests: stdio mode still caches across calls; remote mode re-discovers each call (verified via call-count assertions on the mocked HTTP client across simulated two users in different regions).npm run build(lint + format + test + tsc) is clean. 19 test files / 133 tests pass.What's NOT in this PR (intentional)
Module-level user-data globals in
src/lib/inmemory-store.ts(signedUrlMap,_storedPercyResults) andsrc/tools/sdk-utils/percy-web/fetchPercyToken.ts(globalPercyToken,globalProjectName). All consumer tools are already on the remote wrapper'sINITIALLY_DISABLED_TOOLSlist, so they're contained today. Refactoring to request-scoped state is a larger separate change.Code-template emitters that contain the literal text
process.env.BROWSERSTACK_USERNAME:src/tools/appautomate-utils/appium-sdk/languages/{ruby,nodejs}.tssrc/tools/sdk-utils/bstack/constants.ts(inside a```jsblock)src/tools/sdk-utils/percy-automate/constants.ts(inside a```jsblock, with|| 'YOUR_USERNAME'fallback)These emit code for the user to paste into their own integration. They're producing output, not reading env at runtime. Left as-is.
Drive-by
The single-line diff on
src/tools/testmanagement-utils/update-testcase.tsis an unrelated prettier reformat applied bynpm run format(part of the build gate). Included rather than reverted so future build runs don't dirty the tree.Test plan
npm run buildpasses locally (lint + format + test + tsc)grep -rn "process.env.BROWSERSTACK_USERNAME" src/should now only return entries inappium-sdk/languages/*.ts,sdk-utils/bstack/constants.ts,sdk-utils/percy-automate/constants.ts(all inside code-block templates), andsrc/index.ts(bootstrap)🤖 Generated with Claude Code