Skip to content

feat(internal): support optional username/password in basic auth when configured in IR#14378

Open
Swimburger wants to merge 9 commits intomainfrom
devin/1774983028-basic-auth-optional-fields
Open

feat(internal): support optional username/password in basic auth when configured in IR#14378
Swimburger wants to merge 9 commits intomainfrom
devin/1774983028-basic-auth-optional-fields

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Mar 31, 2026

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/passwordOmit flags in the IR. Default behavior (both fields required) is preserved.

Encoding convention (when optional fields are enabled):

  • Username onlybase64("username:") (trailing colon)
  • Password onlybase64(":password") (leading colon)
  • Bothbase64("username:password") (existing behavior)
  • Neither → Authorization header is omitted / throws error

Changes Made

Generators updated (6 languages):

  • TypeScript (BasicAuthProviderGenerator.ts): Checks this.authScheme.usernameOmit/passwordOmit, branches generateCanCreateStatements() and generateGetAuthRequestStatements() accordingly. Core BasicAuth.ts utility made flexible (optional interface fields, ?? "" fallback). Null guard (authHeader != null ? ...) preserved in all code paths. AuthOptions type now marks fields as optional (?) with Supplier<string> | undefined when the corresponding omit flag is set.
  • Python (client_wrapper_generator.py): Checks basic_auth_scheme.username_omit/password_omit via getattr, conditionally uses or/and guard and or ""/direct args.
  • Java (BasicAuthProviderGenerator.java): Checks basicAuthScheme.getUsernameOmit().orElse(false), conditionally uses &&/|| null checks and ternary-to-empty-string in credential construction. Null-guards suppliers in eitherOmitted path ($N != null ? $N.get() : null) to prevent NPE when only one credential is provided.
  • C# (RootClientGenerator.ts): Checks basicScheme.usernameOmit/passwordOmit, conditionally uses ||/&& guard and ?? ""/direct interpolation.
  • Ruby (RootClientGenerator.ts): Checks basicAuthScheme.usernameOmit/passwordOmit via as unknown as Record<string, unknown> cast (IR v61 SDK lacks these fields in types), conditionally uses ||/&& guard and || ""/direct variable in string interpolation.
  • PHP (RootClientGenerator.ts): Same cast pattern as Ruby (IR v62), same conditional logic as C#/Ruby.

Seed test fixture added:

  • New basic-auth-optional fixture in test-definitions/fern/apis/basic-auth-optional/ with password: omit: true
  • Generated seed output committed for all 7 generators: TypeScript, Python, Java, Go, C#, Ruby, PHP
  • Validates that the eitherOmitted code path produces correct generated code (e.g., password?: string in TS, or "" fallbacks in Python, ?? "" in C#/PHP)
  • IR-to-JSON-schema snapshot added for basic-auth-optional-type_errors:UnauthorizedRequestErrorBody

Key design decisions:

  • When usernameOmit or passwordOmit is not set (default), generators produce identical output to before this PR
  • When either flag is true, both fields become optional in generated code (eitherOmitted pattern)
  • TypeScript BasicAuth.ts core utility interface is unconditionally widened to username?: string; password?: string; — safe because the generator controls what it passes
  • Ruby/PHP generators use as unknown as Record<string, unknown> to access omit flags since their IR SDK versions (v61/v62) don't include these fields in type definitions

Not changed:

  • Go v1/v2 — separate handling
  • IR types — usernameOmit/passwordOmit fields already exist in BasicAuthScheme

Updates since last revision

  • TypeScript AuthOptions type fix: getAuthOptionsProperties now checks usernameOmit/passwordOmit flags to make the corresponding field optional (?) with Supplier<string> | undefined type. Previously, the AuthOptions type did not reflect the omit flags, so clients couldn't be constructed without providing the omittable credential.
  • Biome formatting fix: Ternary expressions in BasicAuthProviderGenerator.ts reformatted to satisfy biome's line-break rules.
  • IR-to-JSON-schema snapshot: Added missing snapshot for basic-auth-optional-type_errors:UnauthorizedRequestErrorBody.
  • Existing basic-auth fixture updated: Seed output for seed/ts-sdk/basic-auth/ regenerated to reflect the unconditionally widened BasicAuth.ts interface and updated unit tests.

Testing

  • Unit tests added/updated (TypeScript BasicAuth.test.ts: both provided, username-only, password-only, neither, both empty)
  • All seed-test-results pass for default fixtures — confirms existing behavior is unchanged
  • New basic-auth-optional seed fixture validates generated output for the eitherOmitted code path across all 7 generators
  • IR-to-JSON-schema snapshot tests pass
  • compile, check, lint, biome CI jobs pass
  • test-ete has a flaky timeout on "python basic generation" (60s timeout, unrelated to this PR)

⚠️ Reviewer Checklist

  1. Java supplier null-safety: In eitherOmitted path, $N != null ? $N.get() : null assumes 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 when canCreate returns true with only one credential.
  2. eitherOmitted semantics: When either usernameOmit or passwordOmit is true, both fields become optional in generated code. Verify this is intended vs. making only the flagged field optional.
  3. Ruby/PHP as unknown as Record<string, unknown> cast: These generators use older IR SDK versions that lack usernameOmit/passwordOmit in 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.
  4. Python getattr usage: 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.
  5. TypeScript BasicAuth.ts interface widening: The core utility's BasicAuth interface now has optional fields regardless of IR flags. This changes the seed/ts-sdk/basic-auth/ output too. The generator controls what it passes, but this does widen the type contract for all generated TypeScript SDKs.
  6. Seed fixture only tests passwordOmit: The basic-auth-optional fixture sets password: omit: true but does not test username: omit: true or both omitted. Verify this single-direction coverage is sufficient.
  7. Changelog entries in 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


Open with Devin

… generators

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.

Swimburger and others added 2 commits March 31, 2026 19:02
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>
@devin-ai-integration devin-ai-integration bot changed the title feat: support optional username and password in basic auth across all generators feat: support optional username/password in basic auth when configured in IR Mar 31, 2026
@devin-ai-integration devin-ai-integration bot changed the title feat: support optional username/password in basic auth when configured in IR feat(internal): support optional username/password in basic auth when configured in IR Mar 31, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

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 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment thread generators/php/sdk/src/root-client/RootClientGenerator.ts
Comment on lines +126 to +127
const scheme = basicAuthScheme as unknown as Record<string, unknown>;
const eitherOmitted = scheme.usernameOmit === true || scheme.passwordOmit === true;
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.

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

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.

Comment on lines +563 to 572
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 \"\""),
],
)
)
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.

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

Fix in Graphite


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

Swimburger and others added 5 commits March 31, 2026 20:06
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>
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.

1 participant