Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d964ecb
feat(ts-sdk): support optional username/password in basic auth when c…
Swimburger Mar 31, 2026
34f45a3
fix(ts-sdk): use per-field omit checks instead of coarse eitherOmitte…
Swimburger Apr 1, 2026
753d586
fix(ts-sdk): fix biome formatting for null check lines
Swimburger Apr 1, 2026
0dad189
fix(ts-sdk): regenerate seed output for basic-auth-optional after per…
Swimburger Apr 1, 2026
a876e08
fix(ts-sdk): update snapshot tests for per-field null check format
Swimburger Apr 1, 2026
04da60e
merge: resolve versions.yml conflict with main (bump to 3.61.1)
Swimburger Apr 1, 2026
fab2111
Merge remote-tracking branch 'origin/main' into devin/1774997664-basi…
devin-ai-integration[bot] Apr 1, 2026
de92ad0
fix(ts-sdk): remove omitted fields entirely from AuthOptions type, us…
Swimburger Apr 2, 2026
5063e6b
fix(ts-sdk): preserve short-circuit evaluation order in buildNullChecks
Swimburger Apr 2, 2026
95c8bf8
fix(ts-sdk): regenerate seed output with interleaved null checks
Swimburger Apr 2, 2026
caebd4a
fix(ts-sdk): update snapshot tests for interleaved null checks and li…
Swimburger Apr 2, 2026
9628f06
merge: resolve versions.yml conflict with main (add 3.60.9 entry)
Swimburger Apr 2, 2026
309a34a
fix(ts-sdk): skip omitted auth fields in wire test generator getAuthC…
Swimburger Apr 2, 2026
a37a0a5
merge: resolve versions.yml conflict with main (bump to 3.61.2)
Swimburger Apr 2, 2026
7941e22
fix(ts-sdk): bump irVersion from 65 to 66 in versions.yml entry
Swimburger Apr 2, 2026
d886376
fix(ts-sdk): correct createdAt date to 2026-04-02 in versions.yml
Swimburger Apr 2, 2026
07fa558
Merge remote-tracking branch 'origin/main' into devin/1774997664-basi…
Swimburger Apr 2, 2026
dfdd31d
merge: resolve versions.yml conflict with main (bump to 3.62.1)
Swimburger Apr 2, 2026
c2fd811
fix(ts-sdk): skip omitted fields in snippet properties and regenerate…
Swimburger Apr 2, 2026
d2040a9
merge: resolve versions.yml conflict with main (bump to 3.62.2)
Swimburger Apr 3, 2026
d335ffa
fix(ts-sdk): use 'omit' instead of 'optional' in versions.yml changelog
Swimburger Apr 3, 2026
f193396
refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted
Swimburger Apr 3, 2026
a62122c
fix(ts-sdk): restore computed property keys [USERNAME_PARAM]/[PASSWOR…
Swimburger Apr 3, 2026
a046153
fix(ts-sdk): bump version to 3.63.0 (feat requires minor bump)
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
Original file line number Diff line number Diff line change
Expand Up @@ -136,20 +136,11 @@ export class BasicAuthProvider implements core.AuthProvider {
const username =
(await core.Supplier.get(this.options[USERNAME_PARAM], { endpointMetadata })) ??
process.env?.[ENV_USERNAME];
if (username == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME,
});
}

if (username == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME }); }
const password =
(await core.Supplier.get(this.options[PASSWORD_PARAM], { endpointMetadata })) ??
process.env?.[ENV_PASSWORD];
if (password == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD,
});
}
if (password == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD }); }

const authHeader = core.BasicAuth.toAuthorizationHeader(username, password);

Expand Down Expand Up @@ -195,14 +186,9 @@ export class BasicAuthProvider implements core.AuthProvider {
} = {}): Promise<core.AuthRequest> {

const username = await core.Supplier.get(this.options[USERNAME_PARAM], { endpointMetadata });
if (username == null) {
return { headers: {} };
}

if (username == null) { return { headers: {} }; }
const password = await core.Supplier.get(this.options[PASSWORD_PARAM], { endpointMetadata });
if (password == null) {
return { headers: {} };
}
if (password == null) { return { headers: {} }; }

const authHeader = core.BasicAuth.toAuthorizationHeader(username, password);

Expand Down Expand Up @@ -249,18 +235,9 @@ export class BasicAuthProvider implements core.AuthProvider {
} = {}): Promise<core.AuthRequest> {

const username = await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[USERNAME_PARAM], { endpointMetadata });
if (username == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME,
});
}

if (username == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME }); }
const password = await core.Supplier.get(this.options[WRAPPER_PROPERTY]?.[PASSWORD_PARAM], { endpointMetadata });
if (password == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD,
});
}
if (password == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD }); }

const authHeader = core.BasicAuth.toAuthorizationHeader(username, password);

Expand Down Expand Up @@ -308,18 +285,9 @@ export class BasicAuthProvider implements core.AuthProvider {
} = {}): Promise<core.AuthRequest> {

const username = await core.Supplier.get(this.options[USERNAME_PARAM], { endpointMetadata });
if (username == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME,
});
}

if (username == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_USERNAME }); }
const password = await core.Supplier.get(this.options[PASSWORD_PARAM], { endpointMetadata });
if (password == null) {
throw new errors.SeedApiError({
message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD,
});
}
if (password == null) { throw new errors.SeedApiError({ message: BasicAuthProvider.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD }); }

const authHeader = core.BasicAuth.toAuthorizationHeader(username, password);

Expand Down
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.

🟡 Generator emits unused USERNAME_PARAM/PASSWORD_PARAM constants for omitted auth fields

In writeConstants at BasicAuthProviderGenerator.ts:159-170, both USERNAME_PARAM and PASSWORD_PARAM are always emitted regardless of omit flags. When a field is omitted (e.g. passwordOmit: true), the corresponding constant is never referenced in canCreate, getAuthRequest, or the AuthOptions type — it becomes dead code.

The generated seed output at seed/ts-sdk/basic-auth-optional/src/auth/BasicAuthProvider.ts:7 confirms this: const _PASSWORD_PARAM = "password" as const; — the linter renamed it with an underscore prefix because it's unused. The fix would be to conditionally skip generating the constant when the corresponding omit flag is true.

(Refers to lines 169-170)

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.

Valid observation about the unused constant. This is a minor issue — the generated biome config already renames it with an underscore prefix (_PASSWORD_PARAM), and the constant is still referenced in the module namespace for the AUTH_CONFIG_ERROR_MESSAGE_PASSWORD string template in the non-omit case. Conditionally skipping the constant would require tracking which fields are omitted across both writeConstants and writeOptions, adding complexity for a cosmetic improvement. I'll leave this as-is unless the maintainer wants it addressed.

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.

🟡 Generator emits unused PASSWORD_PARAM/USERNAME_PARAM constants for omitted basic auth fields

When usernameOmit or passwordOmit is true, writeConstants() at BasicAuthProviderGenerator.ts:169-170 still unconditionally emits the USERNAME_PARAM / PASSWORD_PARAM constant declarations. However, generateCanCreateStatements() (line 269-274), generateGetAuthRequestStatements() (line 333-343), and writeOptions() (line 441-443) all correctly skip referencing the omitted field's constant — meaning the emitted constant is never used in the generated output.

This is confirmed by the generated seed output at seed/ts-sdk/basic-auth-optional/src/auth/BasicAuthProvider.ts:7, where biome's linter renames the unused constant from PASSWORD_PARAM to _PASSWORD_PARAM. While this doesn't cause a runtime failure, it produces unnecessary declarations that trigger linter warnings or auto-renames in generated SDKs.

(Refers to lines 169-170)

Prompt for agents
In writeConstants() in BasicAuthProviderGenerator.ts, the USERNAME_PARAM and PASSWORD_PARAM constants are emitted unconditionally at lines 169-170. When usernameOmit or passwordOmit is true, the corresponding constant is never referenced by any other generated code (canCreate, getAuthRequest, writeOptions all skip omitted fields). The fix is to conditionally emit each constant only when the field is not omitted. Read this.authScheme.usernameOmit and this.authScheme.passwordOmit (as done in other methods like generateCanCreateStatements) and wrap each constants.push() call in an if (!omit) guard. This aligns with the pattern already used throughout the rest of the class.
Open in Devin Review

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

Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
public getAuthOptionsProperties(context: FileContext): OptionalKind<PropertySignatureStructure>[] | undefined {
const hasUsernameEnv = this.authScheme.usernameEnvVar != null;
const hasPasswordEnv = this.authScheme.passwordEnvVar != null;
const usernameOmit = this.authScheme.usernameOmit === true;
const passwordOmit = this.authScheme.passwordOmit === true;
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
const isUsernameOptional = !this.isAuthMandatory || hasUsernameEnv;
const isPasswordOptional = !this.isAuthMandatory || hasPasswordEnv;

Expand All @@ -99,7 +101,6 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
const stringType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
const supplierType = context.coreUtilities.fetcher.SupplierOrEndpointSupplier._getReferenceToType(stringType);

// For env var fallback: prop?: Supplier<T> | undefined
const usernamePropertyType = hasUsernameEnv
? ts.factory.createUnionTypeNode([
supplierType,
Expand All @@ -114,22 +115,31 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
])
: supplierType;

return [
{
// When omit is true, the field is completely removed from the end-user API.
// Internally, the omitted field is treated as an empty string.
const properties: OptionalKind<PropertySignatureStructure>[] = [];

if (!usernameOmit) {
properties.push({
kind: StructureKind.PropertySignature,
name: getPropertyKey(context.case.camelSafe(this.authScheme.username)),
hasQuestionToken: isUsernameOptional,
type: getTextOfTsNode(usernamePropertyType),
docs: this.authScheme.docs ? [this.authScheme.docs] : undefined
},
{
});
}

if (!passwordOmit) {
properties.push({
kind: StructureKind.PropertySignature,
name: getPropertyKey(context.case.camelSafe(this.authScheme.password)),
hasQuestionToken: isPasswordOptional,
type: getTextOfTsNode(passwordPropertyType),
docs: this.authScheme.docs ? [this.authScheme.docs] : undefined
}
];
});
}

return properties;
}

public instantiate(constructorArgs: ts.Expression[]): ts.Expression {
Expand Down Expand Up @@ -249,18 +259,29 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
const usernameEnvVar = this.authScheme.usernameEnvVar;
const passwordEnvVar = this.authScheme.passwordEnvVar;
const wrapperAccess = this.keepIfWrapper("[WRAPPER_PROPERTY]?.");
const usernameOmit = this.authScheme.usernameOmit === true;
const passwordOmit = this.authScheme.passwordOmit === true;

const usernameEnvCheck = usernameEnvVar != null ? " || process.env?.[ENV_USERNAME] != null" : "";
const passwordEnvCheck = passwordEnvVar != null ? " || process.env?.[ENV_PASSWORD] != null" : "";

return `return (options?.${wrapperAccess}[USERNAME_PARAM] != null${usernameEnvCheck}) && (options?.${wrapperAccess}[PASSWORD_PARAM] != null${passwordEnvCheck});`;
// Per-field checks: omittable fields are always satisfied, required fields must be present
const usernameCheck = usernameOmit
? "true"
: `options?.${wrapperAccess}[USERNAME_PARAM] != null${usernameEnvCheck}`;
const passwordCheck = passwordOmit
? "true"
: `options?.${wrapperAccess}[PASSWORD_PARAM] != null${passwordEnvCheck}`;
return `return (${usernameCheck}) && (${passwordCheck});`;
}

private generateGetAuthRequestStatements(context: FileContext): string {
const usernameVar = context.case.camelUnsafe(this.authScheme.username);
const passwordVar = context.case.camelUnsafe(this.authScheme.password);
const usernameEnvVar = this.authScheme.usernameEnvVar;
const passwordEnvVar = this.authScheme.passwordEnvVar;
const usernameOmit = this.authScheme.usernameOmit === true;
const passwordOmit = this.authScheme.passwordOmit === true;

// Build property access chain based on shouldUseWrapper
const thisOptionsAccess = ts.factory.createPropertyAccessExpression(ts.factory.createThis(), "options");
Expand Down Expand Up @@ -308,67 +329,66 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
);
const passwordSupplierGetCode = getTextOfTsNode(passwordSupplierGetCall);

const usernameEnvFallback =
usernameEnvVar != null
? `\n (${usernameSupplierGetCode}) ??\n process.env?.[ENV_USERNAME]`
: usernameSupplierGetCode;

const passwordEnvFallback =
passwordEnvVar != null
? `\n (${passwordSupplierGetCode}) ??\n process.env?.[ENV_PASSWORD]`
: passwordSupplierGetCode;

if (this.neverThrowErrors) {
// When neverThrowErrors is true, return empty headers if credentials are missing
return `
const ${usernameVar} = ${usernameEnvFallback};
if (${usernameVar} == null) {
return { headers: {} };
}

const ${passwordVar} = ${passwordEnvFallback};
if (${passwordVar} == null) {
return { headers: {} };
}
// When a field is omitted, use empty string directly instead of reading from options
const usernameEnvFallback = usernameOmit
? '""'
: usernameEnvVar != null
? `\n (${usernameSupplierGetCode}) ??\n process.env?.[ENV_USERNAME]`
: usernameSupplierGetCode;

const passwordEnvFallback = passwordOmit
? '""'
: passwordEnvVar != null
? `\n (${passwordSupplierGetCode}) ??\n process.env?.[ENV_PASSWORD]`
: passwordSupplierGetCode;

// Build per-field null checks based on individual omit flags.
// Interleave declaration and null-check to preserve short-circuit evaluation:
// if username is null, the password supplier is never evaluated.
const buildNullChecks = (errorAction: string): string => {
const lines: string[] = [];
lines.push(`const ${usernameVar} = ${usernameEnvFallback};`);
if (!usernameOmit) {
lines.push(
`if (${usernameVar} == null) { ${errorAction.replace("__MSG__", `${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE_USERNAME`)} }`
);
}
lines.push(`const ${passwordVar} = ${passwordEnvFallback};`);
if (!passwordOmit) {
lines.push(
`if (${passwordVar} == null) { ${errorAction.replace("__MSG__", `${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD`)} }`
);
}
return lines.map((l) => ` ${l}`).join("\n");
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
};

const authHeader = ${getTextOfTsNode(
const authHeaderCode = getTextOfTsNode(
context.coreUtilities.auth.BasicAuth.toAuthorizationHeader(
ts.factory.createIdentifier(usernameVar),
ts.factory.createIdentifier(passwordVar)
)
)};
);

if (this.neverThrowErrors) {
const errorAction = "return { headers: {} };";
return `
${buildNullChecks(errorAction)}

const authHeader = ${authHeaderCode};

return {
headers: authHeader != null ? { Authorization: authHeader } : {},
};
`;
} else {
// When neverThrowErrors is false, throw an error if credentials are missing
const errorConstructor = getTextOfTsNode(
context.genericAPISdkError.getReferenceToGenericAPISdkError().getExpression()
);

const errorAction = `throw new ${errorConstructor}({ message: __MSG__ });`;
return `
const ${usernameVar} = ${usernameEnvFallback};
if (${usernameVar} == null) {
throw new ${errorConstructor}({
message: ${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE_USERNAME,
});
}

const ${passwordVar} = ${passwordEnvFallback};
if (${passwordVar} == null) {
throw new ${errorConstructor}({
message: ${CLASS_NAME}.AUTH_CONFIG_ERROR_MESSAGE_PASSWORD,
});
}
${buildNullChecks(errorAction)}

const authHeader = ${getTextOfTsNode(
context.coreUtilities.auth.BasicAuth.toAuthorizationHeader(
ts.factory.createIdentifier(usernameVar),
ts.factory.createIdentifier(passwordVar)
)
)};
const authHeader = ${authHeaderCode};

return {
headers: authHeader != null ? { Authorization: authHeader } : {},
Expand All @@ -384,37 +404,59 @@ export class BasicAuthProviderGenerator implements AuthProviderGenerator {
"BasicAuth";
const usernameEnvVar = this.authScheme.usernameEnvVar;
const passwordEnvVar = this.authScheme.passwordEnvVar;
const usernameOmit = this.authScheme.usernameOmit === true;
const passwordOmit = this.authScheme.passwordOmit === true;

const statements: (string | WriterFunction | StatementStructures)[] = [
`export const AUTH_SCHEME = "${authSchemeKey}" as const;`,
`export const AUTH_CONFIG_ERROR_MESSAGE: string = "Please provide username and password when initializing the client" as const;`
];

// Add AUTH_CONFIG_ERROR_MESSAGE constants for username and password
if (usernameEnvVar != null) {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_USERNAME: string = \`Please provide '\${USERNAME_PARAM}' when initializing the client, or set the '\${ENV_USERNAME}' environment variable\` as const;`
);
} else {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_USERNAME: string = \`Please provide '\${USERNAME_PARAM}' when initializing the client\` as const;`
);
// Add AUTH_CONFIG_ERROR_MESSAGE constants only for non-omitted fields
if (!usernameOmit) {
if (usernameEnvVar != null) {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_USERNAME: string = \`Please provide '\${USERNAME_PARAM}' when initializing the client, or set the '\${ENV_USERNAME}' environment variable\` as const;`
);
} else {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_USERNAME: string = \`Please provide '\${USERNAME_PARAM}' when initializing the client\` as const;`
);
}
}

if (passwordEnvVar != null) {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_PASSWORD: string = \`Please provide '\${PASSWORD_PARAM}' when initializing the client, or set the '\${ENV_PASSWORD}' environment variable\` as const;`
);
} else {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_PASSWORD: string = \`Please provide '\${PASSWORD_PARAM}' when initializing the client\` as const;`
);
if (!passwordOmit) {
if (passwordEnvVar != null) {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_PASSWORD: string = \`Please provide '\${PASSWORD_PARAM}' when initializing the client, or set the '\${ENV_PASSWORD}' environment variable\` as const;`
);
} else {
statements.push(
`export const AUTH_CONFIG_ERROR_MESSAGE_PASSWORD: string = \`Please provide '\${PASSWORD_PARAM}' when initializing the client\` as const;`
);
}
}

// Generate AuthOptions type based on keepIfWrapper
const usernamePropertyDef = `[USERNAME_PARAM]${authOptionsProperties[0]?.hasQuestionToken ? "?" : ""}: ${authOptionsProperties[0]?.type}`;
const passwordPropertyDef = `[PASSWORD_PARAM]${authOptionsProperties[1]?.hasQuestionToken ? "?" : ""}: ${authOptionsProperties[1]?.type}`;
const propertyDefs = `${usernamePropertyDef}; ${passwordPropertyDef}`;
// Generate AuthOptions type based on keepIfWrapper — omitted fields are excluded
// Use computed property keys ([USERNAME_PARAM], [PASSWORD_PARAM]) instead of literal names
const computedPropertyDefs: string[] = [];
if (!usernameOmit) {
const isUsernameOptional = !this.isAuthMandatory || usernameEnvVar != null;
const optMark = isUsernameOptional ? "?" : "";
const usernameType = authOptionsProperties.find(
(p) => p.name === getPropertyKey(context.case.camelSafe(this.authScheme.username))
)?.type;
computedPropertyDefs.push(`[USERNAME_PARAM]${optMark}: ${usernameType}`);
}
if (!passwordOmit) {
const isPasswordOptional = !this.isAuthMandatory || passwordEnvVar != null;
const optMark = isPasswordOptional ? "?" : "";
const passwordType = authOptionsProperties.find(
(p) => p.name === getPropertyKey(context.case.camelSafe(this.authScheme.password))
)?.type;
computedPropertyDefs.push(`[PASSWORD_PARAM]${optMark}: ${passwordType}`);
}
const propertyDefs = computedPropertyDefs.join("; ");

// When wrapped (multiple auth schemes), the wrapper property should be optional
// When not wrapped, individual fields already have their own ?: markers based on env vars
Expand Down
Loading
Loading