Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
266c344
feat(csharp-sdk): support optional username/password in basic auth wh…
Swimburger Mar 31, 2026
99467fe
fix(csharp-sdk): use per-field omit checks and constructor optionalit…
Swimburger Apr 1, 2026
3b8db30
fix(csharp-sdk): regenerate seed output for basic-auth-optional after…
Swimburger Apr 1, 2026
86ac58b
merge: resolve versions.yml conflict with main (bump to 2.55.4)
Swimburger Apr 1, 2026
a405301
fix(csharp-sdk): update createdAt date to 2026-04-01 for v2.55.4
Swimburger Apr 1, 2026
b41fd85
fix(csharp-sdk): remove omitted fields entirely from constructor para…
Swimburger Apr 2, 2026
88250dd
ci: retrigger CI after flaky csharp-sdk cancellation
Swimburger Apr 2, 2026
abe481b
ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation
Swimburger Apr 2, 2026
61e6899
ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation …
Swimburger Apr 2, 2026
86bdc62
ci: retrigger CI for flaky csharp-sdk seed-test-results cancellation …
Swimburger Apr 2, 2026
a4edff7
fix(csharp-sdk): skip auth header when both fields omitted and auth i…
Swimburger Apr 2, 2026
0aaecec
merge: resolve versions.yml conflict with main, fix if/else if bug wh…
Swimburger Apr 2, 2026
784d50b
merge: resolve versions.yml conflict with main (bump to 2.56.1)
Swimburger Apr 2, 2026
342a4ae
merge: resolve versions.yml conflict with main (bump to 2.56.2)
Swimburger Apr 2, 2026
93c2242
merge: resolve versions.yml conflict with main (bump to 2.56.3)
Swimburger Apr 2, 2026
c7ddfb5
merge: resolve versions.yml conflict with main (bump to 2.56.4)
Swimburger Apr 2, 2026
3921d83
merge: resolve versions.yml conflict with main (bump to 2.56.5)
Swimburger Apr 2, 2026
3b58b4e
merge: resolve versions.yml conflict with main (bump to 2.56.6)
Swimburger Apr 3, 2026
6867963
fix(csharp-sdk): use 'omit' instead of 'optional' in versions.yml cha…
Swimburger Apr 3, 2026
90f03d8
refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted
Swimburger Apr 3, 2026
a906dce
fix(csharp-sdk): bump version to 2.57.0 (feat requires minor bump)
Swimburger Apr 3, 2026
1f03553
merge: resolve conflict with main (IR v66 name compression + basic au…
Swimburger Apr 3, 2026
996210c
merge: resolve CI workflow conflict with main
Swimburger Apr 3, 2026
fcf385e
fix(csharp-sdk): handle usernameOmit/passwordOmit in dynamic snippets…
Swimburger Apr 3, 2026
6fb25c8
refactor(csharp-sdk): simplify omit checks from === true to !!
Swimburger Apr 3, 2026
6dafafd
fix(csharp-sdk): update irVersion to 66 to match seed.yml
Swimburger Apr 3, 2026
5cb74f2
refactor(csharp-sdk): simplify === true to !! in RootClientGenerator …
Swimburger Apr 3, 2026
bc8f325
fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter …
Swimburger Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions generators/csharp/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,16 +385,24 @@ export class EndpointSnippetGenerator extends WithGeneration {
auth: FernIr.dynamic.BasicAuth;
values: FernIr.dynamic.BasicAuthValues;
}): NamedArgument[] {
return [
{
// usernameOmit/passwordOmit may exist in newer IR versions
const authRecord = auth as unknown as Record<string, unknown>;
const usernameOmitted = !!authRecord.usernameOmit;
const passwordOmitted = !!authRecord.passwordOmit;
const args: NamedArgument[] = [];
if (!usernameOmitted) {
args.push({
name: this.context.getParameterName(auth.username),
assignment: this.csharp.Literal.string(values.username)
},
{
});
}
if (!passwordOmitted) {
args.push({
name: this.context.getParameterName(auth.password),
assignment: this.csharp.Literal.string(values.password)
}
];
});
}
return args;
}

