Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
90e5382
feat(java-sdk): support optional username/password in basic auth when…
Swimburger Mar 31, 2026
71f8797
fix(java-sdk): use per-field omit checks instead of coarse eitherOmit…
Swimburger Apr 1, 2026
892774a
fix(java-sdk): update AbstractRootClientGenerator.visitBasic() with p…
Swimburger Apr 1, 2026
c7e3900
fix(java-sdk): fix spotless formatting for addStatement calls
Swimburger Apr 1, 2026
631e5c4
fix(java-sdk): update seed output for basic-auth-optional to reflect …
Swimburger Apr 1, 2026
65f4da3
Merge remote-tracking branch 'origin/main' into devin/1774997719-basi…
Swimburger Apr 2, 2026
ee43296
fix(java-sdk): remove omitted fields entirely from builder/constructo…
Swimburger Apr 2, 2026
e45749d
fix(java-sdk): regenerate seed output with complete field removal for…
Swimburger Apr 2, 2026
d478ef3
fix(java-sdk): handle omitted fields in ENDPOINT_SECURITY routing aut…
Swimburger Apr 2, 2026
ebee291
fix(java-sdk): fix BasicAuthProvider constructor arg mismatch in ENDP…
Swimburger Apr 2, 2026
8f8edec
fix(java-sdk): apply spotless formatting
Swimburger Apr 2, 2026
d7f8488
fix(java-sdk): make auth error message conditional on which fields ar…
Swimburger Apr 2, 2026
fe3f255
ci: retrigger CI for flaky java-sdk seed-test-results cancellation
Swimburger Apr 2, 2026
1d059f3
ci: retrigger CI for flaky java-sdk seed-test-results cancellation (a…
Swimburger Apr 2, 2026
5e6e9e7
fix(java-sdk): skip auth header when both fields omitted and auth is …
Swimburger Apr 2, 2026
6678bdb
merge: resolve versions.yml conflict with main (bump to 4.1.1)
Swimburger Apr 2, 2026
31f9d42
fix(java-sdk): use 'omit' instead of 'optional' in versions.yml chang…
Swimburger Apr 3, 2026
9bf8f71
fix(java-sdk): correct changelog - both omitted skips auth, not Runti…
Swimburger Apr 3, 2026
7ffe2fd
refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted
Swimburger Apr 3, 2026
0204105
merge: resolve versions.yml conflict with main (bump to 4.1.2)
Swimburger Apr 3, 2026
0279ac7
fix(java-sdk): bump version to 4.2.0 (feat requires minor bump)
Swimburger Apr 3, 2026
b4dc575
Merge remote-tracking branch 'origin/main' into devin/1774997719-basi…
Swimburger Apr 3, 2026
a70da13
fix(java-sdk): handle usernameOmit/passwordOmit in dynamic snippets g…
Swimburger Apr 3, 2026
f625a07
refactor(java-sdk): simplify omit checks from === true to !!
Swimburger Apr 3, 2026
7aeb101
fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter …
Swimburger Apr 3, 2026
68d38a3
ci: retrigger CI (flaky go-sdk job cancellation)
Swimburger Apr 4, 2026
558f069
ci: retrigger CI (flaky python-sdk seed-test-results failure)
Swimburger Apr 4, 2026
c91374e
ci: retrigger CI (2nd attempt - flaky test-ete timeout)
Swimburger Apr 4, 2026
a13c6cf
merge: resolve versions.yml conflict with main (4.1.2 from main)
Swimburger Apr 8, 2026
132be8e
fix: update createdAt date to 2026-04-08 for version 4.2.0
Swimburger Apr 8, 2026
dbb260e
merge: resolve versions.yml conflict with main (4.1.3 from main)
Swimburger Apr 9, 2026
548dda0
merge: resolve versions.yml conflict with main (4.2.0-rc.0 from main)
Swimburger Apr 9, 2026
605de9b
fix: update irVersion to 66 for 4.2.0 to match seed.yml after IR v66 …
Swimburger Apr 9, 2026
68be15f
merge: resolve versions.yml conflict with main (4.1.4 from main)
Swimburger Apr 10, 2026
6fc61f7
Merge remote-tracking branch 'origin/main' into devin/1774997719-basi…
Swimburger Apr 10, 2026
073f537
merge: resolve conflict with main (4.2.0-rc.1, NameUtils.toName wrapper)
Swimburger Apr 10, 2026
23ee5e5
merge: resolve versions.yml conflict with main (4.2.0 from main)
Swimburger Apr 15, 2026
98e6f6b
fix: correct createdAt date for 4.2.1 to match chronological order
Swimburger Apr 15, 2026
40340df
merge: resolve versions.yml conflict with main (bumped to 4.2.2 above…
Swimburger Apr 15, 2026
4718d36
merge: resolve versions.yml conflict with main (bump to 4.2.3)
Swimburger Apr 16, 2026
9115e2c
Merge remote-tracking branch 'origin/main' into devin/1774997719-basi…
Swimburger Apr 17, 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
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,24 @@ export class EndpointSnippetGenerator {
auth: FernIr.dynamic.BasicAuth;
values: FernIr.dynamic.BasicAuthValues;
}): java.BuilderParameter[] {
// 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;
Comment on lines +443 to +445
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

🔴 Forbidden as unknown as X type assertion bypasses type system instead of updating dynamic IR type

The code uses auth as unknown as Record<string, unknown> to access usernameOmit/passwordOmit fields that don't exist on FernIr.dynamic.BasicAuth. This violates CLAUDE.md's explicit rule: "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 dynamic IR's BasicAuth type at packages/ir-sdk/fern/apis/ir-types-latest/definition/dynamic/auth.yml:22-25 only declares username and password. While packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts:736-748 does attach these fields at runtime using an intersection type, the proper fix is to add usernameOmit: optional<boolean> and passwordOmit: optional<boolean> to the dynamic IR's BasicAuth definition, then regenerate the SDK types so the field access is type-safe.

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.

Acknowledged — the as unknown as Record<string, unknown> cast is a known limitation. The proper fix requires adding usernameOmit/passwordOmit to the dynamic IR's BasicAuth type definition and regenerating SDK types. This is deferred to a follow-up (IR schema changes are out of scope for this PR per reviewer instruction).

const credentialParts: string[] = [];
if (!usernameOmitted) {
credentialParts.push(`"${values.username}"`);
}
if (!passwordOmitted) {
credentialParts.push(`"${values.password}"`);
}
if (credentialParts.length === 0) {
return [];
}
return [
{
name: "credentials",
value: java.TypeLiteral.raw(`"${values.username}", "${values.password}"`)
value: java.TypeLiteral.raw(credentialParts.join(", "))
}
];
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,33 @@ public GeneratedJavaFile generateFile() {
ParameterizedTypeName stringSupplierType =
ParameterizedTypeName.get(ClassName.get(Supplier.class), ClassName.get(String.class));

FieldSpec usernameSupplierField = FieldSpec.builder(
stringSupplierType, "usernameSupplier", Modifier.PRIVATE, Modifier.FINAL)
.build();
FieldSpec passwordSupplierField = FieldSpec.builder(
stringSupplierType, "passwordSupplier", Modifier.PRIVATE, Modifier.FINAL)
.build();
boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false);
boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false);

FieldSpec usernameSupplierField = usernameOmitted
? null
: FieldSpec.builder(stringSupplierType, "usernameSupplier", Modifier.PRIVATE, Modifier.FINAL)
.build();
FieldSpec passwordSupplierField = passwordOmitted
? null
: FieldSpec.builder(stringSupplierType, "passwordSupplier", Modifier.PRIVATE, Modifier.FINAL)
.build();

String usernameEnvVar =
basicAuthScheme.getUsernameEnvVar().map(ev -> ev.get()).orElse(null);
String passwordEnvVar =
basicAuthScheme.getPasswordEnvVar().map(ev -> ev.get()).orElse(null);

String errorMessage = "Please provide username and password when initializing the client";
String errorMessage;
if (usernameOmitted && !passwordOmitted) {
errorMessage = "Please provide password when initializing the client";
} else if (!usernameOmitted && passwordOmitted) {
errorMessage = "Please provide username when initializing the client";
} else {
errorMessage = "Please provide username and password when initializing the client";
}

TypeSpec basicAuthProviderClass = TypeSpec.classBuilder(className)
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(authProviderClassName)
.addJavadoc("Auth provider for Basic authentication.\n")
Expand All @@ -89,16 +101,23 @@ public GeneratedJavaFile generateFile() {
Modifier.STATIC,
Modifier.FINAL)
.initializer("$S", errorMessage)
.build())
.addField(usernameSupplierField)
.addField(passwordSupplierField)
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(stringSupplierType, "usernameSupplier")
.addParameter(stringSupplierType, "passwordSupplier")
.addStatement("this.$N = usernameSupplier", usernameSupplierField)
.addStatement("this.$N = passwordSupplier", passwordSupplierField)
.build())
.build());

