From a11c9c0fed2fec6130372b8a66a3ff718ab481d5 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:16:51 -0700 Subject: [PATCH] Fix auth-method dialog overflow with long API keys A long unbroken key pasted as a placement prefix stretched the add-method dialog (preview line) and the connect modal (merged credential affix) past the viewport. Wrap the placement preview mid-token and truncate the merged affix inside the field. --- .../connect-modal-credential-ux.test.ts | 45 ++++++++++++++++++- .../src/components/add-account-modal.tsx | 6 ++- packages/react/src/lib/auth-placements.tsx | 6 ++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/e2e/selfhost/connect-modal-credential-ux.test.ts b/e2e/selfhost/connect-modal-credential-ux.test.ts index 0dd631f74..1c994cfce 100644 --- a/e2e/selfhost/connect-modal-credential-ux.test.ts +++ b/e2e/selfhost/connect-modal-credential-ux.test.ts @@ -3,10 +3,14 @@ // it reads as the header value being built ("Authorization: Bearer ▏token"); // - the "Add authentication method" editor offers placement presets; // - a prefix with no trailing space (sent joined to the value) warns, and the -// warning clears once the space is restored. +// warning clears once the space is restored; +// - a very long pasted key wraps inside the dialog instead of stretching it +// past the viewport (the preview breaks mid-token, the merged affix +// truncates). // Video is the artifact. import { randomBytes } from "node:crypto"; +import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { composePluginApi } from "@executor-js/api/server"; import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; @@ -17,6 +21,16 @@ import { Api, Browser, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin()] as const); +/** Horizontal overflow of the open dialog in px (0 = fits). Infinity when no + * dialog is open, so an assertion against it always fails loudly. */ +const dialogOverflow = (page: { + readonly evaluate: (fn: () => T) => Promise; +}): Promise => + page.evaluate(() => { + const dialog = document.querySelector('[role="dialog"]'); + return dialog === null ? Number.POSITIVE_INFINITY : dialog.scrollWidth - dialog.clientWidth; + }); + const bearerSpec = (): string => JSON.stringify({ openapi: "3.0.3", @@ -75,6 +89,35 @@ scenario( await page.getByPlaceholder("Bearer ").first().fill("Bearer "); await page.getByText("Prefix has no trailing space").waitFor({ state: "detached" }); }); + + await step("A long pasted key never widens the dialog", async () => { + // A key mistakenly pasted into Prefix is the worst case: one + // unbroken 700-char token rendered into the preview line. The + // dialog must keep its width (the preview wraps mid-token). + const longKey = `${"x".repeat(300)}Bearer${"x".repeat(400)}`; + // The preview line renders only once the placement is named. + await page.getByPlaceholder("Authorization").first().fill("Authorization"); + await page.getByPlaceholder("Bearer ").first().fill(longKey); + await page.getByText("Preview").waitFor(); + // ≤ 1px: subpixel rounding can report a fractional scrollWidth. + expect( + await dialogOverflow(page), + "the add-method dialog must not scroll horizontally", + ).toBeLessThanOrEqual(1); + }); + + await step("The merged affix truncates a long saved prefix", async () => { + // Saving the method returns to the connect modal with the new + // method selected; its credential field merges the (700-char) + // prefix as the affix, which must truncate inside the field. + await page.getByRole("button", { name: "Add method", exact: true }).click(); + await page.getByRole("heading", { name: /Add connection/ }).waitFor(); + await page.getByText("Authorization: x").first().waitFor(); + expect( + await dialogOverflow(page), + "the connect modal must not scroll horizontally", + ).toBeLessThanOrEqual(1); + }); }); }), apiClient.openapi diff --git a/packages/react/src/components/add-account-modal.tsx b/packages/react/src/components/add-account-modal.tsx index 7259b199c..4fa5aea16 100644 --- a/packages/react/src/components/add-account-modal.tsx +++ b/packages/react/src/components/add-account-modal.tsx @@ -270,8 +270,10 @@ function PasteCredentialInputs(props: { // from offering to GENERATE a password here; the app's own 1Password // picker covers filling an existing secret.
- - {props.affix} + {/* min-w-0 + inner ellipsis: a method saved with a very long + prefix must truncate inside the field, not stretch it. */} + + {props.affix} {/* oxlint-disable-next-line react/forbid-elements */} + {lead} {placement.prefix ? ( {placement.prefix}