Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b50e565
feat(php-sdk): support optional username/password in basic auth when …
Swimburger Mar 31, 2026
4cdca7e
fix(php-sdk): use per-field omit checks and constructor optionality i…
Swimburger Apr 1, 2026
8ed6052
fix(php-sdk): regenerate seed output for basic-auth-optional (fix pas…
Swimburger Apr 1, 2026
d71fe39
fix(php-sdk): remove omitted fields entirely from constructor params,…
Swimburger Apr 2, 2026
1bfe86e
fix(php-sdk): skip auth header when both fields omitted and auth is n…
Swimburger Apr 2, 2026
ae9411f
fix(php-sdk): use isFirstBlock to prevent else if without preceding i…
Swimburger Apr 2, 2026
ef5f19a
merge: resolve versions.yml conflict with main (bump to 2.3.1)
Swimburger Apr 2, 2026
cece764
merge: resolve versions.yml conflict with main (bump to 2.3.2)
Swimburger Apr 2, 2026
b04497f
fix(php-sdk): use 'omit' instead of 'optional' in versions.yml change…
Swimburger Apr 3, 2026
487b1ef
refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted
Swimburger Apr 3, 2026
7673607
merge: resolve versions.yml conflict with main (bump to 2.3.3)
Swimburger Apr 3, 2026
64441f5
fix(php-sdk): bump version to 2.4.0 (feat requires minor bump)
Swimburger Apr 3, 2026
c32cdc7
Merge remote-tracking branch 'origin/main' into devin/1774997779-basi…
Swimburger Apr 3, 2026
e986a7c
Merge remote-tracking branch 'origin/main' into devin/1774997779-basi…
Swimburger Apr 3, 2026
eb74e5f
fix(php-sdk): bump seed fixture IR version to v63 so passwordOmit is …
Swimburger Apr 3, 2026
4e33302
merge: resolve SeedClient.php conflict with main (keep omit-aware ver…
Swimburger Apr 3, 2026
daa1cd9
fix(php-sdk): handle usernameOmit/passwordOmit in dynamic snippets ge…
Swimburger Apr 3, 2026
93578dd
refactor(php-sdk): extract helper, simplify omit checks, remove ir-ve…
Swimburger Apr 3, 2026
6c2e94e
fix(php-sdk): update irVersion to 66 to match seed.yml
Swimburger Apr 3, 2026
5f3442f
fix(php-sdk): replace non-null assertion with null guard to satisfy b…
Swimburger Apr 3, 2026
76fa7d6
refactor(php-sdk): remove unnecessary as unknown casts - IR SDK v66 h…
Swimburger Apr 3, 2026
637982c
fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter …
Swimburger Apr 3, 2026
751d0c9
ci: retrigger CI (flaky python-sdk and test-ete failures)
Swimburger Apr 4, 2026
7569356
ci: retrigger CI (flaky python-sdk job failure)
Swimburger Apr 4, 2026
913e829
ci: retrigger CI (flaky test-ete timeout)
Swimburger Apr 4, 2026
3a30b9d
Merge remote-tracking branch 'origin/main' into devin/1774997779-basi…
Swimburger Apr 7, 2026
6ec9c1d
fix(php-sdk): clean credential string template and regenerate seed ou…
Swimburger Apr 7, 2026
b2db526
feat(php-sdk): enable wire tests for basic-auth-pw-omitted fixture
Swimburger Apr 7, 2026
b29fcdd
fix(php-sdk): respect usernameOmit/passwordOmit in WireTestGenerator.…
Swimburger Apr 7, 2026
19b38ac
chore(php-sdk): remove orphaned seed output for basic-auth-pw-omitted…
Swimburger Apr 7, 2026
75cff6e
fix(php-sdk): use test-username in wire test setUp to match WireMock …
Swimburger Apr 7, 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/php/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,16 +321,24 @@ export class EndpointSnippetGenerator {
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.getPropertyName(auth.username),
assignment: php.TypeLiteral.string(values.username)
},
{
});
}
if (!passwordOmitted) {
args.push({
name: this.context.getPropertyName(auth.password),
assignment: php.TypeLiteral.string(values.password)
}
];
});
}
return args;
}