private getConstructorBearerAuthArgs({
Expand Down
47 changes: 35 additions & 12 deletions generators/csharp/sdk/src/root-client/RootClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
(s): s is typeof s & { type: "basic" } => s.type === "basic"
);
const isAuthOptional = !this.context.ir.sdkConfig.isAuthMandatory;
let isFirstBlock = true;
for (let i = 0; i < basicSchemes.length; i++) {
const basicScheme = basicSchemes[i];
if (basicScheme == null) {
Expand All @@ -467,15 +468,30 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
const passwordAccess = unified
? `clientOptions.${this.toPascalCase(passwordName)}`
: passwordName;
const usernameOmitted = !!basicScheme.usernameOmit;
const passwordOmitted = !!basicScheme.passwordOmit;
// Condition: only require non-omitted fields to be present
let condition: string;
if (!usernameOmitted && !passwordOmitted) {
condition = `${usernameAccess} != null && ${passwordAccess} != null`;
} else if (usernameOmitted && !passwordOmitted) {
condition = `${passwordAccess} != null`;
} else if (!usernameOmitted && passwordOmitted) {
condition = `${usernameAccess} != null`;
} else {
// Both fields omitted — skip auth header entirely when auth is non-mandatory
continue;
}
Comment on lines +481 to +484
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.

if (isAuthOptional || basicSchemes.length > 1) {
const controlFlowKeyword = i === 0 ? "if" : "else if";
innerWriter.controlFlow(
controlFlowKeyword,
this.csharp.codeblock(`${usernameAccess} != null && ${passwordAccess} != null`)
);
const controlFlowKeyword = isFirstBlock ? "if" : "else if";
innerWriter.controlFlow(controlFlowKeyword, this.csharp.codeblock(condition));
}
isFirstBlock = false;
// Omitted fields use empty string directly
const usernameExpr = usernameOmitted ? `""` : `${usernameAccess}`;
const passwordExpr = passwordOmitted ? `""` : `${passwordAccess}`;
innerWriter.writeTextStatement(
`clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameAccess}}:{${passwordAccess}}"))}"`
`clientOptionsWithAuth.Headers["Authorization"] = $"Basic {Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes($"{${usernameExpr}}:{${passwordExpr}}"))}"`
);
if (isAuthOptional || basicSchemes.length > 1) {
innerWriter.endControlFlow();
Expand Down Expand Up @@ -802,8 +818,12 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
{
const usernameName = this.case.camelSafe(scheme.username);
const passwordName = this.case.camelSafe(scheme.password);
return [
{
const usernameOmitted = !!scheme.usernameOmit;
const passwordOmitted = !!scheme.passwordOmit;
// When omit is true, the field is completely removed from the end-user API.
const params: ConstructorParameter[] = [];
if (!usernameOmitted) {
params.push({
name: usernameName,
docs: scheme.docs ?? `The ${usernameName} to use for authentication.`,
isOptional,
Expand All @@ -817,8 +837,10 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
type: this.Primitive.string,
environmentVariable: scheme.usernameEnvVar,
exampleValue: this.case.screamingSnakeSafe(scheme.username)
},
{
});
}
if (!passwordOmitted) {
params.push({
name: passwordName,
docs: scheme.docs ?? `The ${passwordName} to use for authentication.`,
isOptional,
Expand All @@ -832,8 +854,9 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkGeneratorC
type: this.Primitive.string,
environmentVariable: scheme.passwordEnvVar,
exampleValue: this.case.screamingSnakeSafe(scheme.password)
}
];
});
}
return params;
}
} else if (scheme.type === "oauth") {
if (this.oauth !== null) {
Expand Down
13 changes: 12 additions & 1 deletion generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.58.0
changelogEntry:
- summary: |
Support omitting username or password from basic auth when configured via
`usernameOmit` or `passwordOmit` in the IR. Omitted fields are removed from
the SDK's public API and treated as empty strings internally (e.g., omitting
password encodes `username:`, omitting username encodes `:password`). When
both are omitted, the Authorization header is skipped entirely.
type: feat
createdAt: "2026-04-03"
irVersion: 66

- version: 2.57.0-rc.0
changelogEntry:
- summary: |
Upgrade to IR v66 which compresses the IR Name type, reducing IR size and increasing performance.
type: feat
createdAt: "2026-04-01"
irVersion: 66

- version: 2.56.5
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,11 +732,22 @@ export class DynamicSnippetsConverter {
}
const scheme = auth.schemes[0];
switch (scheme.type) {
case "basic":
return DynamicSnippets.Auth.basic({
case "basic": {
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);
Comment on lines +736 to +749
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 smuggled outside the type system may be lost across serialization boundaries

The DynamicSnippetsConverter attaches usernameOmit/passwordOmit as extra fields on the BasicAuth object using a TypeScript type intersection (DynamicSnippets.BasicAuth & { usernameOmit?: boolean; passwordOmit?: boolean }). These fields are NOT part of the DynamicSnippets.BasicAuth interface defined at packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts:5-8 (which only has username and password). While Auth.basic() preserves them via spread (...value), the consumer in EndpointSnippetGenerator.ts:389 reads them back using an unsafe as unknown as Record<string, unknown> cast.

In the production dynamic-snippets flow, the dynamic IR is serialized to JSON and deserialized by the generator using @fern-api/dynamic-ir-sdk. If the SDK's deserializer strips fields not in the BasicAuth interface, usernameOmit/passwordOmit would silently default to false, causing the snippet generator to emit constructor arguments for omitted parameters — referencing parameters that don't exist in the generated SDK constructor.

Prompt for agents
The usernameOmit/passwordOmit fields need to be part of the official DynamicSnippets.BasicAuth type definition so they survive serialization/deserialization. The proper fix is:

1. Add usernameOmit and passwordOmit as optional boolean fields to the BasicAuth type in the dynamic IR schema at packages/ir-sdk/fern/apis/ir-types-latest/definition/ (the source of truth for the IR SDK types).
2. Regenerate the IR SDK so that DynamicSnippets.BasicAuth includes these fields natively.
3. Remove the type intersection hack in DynamicSnippetsConverter.ts and the unsafe Record<string, unknown> cast in EndpointSnippetGenerator.ts.

This ensures the fields are preserved through any serialization/deserialization boundary and makes the code type-safe.
Open in Devin Review

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

}
case "bearer":
return DynamicSnippets.Auth.bearer({
token: this.inflateName(scheme.token)
Expand Down
6 changes: 3 additions & 3 deletions seed/csharp-sdk/basic-auth-pw-omitted/snippet.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading