Skip to content

feat(csharp): remove omitted basic auth fields from SDK API, use empty string internally#14409

Merged
Swimburger merged 34 commits intomainfrom
devin/1774997734-basic-auth-optional-csharp-sdk
Apr 7, 2026
Merged

feat(csharp): remove omitted basic auth fields from SDK API, use empty string internally#14409
Swimburger merged 34 commits intomainfrom
devin/1774997734-basic-auth-optional-csharp-sdk

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Mar 31, 2026

Description

Refs #14378

When usernameOmit or passwordOmit flags are set in the IR's BasicAuthScheme, the generated C# SDK now completely removes the flagged field from the constructor and client options — it does not appear in the end-user API at all. Internally, the omitted field is treated as an empty string when encoding the Authorization: Basic header (e.g. password: omit: true → header encodes username:). Default behavior (both required) is preserved when no omit flags are set.

Split from #14378 (one PR per generator).

Changes Made

  • RootClientGenerator.ts:
    • Reads basicScheme.usernameOmit / basicScheme.passwordOmit directly (no cast needed — @fern-fern/ir-sdk v66 has typed fields)
    • Constructor parameter removal: omitted fields are excluded entirely from the params array (not just made optional). Only non-omitted fields appear in the generated constructor.
    • Auth header encoding: omitted fields produce clean empty strings in the interpolation (e.g. $"{username}:" when password is omitted) — no {""} wrapper or null-coalescing fallback.
    • Per-field condition logic: only non-omitted fields contribute to the null-check guard (e.g. when only passwordOmit is set, condition is username != null)
    • if/else if control flow fix: replaced i === 0 index check with isFirstBlock tracking variable, so that when a both-omitted scheme is skipped via continue, the next scheme still correctly emits if instead of else if
    • Both-fields-omitted case: when both usernameOmit and passwordOmit are true, the scheme is skipped entirely (continue) — no meaningless Authorization: Basic Og== header is sent
  • EndpointSnippetGenerator.ts (dynamic snippets): Omitted fields are excluded from generated snippet constructor args. Uses as unknown as Record<string, unknown> cast (necessary — @fern-api/dynamic-ir-sdk lacks typed usernameOmit/passwordOmit fields)
  • DynamicSnippetsConverter.ts: Passes usernameOmit/passwordOmit through to dynamic IR so downstream snippet generators can read the flags
  • versions.yml: new 2.58.0 entry (irVersion: 66)
  • snippet.json: Updated to reflect password omission — constructor calls now use new SeedBasicAuthPwOmittedClient("USERNAME") (single arg)
  • New basic-auth-pw-omitted test fixture with password: omit: true, plus seed output at seed/csharp-sdk/basic-auth-pw-omitted/

Updates since last revision

  • Seed output fully regenerated: SeedBasicAuthPwOmittedClient.cs now correctly removes the password parameter from the constructor and encodes the auth header as $"{username}:" (clean interpolation, no {""} wrapper).
  • {""} pattern removed: Auth header encoding changed from $"{username}:{""}" to $"{username}:" — both in the generator logic and the seed output.
  • Biome formatting fix: Removed trailing whitespace on the writeTextStatement line.
  • Merge conflict resolved: Incorporated new 2.56.6 entry from main into versions.yml.
  • Dynamic snippets updated: All Example0.csExample6.cs now exclude the password named argument. BaseMockServerTest.cs instantiates with only "USERNAME".

Testing

  • Seed snapshot generated for basic-auth-pw-omitted fixture
  • Existing seed fixtures unchanged (no regressions)
  • Lint passes (biome check)
  • All 20 seed-test-results pass
  • test-ete failure is pre-existing flaky timeout (upgrade-generator.test.ts 180s) — unrelated to this PR

⚠️ Human Review Checklist

  1. isFirstBlock control flow (lines ~456–490 of RootClientGenerator.ts): Confirm that continue (both-omitted case) does not set isFirstBlock = false, so the next non-skipped scheme correctly gets if.
  2. Auth header interpolation correctness: Verify that $"${usernamePart}:${passwordPart}" in the TypeScript template produces valid C# like $"{username}:" (not $":" or other malformed strings) when one field is omitted. The TS template uses ${} for JS interpolation, while the output $"" is C# interpolation — double-check there's no escaping issue.
  3. Dynamic snippets cast is intentional: EndpointSnippetGenerator.ts uses as unknown as Record<string, unknown> because @fern-api/dynamic-ir-sdk lacks typed omit fields. This is a known limitation — a follow-up to add these fields to the dynamic IR schema would make this fully robust (see Devin Review comment).
  4. DynamicSnippetsConverter.ts schema gap: usernameOmit/passwordOmit are passed through as extra properties on the BasicAuth object, but these fields are not yet in the dynamic IR YAML schema. This works for in-process / raw JSON paths (including seed tests) but would be stripped by schema-based deserialization. Deferring the IR schema update to a follow-up.

Link to Devin session: https://app.devin.ai/sessions/0786b963284f4799acb409d5373cde0a
Requested by: @Swimburger


Open with Devin

…en configured in IR

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

🤖 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 and CI monitoring

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

…y instead of coarse eitherOmitted flag

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Comment on lines +469 to +481
const usernameOmitted = basicScheme.usernameOmit === true;
const passwordOmitted = basicScheme.passwordOmit === true;
// Per-field condition: required fields must be present, omittable fields are always satisfied
let condition: string;
if (!usernameOmitted && !passwordOmitted) {
condition = `${usernameAccess} != null && ${passwordAccess} != null`;
} else if (usernameOmitted && passwordOmitted) {
condition = `${usernameAccess} != null || ${passwordAccess} != null`;
} else if (usernameOmitted) {
condition = `${passwordAccess} != null`;
} else {
condition = `${usernameAccess} != null`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Logic bug: When both username and password are optional (usernameOmit and passwordOmit are both true), but auth itself is required (isAuthOptional = false) and there's only one basic scheme, no condition check is generated. This causes the Authorization header to always be set with "" (empty credentials) even when both username and password are null, contradicting the changelog which states the header should be omitted when neither credential is provided.

Fix: The condition logic needs to account for the case where fields are optional even when isAuthOptional is false. When either field is omitted, a condition check should still be generated:

const eitherOmitted = usernameOmitted || passwordOmitted;
if (isAuthOptional || basicSchemes.length > 1 || eitherOmitted) {
    const controlFlowKeyword = i === 0 ? "if" : "else if";
    innerWriter.controlFlow(controlFlowKeyword, this.csharp.codeblock(condition));
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 1, 2026 16:15
… per-field omit fix

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

This comment was marked as resolved.

Swimburger and others added 2 commits April 1, 2026 18:36
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ms, use empty string internally

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(csharp-sdk): support optional username/password in basic auth when configured in IR feat(csharp-sdk): remove omitted basic auth fields from SDK API, use empty string internally Apr 2, 2026
Swimburger and others added 5 commits April 2, 2026 01:16
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…(attempt 3)

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

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

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

This comment was marked as resolved.

…en first scheme skipped by continue

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Comment on lines +480 to +483
} else {
// Both fields omitted — skip auth header entirely when auth is optional
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When both username and password are omitted, the code skips adding any authorization header via continue. However, this occurs regardless of whether isAuthOptional is true or false. If auth is mandatory (isAuthOptional === false) but both fields are omitted, the generated client will instantiate successfully (with no auth parameters) but all authenticated requests will fail at runtime with 401 errors.

} else {
    // Both fields omitted
    if (!isAuthOptional) {
        // This is a config error - mandatory auth requires at least one field
        throw new Error("Cannot have mandatory auth with both username and password omitted");
    }
    continue;
}

Alternatively, this validation should occur at IR validation time before code generation.

Suggested change
} else {
// Both fields omitted — skip auth header entirely when auth is optional
continue;
}
} else {
// Both fields omitted
if (!isAuthOptional) {
// This is a config error - mandatory auth requires at least one field
throw new Error("Cannot have mandatory auth with both username and password omitted");
}
// Auth is optional — skip auth header entirely
continue;
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Swimburger and others added 7 commits April 2, 2026 20:35
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ngelog and code comment

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Swimburger and others added 8 commits April 3, 2026 15:37
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…th omit)

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

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

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

