feat(csharp): remove omitted basic auth fields from SDK API, use empty string internally#14409
Conversation
…en configured in IR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
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.
…y instead of coarse eitherOmitted flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| 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`; | ||
| } |
There was a problem hiding this comment.
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
Is this helpful? React 👍 or 👎 to let us know.
… 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>
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>
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>
…en first scheme skipped by continue Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| } else { | ||
| // Both fields omitted — skip auth header entirely when auth is optional | ||
| continue; | ||
| } |
There was a problem hiding this comment.
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.
| } 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
Is this helpful? React 👍 or 👎 to let us know.
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>
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>
…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>
| 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); |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| public SeedBasicAuthPwOmittedClient( | ||
| string? username = null, | ||
| string? password = null, |
There was a problem hiding this comment.
why is there still a password field when it should be omitted?
…c-auth-optional-csharp-sdk
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>
SDK Generation Benchmark ResultsComparing PR branch against Full benchmark table (click to expand)
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 |
| 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}:{""}"))}"; |
…string interpolation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SDK Generation Benchmark ResultsComparing PR branch against Full benchmark table (click to expand)
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 |
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>
| 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); |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
SDK Generation Benchmark ResultsComparing PR branch against Full benchmark table (click to expand)
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 |
Description
Refs #14378
When
usernameOmitorpasswordOmitflags are set in the IR'sBasicAuthScheme, 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 theAuthorization: Basicheader (e.g.password: omit: true→ header encodesusername:). Default behavior (both required) is preserved when no omit flags are set.Split from #14378 (one PR per generator).
Changes Made
RootClientGenerator.ts:basicScheme.usernameOmit/basicScheme.passwordOmitdirectly (no cast needed —@fern-fern/ir-sdkv66 has typed fields)paramsarray (not just made optional). Only non-omitted fields appear in the generated constructor.$"{username}:"when password is omitted) — no{""}wrapper or null-coalescing fallback.passwordOmitis set, condition isusername != null)i === 0index check withisFirstBlocktracking variable, so that when a both-omitted scheme is skipped viacontinue, the next scheme still correctly emitsifinstead ofelse ifusernameOmitandpasswordOmitare true, the scheme is skipped entirely (continue) — no meaninglessAuthorization: Basic Og==header is sentEndpointSnippetGenerator.ts(dynamic snippets): Omitted fields are excluded from generated snippet constructor args. Usesas unknown as Record<string, unknown>cast (necessary —@fern-api/dynamic-ir-sdklacks typedusernameOmit/passwordOmitfields)DynamicSnippetsConverter.ts: PassesusernameOmit/passwordOmitthrough to dynamic IR so downstream snippet generators can read the flagsversions.yml: new 2.58.0 entry (irVersion: 66)snippet.json: Updated to reflect password omission — constructor calls now usenew SeedBasicAuthPwOmittedClient("USERNAME")(single arg)basic-auth-pw-omittedtest fixture withpassword: omit: true, plus seed output atseed/csharp-sdk/basic-auth-pw-omitted/Updates since last revision
SeedBasicAuthPwOmittedClient.csnow correctly removes thepasswordparameter 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.writeTextStatementline.versions.yml.Example0.cs–Example6.csnow exclude thepasswordnamed argument.BaseMockServerTest.csinstantiates with only"USERNAME".Testing
basic-auth-pw-omittedfixturebiome check)test-etefailure is pre-existing flaky timeout (upgrade-generator.test.ts180s) — unrelated to this PRisFirstBlockcontrol flow (lines ~456–490 ofRootClientGenerator.ts): Confirm thatcontinue(both-omitted case) does not setisFirstBlock = false, so the next non-skipped scheme correctly getsif.$"${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.EndpointSnippetGenerator.tsusesas unknown as Record<string, unknown>because@fern-api/dynamic-ir-sdklacks 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).DynamicSnippetsConverter.tsschema gap:usernameOmit/passwordOmitare passed through as extra properties on theBasicAuthobject, 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