// Only add fields and constructor params for non-omitted fields
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
if (!usernameOmitted) {
classBuilder.addField(usernameSupplierField);
constructorBuilder.addParameter(stringSupplierType, "usernameSupplier");
constructorBuilder.addStatement("this.$N = usernameSupplier", usernameSupplierField);
}
if (!passwordOmitted) {
classBuilder.addField(passwordSupplierField);
constructorBuilder.addParameter(stringSupplierType, "passwordSupplier");
constructorBuilder.addStatement("this.$N = passwordSupplier", passwordSupplierField);
}

TypeSpec basicAuthProviderClass = classBuilder
.addMethod(constructorBuilder.build())
.addMethod(buildGetAuthHeaders(endpointMetadataClassName, usernameSupplierField, passwordSupplierField))
.addMethod(buildCanCreateMethod(usernameEnvVar, passwordEnvVar))
.build();
Expand All @@ -114,18 +133,37 @@ public GeneratedJavaFile generateFile() {

private MethodSpec buildGetAuthHeaders(
ClassName endpointMetadataClassName, FieldSpec usernameSupplierField, FieldSpec passwordSupplierField) {
return MethodSpec.methodBuilder("getAuthHeaders")
boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false);
boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

MethodSpec.Builder builder = MethodSpec.methodBuilder("getAuthHeaders")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(endpointMetadataClassName, "endpointMetadata")
.returns(ParameterizedTypeName.get(Map.class, String.class, String.class))
.addStatement("String username = $N.get()", usernameSupplierField)
.addStatement("String password = $N.get()", passwordSupplierField)
.beginControlFlow("if (username == null || password == null)")
.addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class)
.endControlFlow()
.addStatement("String credentials = username + \":\" + password")
.addStatement(
.returns(ParameterizedTypeName.get(Map.class, String.class, String.class));

// Get values: omitted fields use empty string directly, non-omitted fields read from supplier
if (usernameOmitted) {
builder.addStatement("String username = \"\"");
} else {
builder.addStatement("String username = $N.get()", usernameSupplierField);
builder.beginControlFlow("if (username == null)")
.addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class)
.endControlFlow();
}