This comment was marked as resolved.

…to dynamic IR

The DynamicSnippetsConverter was constructing dynamic BasicAuth with only
username and password fields, dropping usernameOmit/passwordOmit from the
main IR's BasicAuthScheme. This caused dynamic snippets generators to
always include omitted auth fields (e.g. $password) since they couldn't
detect the omit flags in the dynamic IR data.

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

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +736 to +749
const basicAuth: DynamicSnippets.BasicAuth & {
usernameOmit?: boolean;
passwordOmit?: boolean;
} = {
username: this.inflateName(scheme.username),
password: this.inflateName(scheme.password)
});
};
if (scheme.usernameOmit) {
basicAuth.usernameOmit = scheme.usernameOmit;
}
if (scheme.passwordOmit) {
basicAuth.passwordOmit = scheme.passwordOmit;
}
return DynamicSnippets.Auth.basic(basicAuth);
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot Apr 3, 2026

Choose a reason for hiding this comment

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

🔴 Prohibited as unknown as X type assertion used instead of updating dynamic IR SDK types

Line 389 uses auth as unknown as Record<string, unknown> to access usernameOmit/passwordOmit fields that aren't declared on the FernIr.dynamic.BasicAuth type. This is explicitly prohibited by CLAUDE.md: "Never use as any or as unknown as X. These are escape hatches that bypass the type system entirely. If the types don't line up, fix the types." The root cause is that the @fern-api/dynamic-ir-sdk (v65.5.0, per generators/csharp/dynamic-snippets/package.json:39) hasn't been updated to include usernameOmit/passwordOmit in the BasicAuth interface. The proper fix is to update the dynamic IR SDK's BasicAuth type to include these optional fields, which would also make the corresponding DynamicSnippetsConverter.ts changes at lines 736-749 type-safe instead of relying on undeclared extra properties surviving JSON round-trips.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


public SeedBasicAuthPwOmittedClient(
string? username = null,
string? password = null,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why is there still a password field when it should be omitted?

devin-ai-integration bot and others added 3 commits April 6, 2026 21:40
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ve omitted password field

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

github-actions bot commented Apr 6, 2026

SDK Generation Benchmark Results

Comparing PR branch against main baseline.

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square N/A N/A 132s N/A
go-sdk square N/A N/A 298s N/A
java-sdk square N/A N/A 326s N/A
php-sdk square N/A N/A 127s N/A
python-sdk square N/A N/A 164s N/A
ruby-sdk-v2 square N/A N/A 153s N/A
rust-sdk square N/A N/A 128s N/A
swift-sdk square N/A N/A 157s N/A
ts-sdk square N/A N/A 142s N/A

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).

var clientOptionsWithAuth = clientOptions.Clone();
clientOptionsWithAuth.Headers["Authorization"] =
$"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{username}:{password}"))}";
$"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{username}:{""}"))}";
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

you can remvoe {""}

…string interpolation

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

github-actions bot commented Apr 6, 2026

SDK Generation Benchmark Results

Comparing PR branch against main baseline.

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square N/A N/A 131s N/A
go-sdk square N/A N/A 296s N/A
java-sdk square N/A N/A 330s N/A
php-sdk square N/A N/A 124s N/A
python-sdk square N/A N/A 162s N/A
ruby-sdk-v2 square N/A N/A 156s N/A
rust-sdk square N/A N/A 131s N/A
swift-sdk square N/A N/A 140s N/A
ts-sdk square N/A N/A 142s N/A

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).

