feat(internal): support optional username/password in basic auth when configured in IR#14378
feat(internal): support optional username/password in basic auth when configured in IR#14378Swimburger wants to merge 9 commits intomainfrom
Conversation
… generators 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.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…wordOmit IR flags Default behavior (both required) is preserved. Optional handling only applies when usernameOmit or passwordOmit is explicitly set in the IR. 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 scheme = basicAuthScheme as unknown as Record<string, unknown>; | ||
| const eitherOmitted = scheme.usernameOmit === true || scheme.passwordOmit === true; |
There was a problem hiding this comment.
🟡 Ruby generator uses as unknown as Record<string, unknown> in violation of CLAUDE.md TypeScript rules
CLAUDE.md explicitly prohibits as unknown as X type assertions: "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 Ruby generator uses as unknown as Record<string, unknown> to access usernameOmit/passwordOmit properties. The C# generator at generators/csharp/sdk/src/root-client/RootClientGenerator.ts:470 accesses the same properties directly on the typed object without any cast, and the Java generator uses typed getUsernameOmit() accessors (generators/java/sdk/src/main/java/com/fern/java/client/generators/auth/BasicAuthProviderGenerator.java:117), indicating these fields already exist on the IR types and a proper typed access should be used.
Prompt for agents
In generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts at lines 126-127, replace the `as unknown as Record<string, unknown>` cast with direct typed property access on basicAuthScheme, matching how the C# generator accesses basicScheme.usernameOmit directly at generators/csharp/sdk/src/root-client/RootClientGenerator.ts:470. Change:
const scheme = basicAuthScheme as unknown as Record<string, unknown>;
const eitherOmitted = scheme.usernameOmit === true || scheme.passwordOmit === true;
To:
const eitherOmitted = basicAuthScheme.usernameOmit === true || basicAuthScheme.passwordOmit === true;
If the IR type for the Ruby generator's BasicAuthScheme does not include these fields, update the IR type or import to include them rather than bypassing the type system.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
The as unknown as Record<string, unknown> cast is intentional here. The Ruby generator uses IR SDK v61 (@fern-fern/ir-sdk@^61.7.0) which does not include usernameOmit/passwordOmit on BasicAuthScheme — those fields were added in v63+. The C# generator can access them directly because it uses IR SDK v65.
Bumping the Ruby generator's IR SDK dependency to v65 would be a larger change with potential side effects and is beyond the scope of this PR. The cast safely handles the version mismatch — if the fields don't exist at runtime (older IR), the === true check returns false, preserving the default "both required" behavior.
| if either_omitted: | ||
| writer.write_node( | ||
| AST.ClassInstantiation( | ||
| class_=httpx.HttpX.BASIC_AUTH, | ||
| args=[ | ||
| AST.Expression(f"{username_var}"), | ||
| AST.Expression(f"{password_var}"), | ||
| AST.Expression(f"self.{names.get_username_getter_name(basic_auth_scheme)}() or \"\""), | ||
| AST.Expression(f"self.{names.get_password_getter_name(basic_auth_scheme)}() or \"\""), | ||
| ], | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Missing validation when auth is mandatory and either_omitted is true. The code directly encodes credentials without checking if both are None/empty.
If both username and password are None, this will encode ":" (just a colon) and set it as the Authorization header, violating the requirement that "when neither is provided, the Authorization header is omitted entirely."
Fix:
if either_omitted:
username_expr = f"self.{names.get_username_getter_name(basic_auth_scheme)}()"
password_expr = f"self.{names.get_password_getter_name(basic_auth_scheme)}()"
writer.write_line(f"_username = {username_expr}")
writer.write_line(f"_password = {password_expr}")
writer.write_line("if _username is None and _password is None:")
with writer.indent():
writer.write_line('raise ValueError("At least one of username or password must be provided")')
writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ')
writer.write_node(
AST.ClassInstantiation(
class_=httpx.HttpX.BASIC_AUTH,
args=[
AST.Expression('_username or ""'),
AST.Expression('_password or ""'),
],
)
)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>
…assword Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…wordOmit is set Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…-auth-optional 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>
Description
Updates basic auth handling across all SDK generators to support username-only, password-only, and both-provided scenarios — but only when explicitly configured via
usernameOmit/passwordOmitflags in the IR. Default behavior (both fields required) is preserved.Encoding convention (when optional fields are enabled):
base64("username:")(trailing colon)base64(":password")(leading colon)base64("username:password")(existing behavior)Changes Made
Generators updated (6 languages):
BasicAuthProviderGenerator.ts): Checksthis.authScheme.usernameOmit/passwordOmit, branchesgenerateCanCreateStatements()andgenerateGetAuthRequestStatements()accordingly. CoreBasicAuth.tsutility made flexible (optional interface fields,?? ""fallback). Null guard (authHeader != null ? ...) preserved in all code paths.AuthOptionstype now marks fields as optional (?) withSupplier<string> | undefinedwhen the corresponding omit flag is set.client_wrapper_generator.py): Checksbasic_auth_scheme.username_omit/password_omitviagetattr, conditionally usesor/andguard andor ""/direct args.BasicAuthProviderGenerator.java): ChecksbasicAuthScheme.getUsernameOmit().orElse(false), conditionally uses&&/||null checks and ternary-to-empty-string in credential construction. Null-guards suppliers ineitherOmittedpath ($N != null ? $N.get() : null) to prevent NPE when only one credential is provided.RootClientGenerator.ts): ChecksbasicScheme.usernameOmit/passwordOmit, conditionally uses||/&&guard and?? ""/direct interpolation.RootClientGenerator.ts): ChecksbasicAuthScheme.usernameOmit/passwordOmitviaas unknown as Record<string, unknown>cast (IR v61 SDK lacks these fields in types), conditionally uses||/&&guard and|| ""/direct variable in string interpolation.RootClientGenerator.ts): Same cast pattern as Ruby (IR v62), same conditional logic as C#/Ruby.Seed test fixture added:
basic-auth-optionalfixture intest-definitions/fern/apis/basic-auth-optional/withpassword: omit: trueeitherOmittedcode path produces correct generated code (e.g.,password?: stringin TS,or ""fallbacks in Python,?? ""in C#/PHP)basic-auth-optional-type_errors:UnauthorizedRequestErrorBodyKey design decisions:
usernameOmitorpasswordOmitis not set (default), generators produce identical output to before this PReitherOmittedpattern)BasicAuth.tscore utility interface is unconditionally widened tousername?: string; password?: string;— safe because the generator controls what it passesas unknown as Record<string, unknown>to access omit flags since their IR SDK versions (v61/v62) don't include these fields in type definitionsNot changed:
usernameOmit/passwordOmitfields already exist inBasicAuthSchemeUpdates since last revision
getAuthOptionsPropertiesnow checksusernameOmit/passwordOmitflags to make the corresponding field optional (?) withSupplier<string> | undefinedtype. Previously, the AuthOptions type did not reflect the omit flags, so clients couldn't be constructed without providing the omittable credential.BasicAuthProviderGenerator.tsreformatted to satisfy biome's line-break rules.basic-auth-optional-type_errors:UnauthorizedRequestErrorBody.seed/ts-sdk/basic-auth/regenerated to reflect the unconditionally widenedBasicAuth.tsinterface and updated unit tests.Testing
BasicAuth.test.ts: both provided, username-only, password-only, neither, both empty)basic-auth-optionalseed fixture validates generated output for theeitherOmittedcode path across all 7 generatorscompile,check,lint,biomeCI jobs passtest-etehas a flaky timeout on "python basic generation" (60s timeout, unrelated to this PR)eitherOmittedpath,$N != null ? $N.get() : nullassumes the supplier field itself can be null when a credential is not provided. Verify this matches how the Java client constructor populates the supplier fields whencanCreatereturns true with only one credential.eitherOmittedsemantics: When eitherusernameOmitorpasswordOmitis true, both fields become optional in generated code. Verify this is intended vs. making only the flagged field optional.as unknown as Record<string, unknown>cast: These generators use older IR SDK versions that lackusernameOmit/passwordOmitin type definitions. The cast works at runtime but has no compile-time safety — if the IR field names change, this won't break at build time.getattrusage:getattr(basic_auth_scheme, "username_omit", None)is used defensively. If the IR SDK (v65) definitely has these fields, direct attribute access would be cleaner.BasicAuth.tsinterface widening: The core utility'sBasicAuthinterface now has optional fields regardless of IR flags. This changes theseed/ts-sdk/basic-auth/output too. The generator controls what it passes, but this does widen the type contract for all generated TypeScript SDKs.passwordOmit: Thebasic-auth-optionalfixture setspassword: omit: truebut does not testusername: omit: trueor both omitted. Verify this single-direction coverage is sufficient.versions.yml: Most summaries describe unconditional optional support but the actual behavior is conditional on IR flags. Only the Python entry (v5.2.1) mentions "when configured in IR". Consider updating the others.Link to Devin session: https://app.devin.ai/sessions/0786b963284f4799acb409d5373cde0a
Requested by: @Swimburger