if (passwordOmitted) {
builder.addStatement("String password = \"\"");
} else {
builder.addStatement("String password = $N.get()", passwordSupplierField);
builder.beginControlFlow("if (password == null)")
.addStatement("throw new $T(AUTH_CONFIG_ERROR_MESSAGE)", RuntimeException.class)
.endControlFlow();
}

builder.addStatement("String credentials = username + \":\" + password");

return builder.addStatement(
"String encoded = $T.getEncoder().encodeToString(credentials.getBytes($T.UTF_8))",
Base64.class,
StandardCharsets.class)
Expand All @@ -136,26 +174,43 @@ private MethodSpec buildGetAuthHeaders(
}

private MethodSpec buildCanCreateMethod(String usernameEnvVar, String passwordEnvVar) {
boolean usernameOmitted = basicAuthScheme.getUsernameOmit().orElse(false);
boolean passwordOmitted = basicAuthScheme.getPasswordOmit().orElse(false);

ParameterizedTypeName stringSupplierType =
ParameterizedTypeName.get(ClassName.get(Supplier.class), ClassName.get(String.class));

// Only non-omitted fields appear as parameters
MethodSpec.Builder builder = MethodSpec.methodBuilder("canCreate")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addJavadoc("Checks if this provider can be created with the given suppliers.\n")
.addParameter(stringSupplierType, "usernameSupplier")
.addParameter(stringSupplierType, "passwordSupplier")
.returns(boolean.class);

// Build per-field checks: omitted fields are always satisfied (true)
StringBuilder condition = new StringBuilder();
condition.append("(usernameSupplier != null");
if (usernameEnvVar != null) {
condition.append(" || System.getenv(\"").append(usernameEnvVar).append("\") != null");
if (!usernameOmitted) {
builder.addParameter(stringSupplierType, "usernameSupplier");
condition.append("(usernameSupplier != null");
if (usernameEnvVar != null) {
condition.append(" || System.getenv(\"").append(usernameEnvVar).append("\") != null");
}
condition.append(")");
} else {
condition.append("true");
}
condition.append(") && (passwordSupplier != null");
if (passwordEnvVar != null) {
condition.append(" || System.getenv(\"").append(passwordEnvVar).append("\") != null");

condition.append(" && ");

if (!passwordOmitted) {
builder.addParameter(stringSupplierType, "passwordSupplier");
condition.append("(passwordSupplier != null");
if (passwordEnvVar != null) {
condition.append(" || System.getenv(\"").append(passwordEnvVar).append("\") != null");
}
condition.append(")");
} else {
condition.append("true");
}
condition.append(")");

builder.addStatement("return " + condition);

Expand Down
16 changes: 12 additions & 4 deletions generators/java/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 4.2.3
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-16"
irVersion: 66

- version: 4.2.2
changelogEntry:
- summary: |
Expand Down Expand Up @@ -35,7 +47,6 @@
type: fix
createdAt: "2026-04-14"
irVersion: 66

- version: 4.2.0
changelogEntry:
- summary: |
Expand All @@ -59,7 +70,6 @@
type: feat
createdAt: "2026-04-10"
irVersion: 66

- version: 4.2.0-rc.0
changelogEntry:
- summary: |
Expand Down Expand Up @@ -89,7 +99,6 @@
type: fix
createdAt: "2026-04-09"
irVersion: 65

- version: 4.1.2
changelogEntry:
- summary: |
Expand All @@ -100,7 +109,6 @@
type: fix
createdAt: "2026-04-08"
irVersion: 65

- version: 4.1.1
changelogEntry:
- summary: |
Expand Down
Loading
Loading