fix(connections): preserve managed defaults before enrollment

This commit is contained in:
Dotta 2026-09-05 08:05:31 -05:00
parent 762dbe4d7e
commit 8bac89b1a6
2 changed files with 69 additions and 5 deletions

View File

@ -315,6 +315,17 @@ function recommendedSetupConnectionMethod(
: null;
}
function recommendedManagedConnectorMethod(
entry: AppDefinition | null | undefined,
): ConnectionMethodDef | null {
return recommendedSetupConnectionMethod(
(entry?.methods ?? []).filter((candidate) =>
candidate.oauthStrategy === "paperclip_cloud_connector"
|| candidate.oauthStrategy === "paperclip_id_connector",
),
);
}
function canUseAutomaticOAuthFastPath(entry: AppDefinition | null | undefined): boolean {
if (!entry) return false;
const methods = getAvailableConnectionMethods(entry);
@ -745,6 +756,16 @@ export function ConnectionSetupFlow({
|| candidate.oauthStrategy === "paperclip_id_connector"
),
);
// Before a self-hosted instance enrolls, the server intentionally withholds
// platform-managed methods from the advertised gallery. The setup route still
// needs the managed method's identity model, labels, and defaults because the
// next step is enrollment for that exact method—not the visible PAT/BYO
// compatibility fallback.
const preEnrollmentManagedMethod = entry
&& requestedDefinitionUsesManagedConnector
&& !entryAdvertisesManagedConnector
? recommendedManagedConnectorMethod(fullRequestedDefinition)
: null;
const connectorEnrollmentQuery = useQuery({
queryKey: ["cloud-connector", "enrollment"],
queryFn: () => toolsApi.getCloudConnectorEnrollment(),
@ -1174,7 +1195,15 @@ export function ConnectionSetupFlow({
setCuratedOAuthClientId("");
setCuratedOAuthClientSecret("");
setVercelConnector("");
const initialMethod = recommendedSetupConnectionMethod(methods);
const requestedEntryAdvertisesManagedConnector = requestedEntry.methods.some((candidate) =>
candidate.oauthStrategy === "paperclip_cloud_connector"
|| candidate.oauthStrategy === "paperclip_id_connector"
);
const initialMethod = (
requestedDefinitionUsesManagedConnector && !requestedEntryAdvertisesManagedConnector
? recommendedManagedConnectorMethod(fullRequestedDefinition)
: null
) ?? recommendedSetupConnectionMethod(methods);
setConnectionMethodKey(initialMethod?.key ?? "");
setConfigValues(defaultMethodConfig(initialMethod));
setGoogleSheetsLinks("");
@ -1222,6 +1251,7 @@ export function ConnectionSetupFlow({
reconnectConnectionId,
reconnectSourceMatches,
resumeConnectionId,
fullRequestedDefinition,
requestedAppKey,
requestedAgentId,
routeStage,
@ -1698,6 +1728,9 @@ export function ConnectionSetupFlow({
entry?.name ??
(linkName.trim() || defaultGenericMcpName(linkUrl) || "this app");
const credentialSourceMethods = connectionMethodsForCredentialSource(entry, credentialSource);
const setupCredentialSourceMethods = preEnrollmentManagedMethod
? [preEnrollmentManagedMethod]
: credentialSourceMethods;
const credentialSourceApps = vercelConnectMode
? (galleryQuery.data?.apps ?? []).filter(
(app) => connectionMethodsForCredentialSource(app, credentialSource).length > 0,
@ -1708,9 +1741,9 @@ export function ConnectionSetupFlow({
: null;
const stepLabels = zapierSource
? ZAPIER_STEP_LABELS
: entry && credentialSourceMethods.length > 1
: entry && setupCredentialSourceMethods.length > 1
? ["Access", "Choose connection"]
: entry && credentialSourceMethods[0]?.auth === "oauth"
: entry && setupCredentialSourceMethods[0]?.auth === "oauth"
? ["Access", "Sign in"]
: isGoogleSheetsRobotMethod(entry, connectionMethodKey)
? ["Access", "Share sheet"]
@ -1721,8 +1754,8 @@ export function ConnectionSetupFlow({
// credential, so it reads the selected method's auth kind.
const accessStepMethod = entry
? (connectionMethodKey
? credentialSourceMethods.find((m) => m.key === connectionMethodKey) ?? null
: credentialSourceMethods[0] ?? null)
? setupCredentialSourceMethods.find((m) => m.key === connectionMethodKey) ?? null
: setupCredentialSourceMethods[0] ?? null)
: null;
const accessStepAuthKind: ToolConnectionAuthKind = entry
? accessStepMethod?.auth ?? "none"

View File

@ -725,6 +725,37 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
);
});
it("keeps GitHub's personal identity defaults while its managed method awaits enrollment", async () => {
mockParams.appKey = "github";
listGalleryMock.mockResolvedValue({
apps: [{
...GITHUB,
methods: GITHUB.methods.filter((method) => !method.oauthStrategy),
ownershipAvailability: { platform_shared: false, customer: true, dcr: true },
}],
});
getCloudConnectorEnrollmentMock.mockResolvedValueOnce({
configured: false,
status: "not_configured",
brokerBaseUrl: "https://my-staging.paperclip.app",
instanceId: null,
environment: "staging",
origins: [],
});
await render();
expect(container.textContent).toContain("Access · Sign in");
expect(radioContaining("My GitHub account")?.getAttribute("aria-checked")).toBe("true");
expect(radioContaining("Any agent")?.getAttribute("aria-checked")).toBe("true");
expect(container.textContent).toContain("Which agents may use your GitHub when you’re responsible?");
await passAccessStep();
expect(container.textContent).toContain("Connect with Paperclip");
expect(container.textContent).not.toContain("GitHub token");
});
it("restores the setup step after the one-time enrollment callback", async () => {
mockSearch.value = "source=github&stage=setup&cloud_connector=enrolled";
listGalleryMock.mockResolvedValueOnce({ apps: [GITHUB_MANAGED] });