private getConstructorEnvironmentArg({
Expand Down
88 changes: 67 additions & 21 deletions generators/php/sdk/src/root-client/RootClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,26 +355,25 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
const basicAuthSchemes = this.context.ir.auth.schemes.filter(
(s): s is typeof s & { type: "basic" } => s.type === "basic"
);
if (basicAuthSchemes.length > 0) {
const resolvedBasicAuthSchemes = basicAuthSchemes
.map((scheme) => this.resolveBasicAuthScheme(scheme))
.filter((resolved) => resolved != null);
if (resolvedBasicAuthSchemes.length > 0) {
const isAuthOptional = !this.context.ir.sdkConfig.isAuthMandatory;
for (let i = 0; i < basicAuthSchemes.length; i++) {
const basicAuthScheme = basicAuthSchemes[i];
if (basicAuthScheme == null) {
const needsControlFlow = isAuthOptional || resolvedBasicAuthSchemes.length > 1;
for (let i = 0; i < resolvedBasicAuthSchemes.length; i++) {
const resolved = resolvedBasicAuthSchemes[i];
if (resolved == null) {
continue;
}
const usernameName = this.context.getParameterName(basicAuthScheme.username);
const passwordName = this.context.getParameterName(basicAuthScheme.password);
if (isAuthOptional || basicAuthSchemes.length > 1) {
const controlFlowKeyword = i === 0 ? "if" : "else if";
writer.controlFlow(
controlFlowKeyword,
php.codeblock(`$${usernameName} !== null && $${passwordName} !== null`)
);
const { condition, credentialExpr } = resolved;
if (needsControlFlow) {
writer.controlFlow(i === 0 ? "if" : "else if", php.codeblock(condition));
}
writer.writeLine(
`$defaultHeaders['Authorization'] = "Basic " . base64_encode($${usernameName} . ":" . $${passwordName});`
`$defaultHeaders['Authorization'] = "Basic " . base64_encode(${credentialExpr});`
);
if (isAuthOptional || basicAuthSchemes.length > 1) {
if (needsControlFlow) {
writer.endControlFlow();
}
}
Expand Down Expand Up @@ -605,8 +604,12 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
case "basic": {
const username = this.context.getParameterName(scheme.username);
const password = this.context.getParameterName(scheme.password);
return [
{
// When omit is true, the field is completely removed from the end-user API.
const usernameOmitted = !!scheme.usernameOmit;
const passwordOmitted = !!scheme.passwordOmit;
const params: ConstructorParameter[] = [];
if (!usernameOmitted) {
params.push({
name: username,
docs: this.getAuthParameterDocs({ docs: scheme.docs, name: username }),
isOptional,
Expand All @@ -616,19 +619,22 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
isOptional
}),
environmentVariable: scheme.usernameEnvVar
},
{
});
}
if (!passwordOmitted) {
params.push({
name: password,
docs: this.getAuthParameterDocs({ docs: scheme.docs, name: username }),
docs: this.getAuthParameterDocs({ docs: scheme.docs, name: password }),
isOptional,
typeReference: this.getAuthParameterTypeReference({
typeReference: STRING_TYPE_REFERENCE,
envVar: scheme.passwordEnvVar,
isOptional
}),
environmentVariable: scheme.passwordEnvVar
}
];
});
}
return params;
}
case "header": {
const name = this.context.getParameterName(scheme.name);
Expand Down Expand Up @@ -755,6 +761,46 @@ export class RootClientGenerator extends FileGenerator<PhpFile, SdkCustomConfigS
return docs ?? `The ${name} to use for authentication.`;
}

/**
* Resolves a basic auth scheme into its null-check condition and credential expressions,
* accounting for omitted username/password fields. Returns undefined if both fields are omitted.
*/
private resolveBasicAuthScheme(
scheme: FernIr.AuthScheme & { type: "basic" }
): { condition: string; credentialExpr: string } | undefined {
const usernameName = this.context.getParameterName(scheme.username);
const passwordName = this.context.getParameterName(scheme.password);
const usernameOmitted = !!scheme.usernameOmit;
const passwordOmitted = !!scheme.passwordOmit;

if (usernameOmitted && passwordOmitted) {
return undefined;
}

const conditions: string[] = [];
if (!usernameOmitted) {
conditions.push(`$${usernameName} !== null`);
}
if (!passwordOmitted) {
conditions.push(`$${passwordName} !== null`);
}

// Build a clean credential expression without redundant empty-string concatenation.
let credentialExpr: string;
if (usernameOmitted) {
credentialExpr = `":" . $${passwordName}`;
} else if (passwordOmitted) {
credentialExpr = `$${usernameName} . ":"`;
} else {
credentialExpr = `$${usernameName} . ":" . $${passwordName}`;
}

return {
condition: conditions.join(" && "),
credentialExpr
};
}

private getRootSubpackages(): FernIr.Subpackage[] {
return this.context.ir.rootPackage.subpackages
.map((subpackageId) => {
Expand Down
10 changes: 7 additions & 3 deletions generators/php/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,13 @@ export class WireTestGenerator {
bearer: () => {
authParams.push("token: 'test-token'");
},
basic: () => {
authParams.push("username: 'test-user'");
authParams.push("password: 'test-password'");
basic: (basicScheme) => {
if (!basicScheme.usernameOmit) {
authParams.push("username: 'test-username'");
}
if (!basicScheme.passwordOmit) {
authParams.push("password: 'test-password'");
}
},
header: (header) => {
const paramName = this.case.camelSafe(header.name);
Expand Down
12 changes: 12 additions & 0 deletions generators/php/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: 2.4.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-02"
irVersion: 66

- version: 2.3.2-rc.0
changelogEntry:
- summary: |
Expand Down

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

5 changes: 5 additions & 0 deletions seed/php-sdk/basic-auth-pw-omitted/wire-tests/.gitignore

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

145 changes: 145 additions & 0 deletions seed/php-sdk/basic-auth-pw-omitted/wire-tests/README.md

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

Loading
Loading