}
@@ -152,10 +159,17 @@ export function createCredentialPayloadOrigin(args: {
};
}
- const values = Object.fromEntries(
- args.inputs.map((input) => [input.variable, (args.values[input.variable] ?? "").trim()]),
+ const entries = args.inputs.map(
+ (input) => [input, (args.values[input.variable] ?? "").trim()] as const,
);
- return Object.values(values).every((value) => value.length > 0) ? { values } : null;
+ if (entries.some(([input, value]) => input.optional !== true && value.length === 0)) return null;
+ return {
+ values: Object.fromEntries(
+ entries
+ .filter(([, value]) => value.length > 0)
+ .map(([input, value]) => [input.variable, value]),
+ ),
+ };
}
const numberBadge = (n: number) => (
@@ -211,6 +225,7 @@ function PasteCredentialInputs(props: {
// to see all of it, not toggle per field.
const [revealed, setRevealed] = useState(false);
const inputType = revealed ? "text" : "password";
+ const hasSecretInput = props.inputs.some((input) => input.secret !== false);
if (!props.singleInput) {
return (
@@ -222,12 +237,17 @@ function PasteCredentialInputs(props: {
className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"
>
{input.label}
+ {input.optional === true ? " (optional)" : ""}
+ {input.description ? (
+ {input.description}
+ ) : null}
- setRevealed((v) => !v)} />
+ {input.secret === false ? null : (
+ setRevealed((v) => !v)} />
+ )}
))}
@@ -259,8 +281,14 @@ function PasteCredentialInputs(props: {
{props.inputs.map((input) => (
{labelled && (
-
+
)}
+ {input.description ? (
+
{input.description}
+ ) : null}
{props.affix ? (
// Merged field: the placement's lead + prefix is a FIXED addon (its
// own muted segment, divider, non-selectable), and the user types
@@ -277,7 +305,7 @@ function PasteCredentialInputs(props: {
{/* oxlint-disable-next-line react/forbid-elements */}
-
setRevealed((v) => !v)} />
+ {input.secret === false ? null : (
+ setRevealed((v) => !v)} />
+ )}
) : (
- setRevealed((v) => !v)} />
+ {input.secret === false || !hasSecretInput ? null : (
+ setRevealed((v) => !v)} />
+ )}
)}
@@ -1442,6 +1475,9 @@ function AddAccountModalView(props: AddAccountModalProps) {
// a variable collapse to one input.
const credentialInputs = useMemo(() => {
if (!method || method.kind === "oauth" || method.kind === "none") return [];
+ if (method.credentialInputs && method.credentialInputs.length > 0) {
+ return method.credentialInputs;
+ }
const byVar = new Map();
for (const placement of method.placements) {
const variable = placement.variable ?? "token";
diff --git a/packages/react/src/lib/auth-placements.test.ts b/packages/react/src/lib/auth-placements.test.ts
index f31ca38ade..77540f2b12 100644
--- a/packages/react/src/lib/auth-placements.test.ts
+++ b/packages/react/src/lib/auth-placements.test.ts
@@ -81,6 +81,26 @@ describe("authMethodsFromDescriptors", () => {
]);
});
+ it("preserves strategy credential inputs without inventing HTTP placements", () => {
+ const methods = authMethodsFromDescriptors([
+ {
+ id: "aws_iam",
+ label: "AWS IAM role",
+ kind: "apikey",
+ template: "aws_iam",
+ credentialInputs: [
+ { variable: "access_key_id", label: "Access key ID", secret: false },
+ { variable: "session_token", label: "Session token", optional: true },
+ ],
+ },
+ ]);
+ expect(methods[0]?.placements).toEqual([]);
+ expect(methods[0]?.credentialInputs).toEqual([
+ { variable: "access_key_id", label: "Access key ID", secret: false },
+ { variable: "session_token", label: "Session token", optional: true },
+ ]);
+ });
+
it("keeps `none` methods as no-input connection methods", () => {
const methods = authMethodsFromDescriptors([
{ id: "none", label: "No auth", kind: "none", template: "none" },
diff --git a/packages/react/src/lib/auth-placements.tsx b/packages/react/src/lib/auth-placements.tsx
index e61c307af4..4cbbadabda 100644
--- a/packages/react/src/lib/auth-placements.tsx
+++ b/packages/react/src/lib/auth-placements.tsx
@@ -16,7 +16,10 @@
// ---------------------------------------------------------------------------
import { AuthTemplateSlug } from "@executor-js/sdk/shared";
-import type { AuthMethodDescriptor } from "@executor-js/sdk/shared";
+import type {
+ AuthMethodCredentialInputDescriptor,
+ AuthMethodDescriptor,
+} from "@executor-js/sdk/shared";
export type Carrier = "header" | "query" | "env";
@@ -83,6 +86,7 @@ export interface AuthMethod {
readonly source: "spec" | "custom";
readonly template: AuthTemplateSlug;
readonly placements: readonly Placement[];
+ readonly credentialInputs?: readonly AuthMethodCredentialInputDescriptor[];
/** Declared OAuth endpoints/scopes (only for `kind === "oauth"`). */
readonly oauth?: AuthMethodOAuth;
}
@@ -194,7 +198,9 @@ function authMethodFromDescriptor(descriptor: AuthMethodDescriptor): AuthMethod
...(placement.literal !== undefined ? { literal: placement.literal } : {}),
}),
)
- : DEFAULT_PLACEMENTS;
+ : descriptor.credentialInputs && descriptor.credentialInputs.length > 0
+ ? []
+ : DEFAULT_PLACEMENTS;
return {
id: descriptor.id,
label: descriptor.label,
@@ -202,6 +208,7 @@ function authMethodFromDescriptor(descriptor: AuthMethodDescriptor): AuthMethod
source: "spec",
template,
placements,
+ ...(descriptor.credentialInputs ? { credentialInputs: descriptor.credentialInputs } : {}),
};
}