fix(slack): keep grant revocation enforced during install callbacks

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-09 10:03:53 -05:00
1 parent 785ae16699
commit 20534f871b
3 files changed
+10 -4

No files matched your search

@@ -116,7 +116,7 @@ export function slackChatRegistrationService(db: Db, options: SlackSetupOptions)
if (typeof installerId !== "string" || !/^[UW][A-Z0-9]+$/.test(installerId) || installerId === result.bot_user_id) throw providerFailure("slack_install_account_missing");
if (row.managerGrantId) {
const grant = await managerGrants.get(row.managerGrantId, row.companyId, actor);
if (installerId !== grant.slackUserId || teamId !== grant.workspaceId) throw providerFailure("slack_install_identity_mismatch");
if (grant.status !== "active" || installerId !== grant.slackUserId || teamId !== grant.workspaceId) throw providerFailure("slack_install_identity_mismatch");
}
await endpoint(row.endpointId, actor);
// Reauthorization reuses the runtime signing secret after staging was cleaned.
+7 -3
View File
@@ -1,4 +1,5 @@
import { useLayoutEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { ChatEndpointSetup } from "@/pages/apps/chat/ChatEndpointSetup";
import { Button } from "@/components/ui/button";
import { ChatSetupSidebar } from "@/components/chat/ChatSetupNavigation";
@@ -7,10 +8,11 @@ import { type ChatEndpoint } from "@/api/chatEndpoints";
import { storybookAgents, storybookAuthSession } from "./paperclipData";
import { defaultSlackAppConfiguration } from "@paperclipai/shared";
export type SlackSetupScenario = "choose" | "create" | "manual" | "install" | "declined" | "uncertain" | "manifest_pending" | "recovery" | "verify" | "avatar_failed" | "welcome_failed" | "success";
export type SlackSetupScenario = "choose" | "create" | "manual" | "install" | "declined" | "approval_denied" | "uncertain" | "manifest_pending" | "recovery" | "verify" | "avatar_failed" | "welcome_failed" | "success";
/** Production wizard over a local provider fixture. No requests go to Slack. */
export function SlackSetupFixture({ scenario = "create", managed = false, managedAvailable = true, workspaceCount = 1 }: { scenario?: SlackSetupScenario; managed?: boolean; managedAvailable?: boolean; workspaceCount?: number }) {
const queryClient = useQueryClient();
const [ready, setReady] = useState(false);
const [signalVerification, setSignalVerification] = useState<() => void>(() => () => {});
useLayoutEffect(() => {
@@ -39,9 +41,10 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage
endpoint.setup!.slackAccount = { externalUserId: "UPERSON", paperclipUserId: storybookAuthSession.user.id, status: "linked", welcomeStatus: scenario === "welcome_failed" ? "failed" : "sent", ...(scenario === "welcome_failed" ? {} : { dmChannelId: "DSTORY" }) };
endpoint.status = "verifying"; endpoint.providerAccountId = "TSTORY"; endpoint.botExternalId = "USTORY"; endpoint.botUsername = "maya";
};
if (["install", "declined", "recovery", "manifest_pending"].includes(scenario)) created();
if (["install", "declined", "approval_denied", "recovery", "manifest_pending"].includes(scenario)) created();
if (scenario === "manifest_pending") endpoint.setup!.slackRegistration!.errorCode = "slack_manifest_update_pending";
if (scenario === "declined") endpoint.setup!.slackRegistration!.errorCode = managed ? "slack_approval_pending" : "slack_install_declined";
if (scenario === "approval_denied") endpoint.setup!.slackRegistration!.errorCode = "slack_approval_denied";
if (scenario === "recovery") endpoint.setup!.slackRegistration = { ...endpoint.setup!.slackRegistration!, status: "credentials_saved", errorCode: "slack_configuration_incomplete" };
if (scenario === "uncertain") endpoint.setup!.slackRegistration = { status: "uncertain", ...(managed ? { managerGrantId: grantId } : {}), errorCode: "slack_creation_uncertain", managementUrl: "https://api.slack.com/apps" };
if (["verify", "avatar_failed", "welcome_failed", "success"].includes(scenario)) installed();
@@ -57,6 +60,7 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage
if (path === "/api/companies/company-storybook/chat-slack/setup-options") return Response.json({ managedAvailable: managed && managedAvailable, defaultMethod: managed && managedAvailable ? "managed" : "automatic", workspaces: managed && authorized ? Array.from({ length: Math.max(1, workspaceCount) }, (_, i) => ({ grantId: i ? "22222222-2222-4222-8222-222222222222" : grantId, workspaceId: `TSTORY${i}`, workspaceName: i ? "Research workspace" : "Paperclip", userId: "UPERSON" })) : [] });
if (path === "/api/chat-endpoints/slack-story/slack/managed/authorize") {
authorized = true;
void queryClient.invalidateQueries({ queryKey: ["slack-managed-workspaces", endpoint.companyId] });
return Response.json({ authorizationUrl: `${window.location.href.split("#")[0]}#simulated-manager-consent`, expiresAt: new Date(Date.now() + 600_000).toISOString() });
}
if (path === "/api/chat-endpoints/slack-story/slack/managed/provision") { installed(); return Response.json({}); }
@@ -102,7 +106,7 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage
};
setReady(true);
return () => { window.fetch = original; };
}, [scenario, managed, managedAvailable, workspaceCount]);
}, [scenario, managed, managedAvailable, workspaceCount, queryClient]);
return <div className="space-y-6 p-6">
<aside className="flex flex-wrap items-center gap-3 rounded-lg border border-border p-3 text-sm text-muted-foreground">
Preview fixture: Slack consent, callbacks, and identity discovery are simulated.
@@ -22,6 +22,8 @@ export const MultipleWorkspaces: Story = { name: "02 · Add to Slack · Multiple
export const Connect: Story = { name: "03 · Connect · Waiting for a message", args: { scenario: "verify" } };
export const Success: Story = { name: "03 · Connect · Success", args: { scenario: "success" } };
export const ApprovalPending: Story = { name: "Recovery · Slack approval pending", args: { scenario: "declined" } };
export const ApprovalDeclined: Story = { name: "Recovery · Slack approval declined", args: { scenario: "approval_denied" } };
export const EventConfigurationPending: Story = { name: "Recovery · App configuration pending", args: { scenario: "manifest_pending" } };
export const UncertainCreation: Story = { name: "Recovery · Uncertain creation", args: { scenario: "uncertain" } };
export const SavedCredentials: Story = { name: "Recovery · Saved credentials", args: { scenario: "recovery" } };
export const Unavailable: Story = { name: "Recovery · Managed setup unavailable", args: { scenario: "install", managedAvailable: false } };