@@ -6,6 +6,7 @@ import { getActivelyBannedUserIds } from '@/lib/auth/ban'
66import type { WorkspaceAuthorizationContext } from '@/lib/core/application'
77import { OrchestrationError } from '@/lib/core/orchestration/types'
88import { conversationModeSelection } from '@/lib/mothership/chat/intent'
9+ import { authorizeChatWorkflowRead , isChatModeAvailable } from '@/lib/mothership/chat/lifecycle'
910import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context'
1011
1112export type ChatOwnerContext =
@@ -37,6 +38,7 @@ export async function resolveOwnedChatContext(
3738 userId : copilotChats . userId ,
3839 workspaceId : copilotChats . workspaceId ,
3940 organizationId : copilotChats . organizationId ,
41+ workflowId : copilotChats . workflowId ,
4042 type : copilotChats . type ,
4143 mode : conversationModeSelection ,
4244 memorySpaceId : copilotChats . memorySpaceId ,
@@ -46,7 +48,8 @@ export async function resolveOwnedChatContext(
4648 . limit ( 1 )
4749 if (
4850 ! chat ||
49- Boolean ( chat . workspaceId ) === Boolean ( chat . organizationId ) ||
51+ ( chat . workspaceId && chat . organizationId ) ||
52+ ( ! chat . workspaceId && ! chat . organizationId && ! chat . workflowId ) ||
5053 chat . userId !== resolvePrincipalSubjectUserId ( principal ) ||
5154 ( chat . organizationId && chat . type !== 'mothership' )
5255 ) {
@@ -58,15 +61,49 @@ export async function resolveOwnedChatContext(
5861 const owner : ChatOwnerContext = chat . organizationId
5962 ? { organizationId : chat . organizationId }
6063 : {
61- ...( await resolveActiveWorkspaceApplicationContext ( chat . workspaceId ! ) ) ,
64+ ...( await resolveActiveWorkspaceApplicationContext (
65+ await resolveChatWorkspaceId ( chatId , chat )
66+ ) ) ,
6267 organizationId : undefined ,
6368 }
6469 return {
6570 ...owner ,
6671 chatId,
6772 userId : chat . userId ,
73+ workflowId : chat . workflowId ,
6874 type : chat . type ,
6975 mode : chat . mode ,
7076 memorySpaceId : chat . memorySpaceId ?? null ,
7177 }
7278}
79+
80+ /**
81+ * A workflow chat row from before chats carried a workspace is scoped by its workflow's, while
82+ * its owner may still read that workflow.
83+ */
84+ async function resolveChatWorkspaceId (
85+ chatId : string ,
86+ chat : { userId : string ; workspaceId : string | null ; workflowId : string | null }
87+ ) : Promise < string > {
88+ if ( chat . workspaceId ) return chat . workspaceId
89+ const workflow = chat . workflowId
90+ ? await authorizeChatWorkflowRead ( chatId , chat . userId , chat . workflowId )
91+ : null
92+ if ( ! workflow ?. workspaceId ) throw new OrchestrationError ( 'not_found' , 'Chat not found' )
93+ return workflow . workspaceId
94+ }
95+
96+ /**
97+ * Refuses a chat its owner may no longer read, as the chat detail read does: a workflow chat
98+ * whose workflow they cannot read, or a workspace Plan chat while Plan mode is off for them.
99+ */
100+ export async function requireOwnedChatReadable (
101+ context : Awaited < ReturnType < typeof resolveOwnedChatContext > >
102+ ) {
103+ if (
104+ ( context . workflowId &&
105+ ! ( await authorizeChatWorkflowRead ( context . chatId , context . userId , context . workflowId ) ) ) ||
106+ ! ( await isChatModeAvailable ( context , context . userId ) )
107+ )
108+ throw new OrchestrationError ( 'not_found' , 'Chat not found' )
109+ }
0 commit comments