Swimburger and others added 2 commits April 6, 2026 22:43
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +736 to +749
const basicAuth: DynamicSnippets.BasicAuth & {
usernameOmit?: boolean;
passwordOmit?: boolean;
} = {
username: this.inflateName(scheme.username),
password: this.inflateName(scheme.password)
});
};
if (scheme.usernameOmit) {
basicAuth.usernameOmit = scheme.usernameOmit;
}
if (scheme.passwordOmit) {
basicAuth.passwordOmit = scheme.passwordOmit;
}
return DynamicSnippets.Auth.basic(basicAuth);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 usernameOmit/passwordOmit not added to dynamic IR BasicAuth schema, causing data loss through serialization

The DynamicSnippetsConverter.ts adds usernameOmit and passwordOmit as extra properties to the dynamic BasicAuth object, but these fields are NOT defined in the dynamic IR schema (packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml:22-25). The serialization schema at packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts:9-12 uses objectWithoutOptionalProperties with only username and password, meaning these fields are stripped during any schema-based deserialization. The EndpointSnippetGenerator.ts:389 then casts to Record<string, unknown> to read them, but those fields will be undefined if the dynamic IR went through Fern's schema deserialization. This creates a fragile contract where the feature works only when the IR is passed in-process or via raw JSON.parse (as in the test utility at generators/csharp/dynamic-snippets/src/__test__/utils/buildDynamicSnippetsGenerator.ts:15), but would silently break if any code path uses schema-based parsing.

Prompt for agents
The dynamic IR BasicAuth type needs usernameOmit and passwordOmit fields added to its schema so these values survive serialization/deserialization. Update the following files:

1. packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml - Add usernameOmit (optional boolean) and passwordOmit (optional boolean) to the BasicAuth type properties.

2. After updating the YAML schema, regenerate the SDK types by running the appropriate IR generation command (pnpm ir:generate). This will update:
   - packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts
   - packages/ir-sdk/src/sdk/serialization/resources/dynamic/resources/auth/types/BasicAuth.ts

3. Once the types are updated, the as unknown as Record<string, unknown> cast in generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts can be replaced with direct property access on the properly typed BasicAuth object.

4. The DynamicSnippetsConverter.ts intersection type workaround (DynamicSnippets.BasicAuth & { usernameOmit?: boolean; passwordOmit?: boolean }) can then be simplified to just use DynamicSnippets.BasicAuth directly.

Also consider updating the v65 dynamic auth schema if backward compatibility with that IR version is needed.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a known limitation — usernameOmit/passwordOmit are not yet part of the dynamic IR BasicAuth schema. Adding them requires updating the IR schema YAML, regenerating SDK types, and potentially updating the v65→v62 migration. This was intentionally deferred as an IR-level change (per earlier discussion with @niels). The current implementation works in-process and via raw JSON, which covers the seed test path. A follow-up PR to add these fields to the dynamic IR schema would make this fully robust.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

SDK Generation Benchmark Results

Comparing PR branch against main baseline.

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square N/A N/A 134s N/A
go-sdk square N/A N/A 299s N/A
java-sdk square N/A N/A 327s N/A
php-sdk square N/A N/A 123s N/A
python-sdk square N/A N/A 164s N/A
ruby-sdk-v2 square N/A N/A 154s N/A
rust-sdk square N/A N/A 131s N/A
swift-sdk square N/A N/A 141s N/A
ts-sdk square N/A N/A 141s N/A

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).

@Swimburger Swimburger enabled auto-merge (squash) April 7, 2026 00:00
@Swimburger Swimburger changed the title feat(csharp-sdk): remove omitted basic auth fields from SDK API, use empty string internally feat(csharp): remove omitted basic auth fields from SDK API, use empty string internally Apr 7, 2026
@Swimburger Swimburger merged commit 6d6f906 into main Apr 7, 2026
285 of 288 checks passed
@Swimburger Swimburger deleted the devin/1774997734-basic-auth-optional-csharp-sdk branch April 7, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants