diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c210d87966..061695e27d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -54,7 +54,6 @@ "expo-font": "~55.0.6", "expo-haptics": "~55.0.13", "expo-image": "~55.0.8", - "expo-image-manipulator": "~55.0.14", "expo-image-picker": "~55.0.17", "expo-insights": "55.0.15", "expo-linking": "~55.0.11", @@ -83,11 +82,8 @@ "react-native-svg": "15.15.3", "react-native-worklets": "0.7.2", "sonner-native": "^0.23.1", - "stream-chat": "catalog:", - "stream-chat-expo": "^8.13.7", "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.2", - "zod": "catalog:" + "tailwindcss": "^4.2.2" }, "devDependencies": { "@sentry/cli": "catalog:", diff --git a/apps/mobile/src/app/(app)/chat/[instance-id].tsx b/apps/mobile/src/app/(app)/chat/[instance-id].tsx deleted file mode 100644 index 3203d543f3..0000000000 --- a/apps/mobile/src/app/(app)/chat/[instance-id].tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useLocalSearchParams } from 'expo-router'; -import { View } from 'react-native'; - -import { KiloClawChat } from '@/components/kiloclaw/chat'; -import { useInstanceContext } from '@/lib/hooks/use-instance-context'; -import { useKiloClawStatus } from '@/lib/hooks/use-kiloclaw-queries'; - -export default function ChatScreen() { - const { 'instance-id': instanceId } = useLocalSearchParams<{ 'instance-id': string }>(); - const { organizationId } = useInstanceContext(instanceId); - const { data: status } = useKiloClawStatus(organizationId); - const isRunning = status?.status === 'running'; - const machineName = status?.name ?? 'Chat'; - - return ( - - - - ); -} diff --git a/apps/mobile/src/components/home/kiloclaw-card.tsx b/apps/mobile/src/components/home/kiloclaw-card.tsx index 16599f6e32..c773e782a1 100644 --- a/apps/mobile/src/components/home/kiloclaw-card.tsx +++ b/apps/mobile/src/components/home/kiloclaw-card.tsx @@ -6,9 +6,7 @@ import { isTransitionalStatus, statusLabel, statusTone } from '@/components/kilo import { StatusDot } from '@/components/ui/status-dot'; import { Text } from '@/components/ui/text'; import { agentColor } from '@/lib/agent-color'; -import { useKiloClawLatestMessage } from '@/lib/hooks/use-kiloclaw-latest-message'; import { useKiloClawStatus, useKiloClawStatusQueryKey } from '@/lib/hooks/use-kiloclaw-queries'; -import { parseTimestamp } from '@/lib/utils'; type KiloClawCardProps = { instance: { @@ -27,25 +25,6 @@ function formatUnreadCount(count: number): string { return count > 99 ? '99+' : String(count); } -function formatMessagePreview( - message: { text: string; isFromMe: boolean }, - botEmoji: string | null -): string { - const text = message.text.length > 0 ? message.text : 'New message'; - if (message.isFromMe) { - return `You: ${text}`; - } - return botEmoji ? `${botEmoji} ${text}` : text; -} - -function formatClockTime(date: Date): string { - const hours = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const period = hours >= 12 ? 'PM' : 'AM'; - const displayHours = hours % 12 === 0 ? 12 : hours % 12; - return `${String(displayHours)}:${minutes} ${period}`; -} - function firstLetter(name: string): string { const trimmed = name.trim(); return trimmed.length > 0 ? (trimmed[0]?.toUpperCase() ?? 'K') : 'K'; @@ -69,7 +48,6 @@ export function KiloClawCard({ instance, unreadCount = 0 }: Readonly 0; const accessibilityLabel = hasUnread @@ -118,11 +95,6 @@ export function KiloClawCard({ instance, unreadCount = 0 }: Readonly {displayName} - {lastMessageTime ? ( - - {lastMessageTime} - - ) : null} @@ -137,14 +109,6 @@ export function KiloClawCard({ instance, unreadCount = 0 }: Readonly ) : null} - - {latest ? ( - - - {formatMessagePreview(latest, botEmoji)} - - - ) : null} ); } diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts b/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts index 221f56e395..253bf6a85a 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-conversations.ts @@ -2,10 +2,4 @@ export { useConversations, useConversationDetail, useCreateConversation, - useRenameConversation, - useLeaveConversation, - useMarkConversationRead, - updateConversationPages, - filterConversationPages, } from '@kilocode/kilo-chat-hooks'; -export type { ConversationListInfiniteData } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts b/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts index 2f5fb31ec6..ac26bc57f6 100644 --- a/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts +++ b/apps/mobile/src/components/kilo-chat/hooks/use-messages.ts @@ -1,11 +1 @@ -export { - useMessages, - useSendMessage, - useEditMessage, - useDeleteMessage, - useAddReaction, - useRemoveReaction, - useExecuteAction, - useMessageCacheUpdater, -} from '@kilocode/kilo-chat-hooks'; -export type { SendMessageVariables } from '@kilocode/kilo-chat-hooks'; +export { useMessages, useSendMessage } from '@kilocode/kilo-chat-hooks'; diff --git a/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx index 95ed84f647..d3ca76aa5f 100644 --- a/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx +++ b/apps/mobile/src/components/kilo-chat/kilo-chat-provider.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { EventServiceClient } from '@kilocode/event-service'; import { KiloChatClient } from '@kilocode/kilo-chat'; @@ -8,21 +8,6 @@ import { EVENT_SERVICE_URL, KILO_CHAT_URL } from '@/lib/config'; import { useKiloChatTokenGetter } from './hooks/use-kilo-chat-token'; -type KiloChatContextValue = { - eventService: EventServiceClient; - kiloChatClient: KiloChatClient; -}; - -export const KiloChatContext = createContext(null); - -export function useKiloChatContext(): KiloChatContextValue { - const ctx = useContext(KiloChatContext); - if (!ctx) { - throw new Error('useKiloChatContext must be used within a KiloChatProvider'); - } - return ctx; -} - type KiloChatProviderProps = { children: React.ReactNode; }; @@ -30,7 +15,7 @@ type KiloChatProviderProps = { export function KiloChatProvider({ children }: KiloChatProviderProps) { const getToken = useKiloChatTokenGetter(); - const [value] = useState(() => { + const [value] = useState(() => { const eventService = new EventServiceClient({ url: EVENT_SERVICE_URL, getToken, @@ -51,12 +36,10 @@ export function KiloChatProvider({ children }: KiloChatProviderProps) { }, [value]); return ( - - - {children} - - + + {children} + ); } diff --git a/apps/mobile/src/components/kiloclaw/chat-avatar.tsx b/apps/mobile/src/components/kiloclaw/chat-avatar.tsx deleted file mode 100644 index 9445553291..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat-avatar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { View } from 'react-native'; -import { type MessageAvatarProps, useMessageContext } from 'stream-chat-expo'; - -import logo from '@/../assets/images/logo.png'; -import { Image } from '@/components/ui/image'; - -export function KiloClawMessageAvatar(_props: MessageAvatarProps) { - const { message, lastGroupMessage } = useMessageContext(); - // eslint-disable-next-line typescript-eslint/no-unnecessary-condition -- message can be undefined at runtime in reply swipe context - const isBotMessage = message?.user?.id?.startsWith('bot-'); - - if (!lastGroupMessage) { - return ; - } - - if (isBotMessage) { - return ( - - - - ); - } - - return ; -} diff --git a/apps/mobile/src/components/kiloclaw/chat-hooks.ts b/apps/mobile/src/components/kiloclaw/chat-hooks.ts deleted file mode 100644 index 7f32cf37bc..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat-hooks.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react'; -import { type Event, type Channel as StreamChannel, type StreamChat } from 'stream-chat'; - -export function useBotOnlineStatus( - client: StreamChat | null, - channel: StreamChannel | null, - botUserId: string -): boolean { - const [online, setOnline] = useState(false); - - useEffect(() => { - const handlePresenceChange = (event: Event) => { - if (event.user?.id === botUserId) { - setOnline(Boolean(event.user.online)); - } - }; - - if (client && channel) { - // Check initial state - const member = channel.state.members[botUserId]; - setOnline(Boolean(member?.user?.online)); - client.on('user.presence.changed', handlePresenceChange); - } - - return () => { - client?.off('user.presence.changed', handlePresenceChange); - }; - }, [client, channel, botUserId]); - - return online; -} diff --git a/apps/mobile/src/components/kiloclaw/chat-placeholder.tsx b/apps/mobile/src/components/kiloclaw/chat-placeholder.tsx deleted file mode 100644 index 35017111fe..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat-placeholder.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { View } from 'react-native'; - -import { Text } from '@/components/ui/text'; - -export function ChatPlaceholder({ message }: { message: string }) { - return ( - - {message} - - ); -} diff --git a/apps/mobile/src/components/kiloclaw/chat-shell.tsx b/apps/mobile/src/components/kiloclaw/chat-shell.tsx deleted file mode 100644 index 6bc1ebf35a..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat-shell.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { type Href, useRouter } from 'expo-router'; -import { Settings } from 'lucide-react-native'; -import { Pressable, View } from 'react-native'; - -import { ScreenHeader } from '@/components/screen-header'; -import { Text } from '@/components/ui/text'; -import { useAllKiloClawInstances } from '@/lib/hooks/use-instance-context'; -import { useThemeColors } from '@/lib/hooks/use-theme-colors'; - -function BotStatusIndicator({ online }: { online: boolean }) { - return ( - - - {online ? 'Online' : 'Offline'} - - ); -} - -export function ChatHeader({ - instanceId, - title, - botOnline, -}: { - instanceId: string; - title: string; - botOnline?: boolean; -}) { - const router = useRouter(); - const colors = useThemeColors(); - const { data: instances } = useAllKiloClawInstances(); - - const hasMultipleInstances = (instances?.length ?? 0) > 1; - - const handleTitlePress = () => { - const href: Href = { - pathname: '/(app)/chat/instance-picker', - params: { currentId: instanceId }, - }; - router.push(href); - }; - - const settingsButton = ( - { - router.push(`/(app)/kiloclaw/${instanceId}/dashboard` as Href); - }} - hitSlop={12} - accessibilityLabel="Settings" - className="active:opacity-70" - > - - - ); - - return ( - - {botOnline !== undefined && } - {settingsButton} - - } - /> - ); -} - -export function ChatShell({ - instanceId, - name, - children, -}: { - instanceId: string; - name: string; - children: React.ReactNode; -}) { - return ( - - - {children} - - ); -} diff --git a/apps/mobile/src/components/kiloclaw/chat-theme.ts b/apps/mobile/src/components/kiloclaw/chat-theme.ts deleted file mode 100644 index 2472ec5cbb..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat-theme.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useColorScheme } from 'react-native'; -import { type DeepPartial, type Theme } from 'stream-chat-expo'; - -import { useThemeColors } from '@/lib/hooks/use-theme-colors'; - -export function useStreamChatTheme(): DeepPartial { - const colorScheme = useColorScheme(); - const colors = useThemeColors(); - - const [theme, setTheme] = useState>(() => buildTheme(colorScheme, colors)); - - useEffect(() => { - setTheme(buildTheme(colorScheme, colors)); - }, [colorScheme, colors]); - - return theme; -} - -function buildTheme( - colorScheme: ReturnType, - colors: ReturnType -): DeepPartial { - return { - colors: - colorScheme === 'dark' - ? { - black: colors.foreground, - white: colors.background, - white_smoke: colors.secondary, - white_snow: colors.muted, - grey: colors.mutedForeground, - grey_dark: colors.mutedForeground, - grey_gainsboro: colors.border, - grey_whisper: colors.border, - light_blue: 'hsl(0, 0%, 20%)', - light_gray: 'hsl(0, 0%, 20%)', - blue_alice: 'hsl(0, 0%, 18%)', - text_high_emphasis: colors.foreground, - text_low_emphasis: colors.mutedForeground, - bg_gradient_start: colors.background, - bg_gradient_end: colors.secondary, - icon_background: colors.card, - overlay: 'rgba(0, 0, 0, 0.8)', - } - : {}, - dateHeader: { - container: { - backgroundColor: colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.12)' : undefined, - }, - text: { - color: colorScheme === 'dark' ? colors.foreground : undefined, - }, - }, - inlineDateSeparator: { - container: { - backgroundColor: colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.12)' : undefined, - }, - text: { - color: colorScheme === 'dark' ? colors.foreground : undefined, - }, - }, - messageInput: { - container: { - paddingHorizontal: 12, - borderColor: colors.border, - }, - }, - }; -} diff --git a/apps/mobile/src/components/kiloclaw/chat.tsx b/apps/mobile/src/components/kiloclaw/chat.tsx deleted file mode 100644 index 709ddaba30..0000000000 --- a/apps/mobile/src/components/kiloclaw/chat.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ActivityIndicator, View } from 'react-native'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useFocusEffect } from 'expo-router'; -import { Image as ExpoImage } from 'expo-image'; // eslint-disable-line no-restricted-imports -- raw expo-image needed for Stream Chat SDK ImageComponent prop -import * as Notifications from 'expo-notifications'; -import { type Channel as StreamChannel, StreamChat } from 'stream-chat'; -import { Channel, Chat, MessageInput, MessageList, OverlayProvider } from 'stream-chat-expo'; -import { toast } from 'sonner-native'; - -import { badgeBucketForConversation } from '@kilocode/notifications'; - -import { KiloClawMessageAvatar } from '@/components/kiloclaw/chat-avatar'; -import { ChatPlaceholder } from '@/components/kiloclaw/chat-placeholder'; -import { ChatHeader, ChatShell } from '@/components/kiloclaw/chat-shell'; -import { useBotOnlineStatus } from '@/components/kiloclaw/chat-hooks'; -import { NotificationPrompt } from '@/components/kiloclaw/notification-prompt'; -import { useStreamChatTheme } from '@/components/kiloclaw/chat-theme'; -import { badgeBucketForInstance } from '@/lib/badge-buckets'; -import { useAppLifecycle } from '@/lib/hooks/use-app-lifecycle'; -import { useStreamChatCredentials } from '@/lib/hooks/use-kiloclaw-queries'; -import { setLastActiveInstance } from '@/lib/last-active-instance'; -import { parseNotificationData } from '@/lib/notifications'; -import { useTRPC } from '@/lib/trpc'; - -type KiloClawChatProps = { - instanceId: string; - name: string; - enabled: boolean; - organizationId?: string | null; -}; - -type UnreadCountsData = { badgeBucket: string; badgeCount: number }[]; - -export function KiloClawChat({ - instanceId, - name, - enabled, - organizationId, -}: Readonly) { - const { data: creds, isLoading, error } = useStreamChatCredentials(organizationId, enabled); - const trpc = useTRPC(); - const { isActive } = useAppLifecycle(); - const isFocusedRef = useRef(false); - - const queryClient = useQueryClient(); - const unreadCountsKey = useMemo(() => trpc.user.getUnreadCounts.queryOptions().queryKey, [trpc]); - - const { mutate: markChatRead } = useMutation( - trpc.user.markChatRead.mutationOptions({ - onMutate: async ({ badgeBucket }) => { - await queryClient.cancelQueries({ queryKey: unreadCountsKey }); - const previous = queryClient.getQueryData(unreadCountsKey); - queryClient.setQueryData(unreadCountsKey, old => - (old ?? []).filter(row => row.badgeBucket !== badgeBucket) - ); - return { previous }; - }, - onSuccess: ({ badgeCount }) => { - void Notifications.setBadgeCountAsync(badgeCount); - }, - onError: (err: { message: string }, _input, context) => { - if (context?.previous) { - queryClient.setQueryData(unreadCountsKey, context.previous); - } - toast.error(err.message || 'Failed to update badge count'); - }, - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: unreadCountsKey }); - }, - }) - ); - - useFocusEffect( - useCallback(() => { - const badgeBucket = badgeBucketForInstance(instanceId); - isFocusedRef.current = true; - setLastActiveInstance(instanceId); - markChatRead({ badgeBucket }); - - // If a notification for this chat arrives while the screen is already open it is - // visually suppressed, but the DO still incremented the server-side count. Clear - // it immediately so the badge never drifts above 0 while the user is reading. - const subscription = Notifications.addNotificationReceivedListener(notification => { - const data = parseNotificationData(notification.request.content.data); - if (data?.type === 'chat.message' && data.sandboxId === instanceId) { - markChatRead({ - badgeBucket: badgeBucketForConversation(data.sandboxId, data.conversationId), - }); - } - }); - - return () => { - isFocusedRef.current = false; - subscription.remove(); - }; - }, [instanceId, markChatRead]) - ); - - // Clear badge when the app returns to the foreground while this chat is focused. - // Notifications received in the background do not fire the listener above, and - // useFocusEffect does not re-run on app resume (focus is a navigation concept, - // not an app-state one), so without this the badge stays stuck after backgrounding. - useEffect(() => { - if (isActive && isFocusedRef.current) { - markChatRead({ badgeBucket: badgeBucketForInstance(instanceId) }); - } - }, [isActive, instanceId, markChatRead]); - - if (!enabled) { - return ( - - - - ); - } - - if (isLoading) { - return ( - - - - - - ); - } - - if (error) { - return ( - - - - ); - } - - if (!creds) { - return ( - - - - ); - } - - return ( - - ); -} - -function StreamChatUI({ - instanceId, - name, - apiKey, - userId, - channelId, - organizationId, -}: { - instanceId: string; - name: string; - apiKey: string; - userId: string; - channelId: string; - organizationId?: string | null; -}) { - const { bottom } = useSafeAreaInsets(); - const [headerHeight, setHeaderHeight] = useState(0); - const chatTheme = useStreamChatTheme(); - const trpc = useTRPC(); - const queryClient = useQueryClient(); - - const tokenProvider = useCallback(async () => { - const opts = organizationId - ? trpc.organizations.kiloclaw.getStreamChatCredentials.queryOptions( - { organizationId }, - { staleTime: 0 } - ) - : trpc.kiloclaw.getStreamChatCredentials.queryOptions(undefined, { staleTime: 0 }); - const creds = await queryClient.fetchQuery(opts); - if (!creds?.userToken) { - throw new Error('Failed to fetch Stream Chat credentials'); - } - return creds.userToken; - }, [queryClient, trpc, organizationId]); - - const [client, setClient] = useState(null); - const [channel, setChannel] = useState(null); - const [connectError, setConnectError] = useState(null); - - useEffect(() => { - const chatClient = StreamChat.getInstance(apiKey); - - let cancelled = false; - setConnectError(null); - - const connect = async () => { - try { - // Await disconnect to prevent tokenManager.reset() from racing with the new connection - if (chatClient.userID) { - await chatClient.disconnectUser(); - } - if (cancelled) { - return; - } - await chatClient.connectUser({ id: userId }, tokenProvider); - const ch = chatClient.channel('messaging', channelId); - await ch.watch({ presence: true }); - // eslint-disable-next-line typescript-eslint/no-unnecessary-condition -- cancelled can change across awaits - if (!cancelled) { - setClient(chatClient); - setChannel(ch); - } - } catch (error) { - if (!cancelled) { - setConnectError(error instanceof Error ? error.message : 'Failed to connect to chat.'); - } - } - }; - - void connect(); - - return () => { - cancelled = true; - setClient(null); - setChannel(null); - }; - }, [apiKey, userId, channelId, tokenProvider]); - - // Gracefully close/reopen the websocket on background/foreground. - // This preserves the client and channel state (no disconnect/reconnect). - const { isActive } = useAppLifecycle(); - const wasActiveRef = useRef(isActive); - useEffect(() => { - if (client) { - if (wasActiveRef.current && !isActive) { - void client.closeConnection(); - } else if (!wasActiveRef.current && isActive) { - void client.openConnection(); - } - } - wasActiveRef.current = isActive; - }, [client, isActive]); - - // Bot presence tracking - const sandboxId = channelId.replace(/^default-/, ''); - const botUserId = `bot-${sandboxId}`; - const botOnline = useBotOnlineStatus(client, channel, botUserId); - - if (connectError) { - return ( - - - - ); - } - - if (!client || !channel) { - return ( - - - - - - ); - } - - return ( - - { - setHeaderHeight(e.nativeEvent.layout.height); - }} - > - - - - - {/* eslint-disable-next-line typescript-eslint/no-unsafe-assignment -- expo-image is API-compatible with RN Image */} - - - - - - - - - - - ); -} diff --git a/apps/mobile/src/components/kiloclaw/notification-prompt.tsx b/apps/mobile/src/components/kiloclaw/notification-prompt.tsx deleted file mode 100644 index 0348ab22c9..0000000000 --- a/apps/mobile/src/components/kiloclaw/notification-prompt.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Bell } from 'lucide-react-native'; -import { useCallback, useEffect, useState } from 'react'; -import { Alert, Linking, View } from 'react-native'; -import * as Notifications from 'expo-notifications'; -import * as SecureStore from 'expo-secure-store'; -import { useMutation } from '@tanstack/react-query'; -import { toast } from 'sonner-native'; -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; - -import { Button } from '@/components/ui/button'; -import { Text } from '@/components/ui/text'; -import { useThemeColors } from '@/lib/hooks/use-theme-colors'; -import { - getNotificationPermissionStatus, - getPlatform, - registerForPushNotifications, -} from '@/lib/notifications'; -import { NOTIFICATION_PROMPT_SEEN_KEY } from '@/lib/storage-keys'; -import { useTRPC } from '@/lib/trpc'; - -export function NotificationPrompt({ enabled }: { enabled: boolean }) { - const [visible, setVisible] = useState(false); - const colors = useThemeColors(); - const trpc = useTRPC(); - - const registerToken = useMutation( - trpc.user.registerPushToken.mutationOptions({ - onError: error => { - toast.error(error.message); - }, - }) - ); - - useEffect(() => { - if (!enabled) { - return; - } - - async function check() { - const seen = await SecureStore.getItemAsync(NOTIFICATION_PROMPT_SEEN_KEY); - if (seen) { - return; - } - - const status = await getNotificationPermissionStatus(); - if (status === 'granted') { - return; - } - - setVisible(true); - } - void check(); - }, [enabled]); - - const handleEnable = useCallback(async () => { - const currentStatus = await getNotificationPermissionStatus(); - - if (currentStatus === 'denied') { - Alert.alert( - 'Notifications Disabled', - 'To enable notifications, turn them on in your device settings.', - [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Open Settings', onPress: () => void Linking.openSettings() }, - ] - ); - return; - } - - const result = await Notifications.requestPermissionsAsync(); - if (result.status !== Notifications.PermissionStatus.GRANTED) { - return; - } - - await SecureStore.setItemAsync(NOTIFICATION_PROMPT_SEEN_KEY, 'true'); - setVisible(false); - - const token = await registerForPushNotifications(); - if (token) { - registerToken.mutate( - { token, platform: getPlatform() }, - { - onSuccess: () => { - toast.success('Notifications enabled'); - }, - } - ); - } - }, [registerToken]); - - const handleDismiss = useCallback(async () => { - await SecureStore.setItemAsync(NOTIFICATION_PROMPT_SEEN_KEY, 'true'); - setVisible(false); - }, []); - - if (!visible) { - return null; - } - - return ( - - - - - Get notified when Kilo replies - - We'll send a push notification so you don't miss anything. - - - - - - - - - ); -} diff --git a/apps/mobile/src/components/kiloclaw/onboarding-flow.tsx b/apps/mobile/src/components/kiloclaw/onboarding-flow.tsx index 43916918e8..cb56a270f1 100644 --- a/apps/mobile/src/components/kiloclaw/onboarding-flow.tsx +++ b/apps/mobile/src/components/kiloclaw/onboarding-flow.tsx @@ -309,7 +309,7 @@ export function OnboardingFlow() { ]); const onOpenInstance = useCallback(() => { - // Dismiss the onboarding modal, then open the chat. `chat/[instance-id]` + // Dismiss the onboarding modal, then open the chat. `chat/[sandbox-id]` // is at the (app) layer, so it renders above the tab bar once the modal // closes. router.back(); diff --git a/apps/mobile/src/lib/badge-buckets.ts b/apps/mobile/src/lib/badge-buckets.ts index cb32814020..017be2aa0d 100644 --- a/apps/mobile/src/lib/badge-buckets.ts +++ b/apps/mobile/src/lib/badge-buckets.ts @@ -1 +1,2 @@ -export const badgeBucketForInstance = (sandboxId: string) => `kiloclaw:${sandboxId}` as const; +export const badgeBucketForInstance = (sandboxId: string): `kiloclaw:${string}` => + `kiloclaw:${sandboxId}`; diff --git a/apps/mobile/src/lib/hooks/use-kiloclaw-latest-message.ts b/apps/mobile/src/lib/hooks/use-kiloclaw-latest-message.ts deleted file mode 100644 index 64b1b366a2..0000000000 --- a/apps/mobile/src/lib/hooks/use-kiloclaw-latest-message.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { useStreamChatCredentials } from '@/lib/hooks/use-kiloclaw-queries'; - -const STREAM_CHAT_API_BASE = 'https://chat.stream-io-api.com'; - -type LatestMessage = { - text: string; - isFromMe: boolean; - created_at: string; -}; - -type StreamChatCredentials = { - apiKey: string; - userId: string; - userToken: string; - channelId: string; -}; - -type ChannelQueryResponse = { - messages?: { - text?: string; - created_at?: string; - user?: { id?: string }; - }[]; -}; - -async function fetchLatestMessage(creds: StreamChatCredentials): Promise { - const res = await fetch( - `${STREAM_CHAT_API_BASE}/channels/messaging/${creds.channelId}/query?api_key=${creds.apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: creds.userToken, - }, - body: JSON.stringify({ - state: true, - messages: { limit: 1 }, - }), - } - ); - - if (!res.ok) { - if (res.status === 404) { - return null; - } - const body = await res.text().catch(() => '(unreadable)'); - throw new Error(`Stream Chat query failed (${res.status}): ${body}`); - } - - const payload = (await res.json()) as ChannelQueryResponse; - const message = payload.messages?.[0]; - if (!message?.created_at) { - return null; - } - - return { - text: message.text ?? '', - isFromMe: message.user?.id === creds.userId, - created_at: message.created_at, - }; -} - -/** - * Fetch the most recent message on the KiloClaw chat channel directly from - * Stream Chat, reusing the short-lived user credentials exposed by - * `useStreamChatCredentials`. No extra backend endpoint required. - */ -export function useKiloClawLatestMessage(organizationId?: string | null, enabled = true) { - const { data: creds } = useStreamChatCredentials(organizationId, enabled); - const queryEnabled = enabled && Boolean(creds); - return useQuery({ - queryKey: ['kiloclaw-latest-message', creds?.channelId ?? null], - queryFn: async () => { - if (!creds) { - return null; - } - const latest = await fetchLatestMessage(creds); - return latest; - }, - enabled: queryEnabled, - staleTime: 30_000, - refetchInterval: queryEnabled ? 60_000 : false, - }); -} diff --git a/apps/mobile/src/lib/hooks/use-kiloclaw-queries.ts b/apps/mobile/src/lib/hooks/use-kiloclaw-queries.ts index 6ea2818ebd..2796de3bb6 100644 --- a/apps/mobile/src/lib/hooks/use-kiloclaw-queries.ts +++ b/apps/mobile/src/lib/hooks/use-kiloclaw-queries.ts @@ -289,24 +289,6 @@ export function useKiloClawSecretCatalog(organizationId?: string | null) { return isOrg ? org : personal; } -export function useStreamChatCredentials(organizationId?: string | null, enabled = true) { - const trpc = useTRPC(); - const { isOrg, personalEnabled, orgEnabled, orgInput } = resolveContext(organizationId, enabled); - const personal = useQuery( - trpc.kiloclaw.getStreamChatCredentials.queryOptions(undefined, { - enabled: personalEnabled, - staleTime: 5 * 60_000, - }) - ); - const org = useQuery( - trpc.organizations.kiloclaw.getStreamChatCredentials.queryOptions(orgInput, { - enabled: orgEnabled, - staleTime: 5 * 60_000, - }) - ); - return isOrg ? org : personal; -} - export function useKiloClawConfig(organizationId?: string | null) { const trpc = useTRPC(); const { isOrg, personalEnabled, orgEnabled, orgInput } = resolveContext(organizationId); diff --git a/apps/mobile/src/lib/hooks/use-unread-counts.ts b/apps/mobile/src/lib/hooks/use-unread-counts.ts index f88f95cabc..04b28d5db5 100644 --- a/apps/mobile/src/lib/hooks/use-unread-counts.ts +++ b/apps/mobile/src/lib/hooks/use-unread-counts.ts @@ -1,11 +1,13 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; +import { badgeBucketForInstance } from '@/lib/badge-buckets'; import { useTRPC } from '@/lib/trpc'; /** - * Fetches unread message counts for the current user and returns a Map keyed - * by badge bucket for O(1) lookup from dashboard cards. + * Fetches unread message counts for the current user and returns a Map keyed by + * instance badge bucket for O(1) lookup from dashboard cards. Conversation + * buckets are summed into their parent instance bucket. * * Freshness is driven by invalidations, not polling: * - Foreground chat push → invalidate (see `use-unread-counts-invalidation`). @@ -23,7 +25,10 @@ export function useUnreadCounts() { const byBadgeBucket = useMemo(() => { const map = new Map(); for (const row of query.data ?? []) { - map.set(row.badgeBucket, row.badgeCount); + const parts = row.badgeBucket.split(':'); + const aggregateBucket = + parts[0] === 'kiloclaw' && parts[1] ? badgeBucketForInstance(parts[1]) : row.badgeBucket; + map.set(aggregateBucket, (map.get(aggregateBucket) ?? 0) + row.badgeCount); } return map; }, [query.data]); diff --git a/apps/mobile/src/lib/last-active-instance.ts b/apps/mobile/src/lib/last-active-instance.ts index 05f9577d0f..8929829b44 100644 --- a/apps/mobile/src/lib/last-active-instance.ts +++ b/apps/mobile/src/lib/last-active-instance.ts @@ -12,8 +12,3 @@ export async function loadLastActiveInstance(): Promise { export function getLastActiveInstance(): string | null { return cached; } - -export function setLastActiveInstance(id: string): void { - cached = id; - void SecureStore.setItemAsync(LAST_ACTIVE_INSTANCE_KEY, id); -} diff --git a/apps/web/package.json b/apps/web/package.json index eb8afda600..ea1c8e31e3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -143,8 +143,6 @@ "remark-gfm": "^4.0.1", "server-only": "^0.0.1", "sonner": "^2.0.7", - "stream-chat": "^9.38.0", - "stream-chat-react": "^13.14.2", "stripe": "catalog:", "stytch": "^12.43.1", "tailwind-merge": "^3.5.0", diff --git a/apps/web/src/app/(app)/claw/claw-chat.css b/apps/web/src/app/(app)/claw/claw-chat.css deleted file mode 100644 index e51d77aeac..0000000000 --- a/apps/web/src/app/(app)/claw/claw-chat.css +++ /dev/null @@ -1,153 +0,0 @@ -@import 'stream-chat-react/dist/css/v2/index.css'; - -/* ── Stream Chat theme overrides ────────────────────────────────────────────── - Stream Chat CSS is imported into layer(base) so these unlayered overrides - always win per the CSS cascade (unlayered > layered). - Scoped to .claw-chat-wrapper to avoid leaking outside the ChatTab. */ -.claw-chat-wrapper { - font-family: inherit; - border-radius: var(--radius-lg); - border: 1px solid oklch(1 0 0 / 6%); - background: oklch(0.269 0 0 / 0.2); - overflow: hidden; -} - -.claw-chat-wrapper .str-chat, -.claw-chat-wrapper .str-chat-channel, -.claw-chat-wrapper .str-chat__container { - height: 100%; -} - -.claw-chat-wrapper .str-chat { - /* ── Global theme: colors ─────────────────────────────────────────────── */ - --str-chat__primary-color: oklch(0.546 0.245 262.881); - --str-chat__active-primary-color: oklch(0.488 0.243 264.376); - --str-chat__primary-color-low-emphasis: oklch(0.546 0.245 262.881 / 0.3); - --str-chat__primary-overlay-color: oklch(0.546 0.245 262.881 / 0.6); - --str-chat__on-primary-color: oklch(0.985 0 0); - - --str-chat__background-color: transparent; - --str-chat__secondary-background-color: transparent; - - --str-chat__primary-surface-color: oklch(0.546 0.245 262.881 / 0.15); - --str-chat__primary-surface-color-low-emphasis: oklch(0.546 0.245 262.881 / 0.08); - --str-chat__surface-color: oklch(0.269 0 0 / 0.4); - --str-chat__secondary-surface-color: oklch(0.269 0 0 / 0.3); - --str-chat__tertiary-surface-color: oklch(0.269 0 0 / 0.2); - - --str-chat__text-color: oklch(0.985 0 0); - --str-chat__text-low-emphasis-color: oklch(0.708 0 0); - --str-chat__disabled-color: oklch(0.708 0 0); - --str-chat__on-disabled-color: oklch(0.985 0 0); - - --str-chat__danger-color: oklch(0.704 0.191 22.216); - --str-chat__info-color: oklch(0.696 0.17 162.48); - --str-chat__unread-badge-color: oklch(0.704 0.191 22.216); - --str-chat__on-unread-badge-color: oklch(0.985 0 0); - --str-chat__message-highlight-color: oklch(0.332 0.06 83); - - --str-chat__overlay-color: oklch(0 0 0 / 0.7); - --str-chat__secondary-overlay-color: oklch(0 0 0 / 0.4); - --str-chat__secondary-overlay-text-color: oklch(0.985 0 0); - --str-chat__opaque-surface-background-color: oklch(0.985 0 0 / 0.85); - --str-chat__opaque-surface-text-color: oklch(0.145 0 0); - --str-chat__box-shadow-color: oklch(0 0 0 / 0.8); - - /* ── Global theme: typography ─────────────────────────────────────────── */ - /* Note: `inherit` cannot be used as --str-chat__font-family because it's - a CSS-wide keyword that invalidates `font` shorthand substitution. - We use Inter directly to match the Kilo UI, with a system fallback. */ - --str-chat__font-family: Inter, ui-sans-serif, system-ui, sans-serif; - --str-chat__caption-text: 0.6875rem/1.3 var(--str-chat__font-family); - --str-chat__caption-medium-text: 500 0.6875rem/1.3 var(--str-chat__font-family); - --str-chat__caption-strong-text: 700 0.6875rem/1.3 var(--str-chat__font-family); - --str-chat__body-text: 0.8125rem/1.4 var(--str-chat__font-family); - --str-chat__body-medium-text: 500 0.8125rem/1.4 var(--str-chat__font-family); - --str-chat__body2-text: 0.875rem/1.4 var(--str-chat__font-family); - --str-chat__body2-medium-text: 500 0.875rem/1.4 var(--str-chat__font-family); - --str-chat__subtitle-text: 0.875rem/1.3 var(--str-chat__font-family); - --str-chat__subtitle-medium-text: 500 0.875rem/1.3 var(--str-chat__font-family); - --str-chat__subtitle2-text: 1rem/1.2 var(--str-chat__font-family); - --str-chat__subtitle2-medium-text: 500 1rem/1.2 var(--str-chat__font-family); - --str-chat__headline-text: 1.125rem/1.2 var(--str-chat__font-family); - --str-chat__headline2-text: 1.25rem/1.2 var(--str-chat__font-family); - - /* ── Global theme: border radius ──────────────────────────────────────── */ - --str-chat__border-radius-xs: 6px; - --str-chat__border-radius-sm: 8px; - --str-chat__border-radius-md: 10px; - --str-chat__border-radius-lg: 14px; - --str-chat__border-radius-circle: 999px; - - /* ── Component: message bubbles (badge-style: transparent bg + border) ── */ - --str-chat__message-bubble-background-color: transparent; - --str-chat__message-bubble-color: oklch(0.708 0 0); - --str-chat__message-bubble-border-block-start: 1px solid oklch(1 0 0 / 10%); - --str-chat__message-bubble-border-block-end: 1px solid oklch(1 0 0 / 10%); - --str-chat__message-bubble-border-inline-start: 1px solid oklch(1 0 0 / 10%); - --str-chat__message-bubble-border-inline-end: 1px solid oklch(1 0 0 / 10%); - --str-chat__message-bubble-border-radius: var(--str-chat__border-radius-md); - --str-chat__own-message-bubble-background-color: transparent; - --str-chat__own-message-bubble-color: oklch(0.708 0 0); - - /* ── Component: message input ─────────────────────────────────────────── */ - --str-chat__message-input-background-color: transparent; - --str-chat__message-input-color: oklch(0.985 0 0); - --str-chat__message-textarea-background-color: oklch(0.269 0 0 / 0.4); - --str-chat__message-textarea-border-block-start: 1px solid oklch(1 0 0 / 6%); - --str-chat__message-textarea-border-block-end: 1px solid oklch(1 0 0 / 6%); - --str-chat__message-textarea-border-inline-start: 1px solid oklch(1 0 0 / 6%); - --str-chat__message-textarea-border-inline-end: 1px solid oklch(1 0 0 / 6%); - --str-chat__message-textarea-color: oklch(0.985 0 0); - - /* ── Component: message list ──────────────────────────────────────────── */ - --str-chat__message-list-background-color: transparent; - --str-chat__message-list-color: oklch(0.985 0 0); - - /* ── Component: channel header ────────────────────────────────────────── */ - --str-chat__channel-header-background-color: transparent; - - /* ── Component: date separator ────────────────────────────────────────── */ - --str-chat__date-separator-color: oklch(0.708 0 0); - --str-chat__date-separator-line-color: oklch(1 0 0 / 10%); - - /* ── Component: message actions ───────────────────────────────────────── */ - --str-chat__message-actions-box-background-color: oklch(0.269 0 0 / 0.9); - --str-chat__message-actions-box-color: oklch(0.985 0 0); - --str-chat__message-actions-box-box-shadow: 0 4px 12px oklch(0 0 0 / 0.4); -} - -/* Constrain send button icon to 20x20 */ -.claw-chat-wrapper .str-chat__send-button svg { - width: 20px; - height: 20px; -} - -/* Hide bot sender name (long ID strings) */ -.claw-chat-wrapper .str-chat__message-simple-name { - display: none; -} - -/* ── Thinking indicator ────────────────────────────────────────────────────── */ -.claw-thinking-message { - display: flex; - align-items: center; - padding: 8px 16px; -} - -.claw-thinking-text { - font-style: italic; - font: var(--str-chat__body-text); - color: oklch(0.708 0 0); - animation: claw-thinking-pulse 1.5s ease-in-out infinite; -} - -@keyframes claw-thinking-pulse { - 0%, - 100% { - opacity: 0.4; - } - 50% { - opacity: 1; - } -} diff --git a/apps/web/src/app/(app)/claw/components/ChatTab.tsx b/apps/web/src/app/(app)/claw/components/ChatTab.tsx index 7ef6930f89..ef29dbf966 100644 --- a/apps/web/src/app/(app)/claw/components/ChatTab.tsx +++ b/apps/web/src/app/(app)/claw/components/ChatTab.tsx @@ -1,227 +1,10 @@ 'use client'; - -import { createContext, use, useCallback, useEffect, useState } from 'react'; -import type { Channel as StreamChannel, Event } from 'stream-chat'; -import { useQueryClient } from '@tanstack/react-query'; -import { MessageSquare, RotateCw } from 'lucide-react'; -import { - Chat, - Channel, - Window, - MessageList, - MessageInput, - MessageSimple, - Thread, - useCreateChatClient, - useChatContext, - useChannelStateContext, - useMessageContext, -} from 'stream-chat-react'; -import { useClawStreamChatCredentials } from '../hooks/useClawHooks'; -import { useTRPC } from '@/lib/trpc/utils'; -import { useClawContext } from './ClawContext'; - -const BotUserIdContext = createContext(''); - -type ChatTabProps = { - /** Only fetch credentials and connect when true (tab is active + instance running). */ - enabled: boolean; -}; - -export function ChatTab({ enabled }: ChatTabProps) { - const { data: creds, isLoading, error } = useClawStreamChatCredentials(enabled); - - if (!enabled) { - return ; - } - - if (isLoading) { - return ; - } - - if (error) { - return ; - } - - if (!creds) { - return ( -
-
- -
-
-

Chat requires an upgrade

-

- This instance was provisioned before chat was enabled. Use the{' '} - - - Upgrade to Latest - {' '} - button above to activate real-time chat with your KiloClaw bot. -

-
-
- ); - } - - return ; -} - -// ─── Internal components ──────────────────────────────────────────────────── - -function StreamChatUI({ - apiKey, - userId, - channelId, -}: { - apiKey: string; - userId: string; - channelId: string; -}) { - const trpc = useTRPC(); - const queryClient = useQueryClient(); - const { organizationId } = useClawContext(); - - // Stable token provider that fetches a fresh short-lived token on every call. - // stream-chat-react calls this when the current token expires (via `exp` claim). - // Routes to the correct tRPC endpoint based on personal vs org context. - const tokenProvider = useCallback(async () => { - const opts = organizationId - ? trpc.organizations.kiloclaw.getStreamChatCredentials.queryOptions( - { organizationId }, - { staleTime: 0 } - ) - : trpc.kiloclaw.getStreamChatCredentials.queryOptions(undefined, { - staleTime: 0, - }); - const creds = await queryClient.fetchQuery(opts); - if (!creds?.userToken) { - throw new Error('Failed to fetch Stream Chat credentials'); - } - return creds.userToken; - }, [queryClient, trpc, organizationId]); - - const client = useCreateChatClient({ - apiKey, - tokenOrProvider: tokenProvider, - userData: { id: userId }, - }); - - const [channel, setChannel] = useState(); - - useEffect(() => { - if (!client) return; - const ch = client.channel('messaging', channelId); - let cancelled = false; - void (async () => { - await ch.watch({ presence: true }); - if (cancelled) return; - // Disable file uploads client-side by stripping the capability before - // Channel reads it. This hides the attachment button, disables drag- - // and-drop, and makes paste-to-upload a no-op — all three paths in - // stream-chat-react gate on channel.data.own_capabilities["upload-file"]. - if (ch.data?.own_capabilities) { - ch.data.own_capabilities = ch.data.own_capabilities.filter( - capability => capability !== 'upload-file' - ); - } - setChannel(ch); - })(); - return () => { - cancelled = true; - void ch.stopWatching(); - }; - }, [client, channelId]); - - // channelId is "default-{sandboxId}", bot user is "bot-{sandboxId}" - const sandboxId = channelId.replace(/^default-/, ''); - const botUserId = `bot-${sandboxId}`; - - if (!client || !channel) { - return ; - } - - return ( - -
- - - - - - - - - - -
-
- ); -} - -function ClawMessage() { - const botUserId = use(BotUserIdContext); - const { message } = useMessageContext(); - const isBotThinking = - message.user?.id === botUserId && !message.text?.trim() && !message.attachments?.length; - - if (isBotThinking) { - return ( -
- Thinking… -
- ); - } - - return ; -} - -function useBotOnlineStatus(botUserId: string): boolean { - const { client } = useChatContext(); - const { channel } = useChannelStateContext(); - - const getBotOnline = useCallback((): boolean => { - const member = channel.state.members[botUserId]; - return !!member?.user?.online; - }, [channel, botUserId]); - - const [online, setOnline] = useState(getBotOnline); - +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +export default function ChatTab({ sandboxId }: { sandboxId: string }) { + const router = useRouter(); useEffect(() => { - setOnline(getBotOnline()); - - const handlePresenceChange = (event: Event) => { - if (event.user?.id === botUserId) { - setOnline(!!event.user.online); - } - }; - - client.on('user.presence.changed', handlePresenceChange); - return () => { - client.off('user.presence.changed', handlePresenceChange); - }; - }, [client, botUserId, getBotOnline]); - - return online; -} - -function BotStatusBar({ botUserId }: { botUserId: string }) { - const online = useBotOnlineStatus(botUserId); - - return ( -
- - KiloClaw {online ? 'Online' : 'Offline'} -
- ); -} - -function ChatPlaceholder({ message, isError = false }: { message: string; isError?: boolean }) { - return ( -
- {message} -
- ); + router.replace(`/claw/kilo-chat?sandboxId=${sandboxId}`); + }, [router, sandboxId]); + return null; } diff --git a/apps/web/src/app/(app)/claw/components/ClawChatPage.tsx b/apps/web/src/app/(app)/claw/components/ClawChatPage.tsx index 0483c52701..a9ad8bc409 100644 --- a/apps/web/src/app/(app)/claw/components/ClawChatPage.tsx +++ b/apps/web/src/app/(app)/claw/components/ClawChatPage.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import { useKiloClawStatus } from '@/hooks/useKiloClaw'; import { useOrgKiloClawStatus } from '@/hooks/useOrgKiloClaw'; import { ClawContextProvider } from './ClawContext'; -import { ChatTab } from './ChatTab'; +import ChatTab from './ChatTab'; import { ClawConfigServiceBanner } from './ClawConfigServiceBanner'; import { BillingWrapper } from './billing/BillingWrapper'; import { SetPageTitle } from '@/components/SetPageTitle'; @@ -56,13 +56,12 @@ function ClawChatWithStatus({ organizationId }: { organizationId?: string }) { if (!status || status.status === null) return null; - const isRunning = status.status === 'running'; const chatContent = ( <> - + diff --git a/apps/web/src/app/(app)/claw/hooks/useClawHooks.ts b/apps/web/src/app/(app)/claw/hooks/useClawHooks.ts index cb336b77d2..a64f6de4bf 100644 --- a/apps/web/src/app/(app)/claw/hooks/useClawHooks.ts +++ b/apps/web/src/app/(app)/claw/hooks/useClawHooks.ts @@ -418,30 +418,6 @@ export function useClawGoogleSetupCommand(enabled: boolean) { return organizationId ? org : personal; } -// Stream Chat - -export function useClawStreamChatCredentials(enabled: boolean) { - const trpc = useTRPC(); - const { organizationId } = useClawContext(); - - const personal = useQuery({ - ...trpc.kiloclaw.getStreamChatCredentials.queryOptions(undefined, { - staleTime: 5 * 60_000, - }), - enabled: enabled && !organizationId, - }); - - const org = useQuery({ - ...trpc.organizations.kiloclaw.getStreamChatCredentials.queryOptions( - { organizationId: organizationId ?? '' }, - { staleTime: 5 * 60_000 } - ), - enabled: enabled && !!organizationId, - }); - - return organizationId ? org : personal; -} - // Kilo CLI Run export function useClawKiloCliRunStatus(runId: string | null) { diff --git a/apps/web/src/app/(app)/claw/layout.tsx b/apps/web/src/app/(app)/claw/layout.tsx index 6aa97f7666..ee24137ed7 100644 --- a/apps/web/src/app/(app)/claw/layout.tsx +++ b/apps/web/src/app/(app)/claw/layout.tsx @@ -2,7 +2,6 @@ import { getUserFromAuthOrRedirect } from '@/lib/user.server'; import { PylonWidget } from '@/components/pylon-widget'; import { PylonSupportButton } from '@/components/pylon-support-button'; import { PersonalInstancePresenceMount } from './components/PersonalInstancePresenceMount'; -import './claw-chat.css'; export default async function ClawLayout({ children }: { children: React.ReactNode }) { await getUserFromAuthOrRedirect(); diff --git a/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx b/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx index cabace358d..81e90f5079 100644 --- a/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/claw/layout.tsx @@ -1,7 +1,6 @@ import { PylonSupportButton } from '@/components/pylon-support-button'; import { PylonWidget } from '@/components/pylon-widget'; import { OrgInstancePresenceMount } from './components/OrgInstancePresenceMount'; -import '@/app/(app)/claw/claw-chat.css'; export default function OrgClawLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/apps/web/src/app/api/kiloclaw/chat-credentials/route.ts b/apps/web/src/app/api/kiloclaw/chat-credentials/route.ts deleted file mode 100644 index a52a5a171f..0000000000 --- a/apps/web/src/app/api/kiloclaw/chat-credentials/route.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NextResponse } from 'next/server'; -import { TRPCError } from '@trpc/server'; -import { getUserFromAuth } from '@/lib/user.server'; -import { KiloClawUserClient } from '@/lib/kiloclaw/kiloclaw-user-client'; -import { KiloClawApiError } from '@/lib/kiloclaw/kiloclaw-internal-client'; -import { generateApiToken, TOKEN_EXPIRY } from '@/lib/tokens'; -import { requireKiloClawAccessAtInstance } from '@/lib/kiloclaw/access-gate'; -import { - getActiveInstance, - getActiveOrgInstance, - workerInstanceId, -} from '@/lib/kiloclaw/instance-registry'; - -export async function GET() { - const { user, authFailedResponse, organizationId } = await getUserFromAuth({ - adminOnly: false, - }); - if (authFailedResponse) return authFailedResponse; - - // Personal-only billing gate — org access is gated at org membership level - // (validated by getUserFromAuth). Matches tRPC org router's - // getStreamChatCredentials which uses organizationMemberProcedure (no billing gate). - if (!organizationId) { - const instance = await getActiveInstance(user.id); - if (!instance) { - return NextResponse.json({ error: 'No active KiloClaw instance found' }, { status: 404 }); - } - - try { - await requireKiloClawAccessAtInstance(user.id, instance.id); - } catch (err) { - if (err instanceof TRPCError && err.code === 'NOT_FOUND') { - return NextResponse.json({ error: err.message }, { status: 404 }); - } - if (err instanceof TRPCError && err.code === 'FORBIDDEN') { - return NextResponse.json({ error: err.message }, { status: 403 }); - } - throw err; - } - } - - try { - const instance = organizationId - ? await getActiveOrgInstance(user.id, organizationId) - : await getActiveInstance(user.id); - - // No org instance → 404. Without this guard workerInstanceId(null) - // → undefined → the worker queries the personal DO, leaking personal - // credentials into the org context. - if (organizationId && !instance) { - return NextResponse.json( - { error: 'No active instance for this organization' }, - { status: 404 } - ); - } - - const token = generateApiToken(user, undefined, { - expiresIn: TOKEN_EXPIRY.fiveMinutes, - }); - const client = new KiloClawUserClient(token); - const creds = await client.getChatCredentials({ - userId: user.id, - instanceId: workerInstanceId(instance), - }); - return NextResponse.json(creds); - } catch (err) { - const status = err instanceof KiloClawApiError ? err.statusCode : 502; - console.error('[api/kiloclaw/chat-credentials] error:', err); - return NextResponse.json({ error: 'KiloClaw request failed' }, { status }); - } -} diff --git a/apps/web/src/hooks/useKiloClaw.ts b/apps/web/src/hooks/useKiloClaw.ts index 18ee66dcd0..d9c162c3ca 100644 --- a/apps/web/src/hooks/useKiloClaw.ts +++ b/apps/web/src/hooks/useKiloClaw.ts @@ -61,16 +61,6 @@ export function useRefreshDevicePairing() { }; } -export function useStreamChatCredentials(enabled: boolean) { - const trpc = useTRPC(); - return useQuery( - trpc.kiloclaw.getStreamChatCredentials.queryOptions(undefined, { - enabled, - staleTime: 5 * 60_000, // credentials don't change; avoid redundant refetches - }) - ); -} - export function useKiloClawGatewayStatus(enabled: boolean) { const trpc = useTRPC(); return useQuery( diff --git a/apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts b/apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts index 942948a41e..368a2bee61 100644 --- a/apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts +++ b/apps/web/src/lib/kiloclaw/kiloclaw-internal-client.ts @@ -300,37 +300,6 @@ export class KiloClawInternalClient { }); } - async getStreamChatCredentials( - userId: string, - instanceId?: string - ): Promise<{ - apiKey: string; - userId: string; - userToken: string; - channelId: string; - } | null> { - const params = new URLSearchParams({ userId }); - if (instanceId) params.set('instanceId', instanceId); - return this.request(`/api/platform/stream-chat-credentials?${params.toString()}`, undefined, { - userId, - }); - } - - async sendChatMessage( - userId: string, - message: string, - instanceId?: string - ): Promise<{ success: boolean; channelId: string }> { - return this.request( - '/api/platform/send-chat-message', - { - method: 'POST', - body: JSON.stringify({ userId, message, instanceId }), - }, - { userId } - ); - } - async getMorningBriefingStatus( userId: string, instanceId?: string diff --git a/apps/web/src/lib/kiloclaw/kiloclaw-user-client.ts b/apps/web/src/lib/kiloclaw/kiloclaw-user-client.ts index 137d33e410..92a511da65 100644 --- a/apps/web/src/lib/kiloclaw/kiloclaw-user-client.ts +++ b/apps/web/src/lib/kiloclaw/kiloclaw-user-client.ts @@ -2,12 +2,7 @@ import 'server-only'; import { KILOCLAW_API_URL } from '@/lib/config.server'; import { KiloClawApiError } from './kiloclaw-internal-client'; -import type { - UserConfigResponse, - PlatformStatusResponse, - RestartMachineResponse, - ChatCredentials, -} from './types'; +import type { UserConfigResponse, PlatformStatusResponse, RestartMachineResponse } from './types'; type RequestContext = { userId: string; instanceId?: string }; @@ -65,10 +60,6 @@ export class KiloClawUserClient { return this.request('/api/kiloclaw/status', undefined, ctx); } - async getChatCredentials(ctx?: RequestContext): Promise { - return this.request('/api/kiloclaw/chat-credentials', undefined, ctx); - } - async restartMachine( options?: { imageTag?: string }, ctx?: RequestContext diff --git a/apps/web/src/lib/kiloclaw/types.ts b/apps/web/src/lib/kiloclaw/types.ts index fe0a6fb343..27192b105b 100644 --- a/apps/web/src/lib/kiloclaw/types.ts +++ b/apps/web/src/lib/kiloclaw/types.ts @@ -584,14 +584,6 @@ export type UpdateProviderRolloutResponse = { availability: ProviderRolloutAvailability; }; -/** Stream Chat credentials for a user's KiloClaw channel */ -export type ChatCredentials = { - apiKey: string; - userId: string; - userToken: string; - channelId: string; -} | null; - /** Combined status returned by tRPC getStatus */ export type KiloClawDashboardStatus = PlatformStatusResponse & { /** Worker base URL for constructing the "Open" link. Falls back to claw.kilo.ai. */ diff --git a/apps/web/src/routers/kiloclaw-router.ts b/apps/web/src/routers/kiloclaw-router.ts index 2db8bc5bdb..ae05c81cbd 100644 --- a/apps/web/src/routers/kiloclaw-router.ts +++ b/apps/web/src/routers/kiloclaw-router.ts @@ -2413,78 +2413,6 @@ export const kiloclawRouter = createTRPCRouter({ return instance ? { instanceId: instance.id } : null; }), - getStreamChatCredentials: clawAccessProcedure.query(async ({ ctx }) => { - const instance = await getActiveInstance(ctx.user.id); - const client = new KiloClawInternalClient(); - return client.getStreamChatCredentials(ctx.user.id, workerInstanceId(instance)); - }), - - sendChatMessage: clawAccessProcedure - .input( - z.object({ - instanceId: z.string().uuid().optional(), - message: z.string().min(1).max(32_000), - }) - ) - .mutation(async ({ ctx, input }) => { - if (input.instanceId) { - // Explicit instanceId: verify ownership and non-destroyed - const [row] = await db - .select({ id: kiloclaw_instances.id }) - .from(kiloclaw_instances) - .where( - and( - eq(kiloclaw_instances.id, input.instanceId), - eq(kiloclaw_instances.user_id, ctx.user.id), - isNull(kiloclaw_instances.destroyed_at) - ) - ) - .limit(1); - if (!row) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'No active KiloClaw instance found', - }); - } - } else { - // No instanceId: verify the user has any active instance - const instance = await getActiveInstance(ctx.user.id); - if (!instance) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'No active KiloClaw instance found', - }); - } - } - - const client = new KiloClawInternalClient(); - try { - return await client.sendChatMessage(ctx.user.id, input.message, input.instanceId); - } catch (err) { - if (err instanceof KiloClawApiError) { - const { message } = getKiloClawApiErrorPayload(err); - const code = - err.statusCode === 400 - ? 'BAD_REQUEST' - : err.statusCode === 403 - ? 'FORBIDDEN' - : err.statusCode === 404 - ? 'NOT_FOUND' - : err.statusCode === 503 - ? 'PRECONDITION_FAILED' - : 'INTERNAL_SERVER_ERROR'; - throw new TRPCError({ - code, - message: message ?? 'Failed to send chat message', - }); - } - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to send chat message', - }); - } - }), - getMorningBriefingStatus: clawAccessProcedure.query(async ({ ctx }) => { const instance = await getActiveInstance(ctx.user.id); const client = new KiloClawInternalClient(); diff --git a/apps/web/src/routers/kiloclaw-send-chat-message.test.ts b/apps/web/src/routers/kiloclaw-send-chat-message.test.ts deleted file mode 100644 index a7fdc7b82e..0000000000 --- a/apps/web/src/routers/kiloclaw-send-chat-message.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { describe, expect, it, beforeAll, beforeEach, jest } from '@jest/globals'; -import { db, cleanupDbForTest } from '@/lib/drizzle'; -import { kiloclaw_instances, kiloclaw_subscriptions } from '@kilocode/db/schema'; -import { insertTestUser } from '@/tests/helpers/user.helper'; -import type { User } from '@kilocode/db/schema'; - -// ── Mocks ────────────────────────────────────────────────────────────────── - -// Mock KiloClawInternalClient to avoid real HTTP calls -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mockSendChatMessage: jest.Mock = jest.fn(); -jest.mock('@/lib/kiloclaw/kiloclaw-internal-client', () => { - // Import the real KiloClawApiError so tests can throw it - const actual: Record = jest.requireActual( - '@/lib/kiloclaw/kiloclaw-internal-client' - ); - return { - KiloClawInternalClient: jest.fn().mockImplementation(() => ({ - sendChatMessage: mockSendChatMessage, - })), - KiloClawApiError: actual.KiloClawApiError, - }; -}); - -jest.mock('next/headers', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fn = jest.fn as (...args: any[]) => jest.Mock; - return { - cookies: fn().mockResolvedValue({ get: fn() }), - headers: fn().mockReturnValue(new Map()), - }; -}); - -// ── Dynamic imports (after mocks) ────────────────────────────────────────── - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let createCallerForUser: (userId: string) => Promise; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let KiloClawApiError: any; - -beforeAll(async () => { - const mod = await import('@/routers/test-utils'); - createCallerForUser = mod.createCallerForUser; - const clientMod = await import('@/lib/kiloclaw/kiloclaw-internal-client'); - KiloClawApiError = clientMod.KiloClawApiError; -}); - -// ── Helpers ──────────────────────────────────────────────────────────────── - -let user: User; -let otherUser: User; - -beforeEach(async () => { - await cleanupDbForTest(); - mockSendChatMessage.mockReset(); - - user = await insertTestUser({ - google_user_email: `sendchat-test-${Math.random()}@example.com`, - }); - otherUser = await insertTestUser({ - google_user_email: `sendchat-other-${Math.random()}@example.com`, - }); -}); - -async function createActiveInstance(userId: string): Promise { - const [row] = await db - .insert(kiloclaw_instances) - .values({ - user_id: userId, - sandbox_id: `sandbox-${userId.slice(0, 8)}`, - }) - .returning(); - return row.id; -} - -async function createDestroyedInstance(userId: string): Promise { - const [row] = await db - .insert(kiloclaw_instances) - .values({ - user_id: userId, - sandbox_id: `sandbox-destroyed-${userId.slice(0, 8)}`, - destroyed_at: new Date().toISOString(), - }) - .returning(); - return row.id; -} - -async function grantKiloClawAccess(userId: string, instanceId: string): Promise { - await db.insert(kiloclaw_subscriptions).values({ - user_id: userId, - instance_id: instanceId, - plan: 'standard', - status: 'active', - stripe_subscription_id: `sub_test_${crypto.randomUUID()}`, - }); -} - -// ── Tests ────────────────────────────────────────────────────────────────── - -describe('kiloclaw.sendChatMessage', () => { - describe('billing gate (clawAccessProcedure)', () => { - it('rejects users without KiloClaw access', async () => { - await createActiveInstance(user.id); - const caller = await createCallerForUser(user.id); - - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'FORBIDDEN', - }); - }); - - it('allows users with active subscription', async () => { - const instanceId = await createActiveInstance(user.id); - await grantKiloClawAccess(user.id, instanceId); - mockSendChatMessage.mockResolvedValue({ success: true, channelId: 'chan-1' }); - - const caller = await createCallerForUser(user.id); - const result = await caller.kiloclaw.sendChatMessage({ message: 'test' }); - expect(result.success).toBe(true); - }); - }); - - describe('ownership validation', () => { - it('rejects when user has access but no active instance (no instanceId)', async () => { - const destroyedInstanceId = await createDestroyedInstance(user.id); - await grantKiloClawAccess(user.id, destroyedInstanceId); - const caller = await createCallerForUser(user.id); - - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'NOT_FOUND', - message: 'No active KiloClaw instance found', - }); - }); - - it('rejects when instanceId belongs to another user', async () => { - const accessInstanceId = await createActiveInstance(user.id); - await grantKiloClawAccess(user.id, accessInstanceId); - const otherInstanceId = await createActiveInstance(otherUser.id); - - const caller = await createCallerForUser(user.id); - await expect( - caller.kiloclaw.sendChatMessage({ instanceId: otherInstanceId, message: 'test' }) - ).rejects.toMatchObject({ - code: 'NOT_FOUND', - message: 'No active KiloClaw instance found', - }); - }); - - it('rejects when instanceId points to a destroyed instance', async () => { - const accessInstanceId = await createActiveInstance(user.id); - await grantKiloClawAccess(user.id, accessInstanceId); - const destroyedId = await createDestroyedInstance(user.id); - - const caller = await createCallerForUser(user.id); - await expect( - caller.kiloclaw.sendChatMessage({ instanceId: destroyedId, message: 'test' }) - ).rejects.toMatchObject({ - code: 'NOT_FOUND', - message: 'No active KiloClaw instance found', - }); - }); - - it('allows sending to own active instance by instanceId', async () => { - const instanceId = await createActiveInstance(user.id); - await grantKiloClawAccess(user.id, instanceId); - mockSendChatMessage.mockResolvedValue({ success: true, channelId: 'chan-1' }); - - const caller = await createCallerForUser(user.id); - const result = await caller.kiloclaw.sendChatMessage({ - instanceId, - message: 'hello', - }); - expect(result.success).toBe(true); - expect(mockSendChatMessage).toHaveBeenCalledWith(user.id, 'hello', instanceId); - }); - }); - - describe('error translation (KiloClawApiError → TRPCError)', () => { - beforeEach(async () => { - const instanceId = await createActiveInstance(user.id); - await grantKiloClawAccess(user.id, instanceId); - }); - - it('maps worker 400 to tRPC BAD_REQUEST', async () => { - mockSendChatMessage.mockRejectedValue(new KiloClawApiError(400, '{"error":"bad input"}')); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'BAD_REQUEST', - message: 'bad input', - }); - }); - - it('maps worker 403 to tRPC FORBIDDEN', async () => { - mockSendChatMessage.mockRejectedValue(new KiloClawApiError(403, '{"error":"forbidden"}')); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'FORBIDDEN', - message: 'forbidden', - }); - }); - - it('maps worker 404 to tRPC NOT_FOUND', async () => { - mockSendChatMessage.mockRejectedValue( - new KiloClawApiError(404, '{"error":"Stream Chat is not set up for this instance"}') - ); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'NOT_FOUND', - message: 'Stream Chat is not set up for this instance', - }); - }); - - it('maps worker 503 to tRPC PRECONDITION_FAILED', async () => { - mockSendChatMessage.mockRejectedValue( - new KiloClawApiError(503, '{"error":"Stream Chat is not configured"}') - ); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'PRECONDITION_FAILED', - message: 'Stream Chat is not configured', - }); - }); - - it('maps unknown worker errors to tRPC INTERNAL_SERVER_ERROR', async () => { - mockSendChatMessage.mockRejectedValue(new KiloClawApiError(502, '')); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to send chat message', - }); - }); - - it('maps non-KiloClawApiError to tRPC INTERNAL_SERVER_ERROR', async () => { - mockSendChatMessage.mockRejectedValue(new Error('network error')); - - const caller = await createCallerForUser(user.id); - await expect(caller.kiloclaw.sendChatMessage({ message: 'test' })).rejects.toMatchObject({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Failed to send chat message', - }); - }); - }); -}); diff --git a/apps/web/src/routers/organizations/organization-kiloclaw-router.ts b/apps/web/src/routers/organizations/organization-kiloclaw-router.ts index b2fbe70d03..831a8d7734 100644 --- a/apps/web/src/routers/organizations/organization-kiloclaw-router.ts +++ b/apps/web/src/routers/organizations/organization-kiloclaw-router.ts @@ -1130,39 +1130,6 @@ export const organizationKiloclawRouter = createTRPCRouter({ return { success: true }; }), - // ── Stream Chat ──────────────────────────────────────────────── - - getStreamChatCredentials: organizationMemberProcedure.query(async ({ ctx, input }) => { - const instance = await getActiveOrgInstance(ctx.user.id, input.organizationId); - const client = new KiloClawInternalClient(); - return client.getStreamChatCredentials(ctx.user.id, workerInstanceId(instance)); - }), - - sendChatMessage: organizationMemberMutationProcedure - .input(z.object({ organizationId: z.uuid(), message: z.string().min(1).max(32_000) })) - .mutation(async ({ ctx, input }) => { - const instance = await requireOrgInstance(ctx.user.id, input.organizationId); - const client = new KiloClawInternalClient(); - try { - return await client.sendChatMessage(ctx.user.id, input.message, instance.id); - } catch (err) { - if (err instanceof KiloClawApiError) { - const { message } = getKiloClawApiErrorPayload(err); - const code = - err.statusCode === 404 - ? 'NOT_FOUND' - : err.statusCode === 503 - ? 'PRECONDITION_FAILED' - : 'INTERNAL_SERVER_ERROR'; - throw new TRPCError({ - code, - message: message ?? 'Failed to send chat message', - }); - } - throw err; - } - }), - getMorningBriefingStatus: organizationMemberProcedure.query(async ({ ctx, input }) => { const instance = await requireOrgInstance(ctx.user.id, input.organizationId); const client = new KiloClawInternalClient(); diff --git a/package.json b/package.json index fa2dd4d7cd..a8a686518e 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,6 @@ }, "patchedDependencies": { "@storybook/nextjs@9.1.20": "patches/@storybook__nextjs@9.1.20.patch", - "@gorhom/bottom-sheet@5.1.8": "patches/@gorhom__bottom-sheet@5.1.8.patch", - "stream-chat-react-native-core": "patches/stream-chat-react-native-core.patch", "expo-server-sdk": "patches/expo-server-sdk.patch" }, "onlyBuiltDependencies": [ @@ -77,7 +75,6 @@ "esbuild", "libpq", "protobufjs", - "stream-chat-react-native-core", "workerd" ] } diff --git a/patches/@gorhom__bottom-sheet@5.1.8.patch b/patches/@gorhom__bottom-sheet@5.1.8.patch deleted file mode 100644 index c73a467bdc..0000000000 --- a/patches/@gorhom__bottom-sheet@5.1.8.patch +++ /dev/null @@ -1,97 +0,0 @@ -diff --git a/lib/commonjs/hooks/useBoundingClientRect.js b/lib/commonjs/hooks/useBoundingClientRect.js -index b4a90b76ee55bf2cad9cf461017621b1ddab0fe1..3140ff9d1d3bd9ae28fc5124ac642af0eda74ea7 100644 ---- a/lib/commonjs/hooks/useBoundingClientRect.js -+++ b/lib/commonjs/hooks/useBoundingClientRect.js -@@ -45,19 +45,25 @@ function useBoundingClientRect(ref, handler) { - return; - } - -+ if ( - // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -- if (ref.current.unstable_getBoundingClientRect !== null) { -+ ref.current.unstable_getBoundingClientRect !== null && -+ // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -+ typeof ref.current.unstable_getBoundingClientRect === 'function') { - // @ts-ignore https://github.com/facebook/react/commit/53b1f69ba -- const layout = ref.current.unstable_getBoundingClientRect(); -+ var layout = ref.current.unstable_getBoundingClientRect(); - handler(layout); - return; - } - -+ if ( - // @ts-ignore once it `unstable_getBoundingClientRect` gets stable 🤞. -- if (ref.current.getBoundingClientRect !== null) { -+ ref.current.getBoundingClientRect !== null && -+ // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. -+ typeof ref.current.getBoundingClientRect === 'function') { - // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. -- const layout = ref.current.getBoundingClientRect(); -- handler(layout); -+ var _layout = ref.current.getBoundingClientRect(); -+ handler(_layout); - } - }); - } -diff --git a/lib/module/hooks/useBoundingClientRect.js b/lib/module/hooks/useBoundingClientRect.js -index a723aede9d4cfbb46f5985c531e0dae8f517aba8..2da7edb539836fef15f9ed29d38cfe4608afd121 100644 ---- a/lib/module/hooks/useBoundingClientRect.js -+++ b/lib/module/hooks/useBoundingClientRect.js -@@ -41,16 +41,22 @@ export function useBoundingClientRect(ref, handler) { - return; - } - -+ if ( - // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -- if (ref.current.unstable_getBoundingClientRect !== null) { -+ ref.current.unstable_getBoundingClientRect !== null && -+ // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -+ typeof ref.current.unstable_getBoundingClientRect === 'function') { - // @ts-ignore https://github.com/facebook/react/commit/53b1f69ba - const layout = ref.current.unstable_getBoundingClientRect(); - handler(layout); - return; - } - -+ if ( - // @ts-ignore once it `unstable_getBoundingClientRect` gets stable 🤞. -- if (ref.current.getBoundingClientRect !== null) { -+ ref.current.getBoundingClientRect !== null && -+ // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. -+ typeof ref.current.getBoundingClientRect === 'function') { - // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. - const layout = ref.current.getBoundingClientRect(); - handler(layout); -diff --git a/src/hooks/useBoundingClientRect.ts b/src/hooks/useBoundingClientRect.ts -index cc85c8ced2de8ec514360368ed20af733f8f9aec..9abe8294d6004be4500871e46a3621a9e5b9d93b 100644 ---- a/src/hooks/useBoundingClientRect.ts -+++ b/src/hooks/useBoundingClientRect.ts -@@ -55,16 +55,24 @@ export function useBoundingClientRect( - return; - } - -- // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -- if (ref.current.unstable_getBoundingClientRect !== null) { -+ if ( -+ // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -+ ref.current.unstable_getBoundingClientRect !== null && -+ // @ts-ignore 👉 https://github.com/facebook/react/commit/53b1f69ba -+ typeof ref.current.unstable_getBoundingClientRect === 'function' -+ ) { - // @ts-ignore https://github.com/facebook/react/commit/53b1f69ba - const layout = ref.current.unstable_getBoundingClientRect(); - handler(layout); - return; - } - -- // @ts-ignore once it `unstable_getBoundingClientRect` gets stable 🤞. -- if (ref.current.getBoundingClientRect !== null) { -+ if ( -+ // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. -+ ref.current.getBoundingClientRect !== null && -+ // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. -+ typeof ref.current.getBoundingClientRect === 'function' -+ ) { - // @ts-ignore once it `unstable_getBoundingClientRect` gets stable. - const layout = ref.current.getBoundingClientRect(); - handler(layout); diff --git a/patches/stream-chat-react-native-core.patch b/patches/stream-chat-react-native-core.patch deleted file mode 100644 index 789cb69c31..0000000000 --- a/patches/stream-chat-react-native-core.patch +++ /dev/null @@ -1,65 +0,0 @@ -diff --git a/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx b/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx -index 61e82d37fdfa6e7d5199330549f35e1b220d867f..69abf2e32aa96b995f11f1484c5e33ac03cee6a9 100644 ---- a/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx -+++ b/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx -@@ -1,6 +1,7 @@ - import React from 'react'; - --import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native'; -+import { Alert, StyleSheet, Text, View } from 'react-native'; -+import { Image as ExpoImage } from 'expo-image'; - - import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 'stream-chat'; - -@@ -67,8 +68,7 @@ const AttachmentVideo = (props: AttachmentPickerItemType) => { - - return ( - -- { - image, - ]} - > -+ - {selected && ( - - -@@ -91,7 +92,7 @@ const AttachmentVideo = (props: AttachmentPickerItemType) => { - - ) : null} - -- -+
- - ); - }; -@@ -138,8 +139,7 @@ const AttachmentImage = (props: AttachmentPickerItemType) => { - - return ( - -- { - image, - ]} - > -+ - {selected && ( - - - - )} -- -+
- - ); - }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fde8db827f..5fd5cf94ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,9 +51,6 @@ catalogs: p-limit: specifier: ^7.3.0 version: 7.3.0 - stream-chat: - specifier: ^9.38.0 - version: 9.38.0 stripe: specifier: ^19.3.0 version: 19.3.0 @@ -90,18 +87,12 @@ overrides: axios: '>=1.15.0 <2' patchedDependencies: - '@gorhom/bottom-sheet@5.1.8': - hash: c5eaae9a28f5662f32d66e0129609680309eddc44720d6a2b1e02bbc2b5dd11f - path: patches/@gorhom__bottom-sheet@5.1.8.patch '@storybook/nextjs@9.1.20': hash: e1857649664eed8f87877c352d277c90d4af5a58d0ad931105f033c8c08165c1 path: patches/@storybook__nextjs@9.1.20.patch expo-server-sdk: hash: 7850520582b5b394397b35d1ea195192fe78589d8a6a748fe15177b818c4ed0b path: patches/expo-server-sdk.patch - stream-chat-react-native-core: - hash: 6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0 - path: patches/stream-chat-react-native-core.patch importers: @@ -239,9 +230,6 @@ importers: expo-image: specifier: ~55.0.8 version: 55.0.8(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - expo-image-manipulator: - specifier: ~55.0.14 - version: 55.0.14(expo@55.0.12) expo-image-picker: specifier: ~55.0.17 version: 55.0.17(expo@55.0.12) @@ -326,21 +314,12 @@ importers: sonner-native: specifier: ^0.23.1 version: 0.23.1(53175ba88151f39b99a3b76a61c65c1d) - stream-chat: - specifier: 'catalog:' - version: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - stream-chat-expo: - specifier: ^8.13.7 - version: 8.13.7(e673e8bffb1896cc06607271df6a38dc) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 tailwindcss: specifier: ^4.2.2 version: 4.2.2 - zod: - specifier: 'catalog:' - version: 4.3.6 devDependencies: '@sentry/cli': specifier: ^3.3.4 @@ -817,12 +796,6 @@ importers: sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - stream-chat: - specifier: ^9.38.0 - version: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - stream-chat-react: - specifier: ^13.14.2 - version: 13.14.2(@emoji-mart/data@1.2.1)(@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.4))(@types/react@19.2.14)(emoji-mart@5.6.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(stream-chat@9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3) stripe: specifier: 'catalog:' version: 19.3.0(@types/node@24.12.0) @@ -1604,7 +1577,7 @@ importers: version: 7.0.0-dev.20260319.1 jest: specifier: ^30.3.0 - version: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) + version: 30.3.0(@types/node@24.12.0)(esbuild-register@3.6.0(esbuild@0.27.4)) typescript: specifier: 'catalog:' version: 5.9.3 @@ -3259,9 +3232,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@braintree/sanitize-url@6.0.4': - resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} - '@chat-adapter/shared@4.20.1': resolution: {integrity: sha512-UawGmT7O+3vxvaU9f+lc0PVQKU+TvE0PUxa0zL43qH1rqGkosngtT3cOOhW6JOx+rxt3jox2a99xr8hnJPkshA==} @@ -3930,36 +3900,9 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.19': - resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} - peerDependencies: - react: '>=17.0.0' - react-dom: '>=17.0.0' - '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@gorhom/bottom-sheet@5.1.8': - resolution: {integrity: sha512-QuYIVjn3K9bW20n5bgOSjvxBYoWG4YQXiLGOheEAMgISuoT6sMcA270ViSkkb0fenPxcIOwzCnFNuxmr739T9A==} - peerDependencies: - '@types/react': '*' - '@types/react-native': '*' - react: '*' - react-native: '*' - react-native-gesture-handler: '>=2.16.1' - react-native-reanimated: '>=3.16.0 || >=4.0.0-' - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-native': - optional: true - - '@gorhom/portal@1.0.14': - resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==} - peerDependencies: - react: '*' - react-native: '*' - '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -6102,30 +6045,6 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/focus@3.21.5': - resolution: {integrity: sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/interactions@3.27.1': - resolution: {integrity: sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.10': - resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/utils@3.33.1': - resolution: {integrity: sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@react-native-community/netinfo@11.5.2': resolution: {integrity: sha512-/g0m65BtX9HU+bPiCH2517bOHpEIUsGrWFXDzi1a5nNKn5KujQgm04WhL7/OSXWKHyrT8VVtUoJA0XKRxueBpQ==} peerDependencies: @@ -6238,19 +6157,6 @@ packages: '@react-navigation/routers@7.5.3': resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} - '@react-stately/flags@3.1.2': - resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - - '@react-stately/utils@3.11.0': - resolution: {integrity: sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/shared@3.33.1': - resolution: {integrity: sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@redis/bloom@5.11.0': resolution: {integrity: sha512-KYiVilAhAFN3057afUb/tfYJpsEyTkQB+tQcn5gVVA7DgcNOAj8lLxe4j8ov8BF6I9C1Fe/kwlbuAICcTMX8Lw==} engines: {node: '>= 18'} @@ -7333,14 +7239,6 @@ packages: peerDependencies: storybook: ^9.1.17 - '@stream-io/escape-string-regexp@5.0.1': - resolution: {integrity: sha512-qIaSrzJXieZqo2fZSYTdzwSbZgHHsT3tkd646vvZhh4fr+9nO4NlvqGmPF43Y+OfZiWf+zYDFgNiPGG5+iZulQ==} - engines: {node: '>=12'} - - '@stream-io/transliterate@1.5.5': - resolution: {integrity: sha512-r6Qp0HylAZhHNWHxU1nGfRI2Dtkbs1iqLCnOp1bvKhv8yj0/sEUigN0dk0LGPbE4I7zDO3tppyd7PaTPBvvJkg==} - engines: {node: '>=12'} - '@streamparser/json@0.0.22': resolution: {integrity: sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==} @@ -8037,15 +7935,6 @@ packages: '@opentelemetry/sdk-metrics': '>=2.0.0 <3.0.0' '@opentelemetry/sdk-trace-base': '>=2.0.0 <3.0.0' - '@virtuoso.dev/react-urx@0.2.13': - resolution: {integrity: sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16' - - '@virtuoso.dev/urx@0.2.13': - resolution: {integrity: sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==} - '@vitest/coverage-v8@4.1.0': resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} peerDependencies: @@ -8434,10 +8323,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - attr-accept@2.2.5: - resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} - engines: {node: '>=4'} - auto-bind@5.0.1: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9280,9 +9165,6 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -9703,9 +9585,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} @@ -10077,11 +9956,6 @@ packages: peerDependencies: expo: '*' - expo-image-manipulator@55.0.14: - resolution: {integrity: sha512-j46l8ok7lWrDvgYaIJTjrSg7zBuDrGIbR7TFI6VnI/IfFUi/CGqMfw1Ks+2wzXB1Vcs+LLH8OLv+WR1y+/zVKg==} - peerDependencies: - expo: '*' - expo-image-picker@55.0.17: resolution: {integrity: sha512-oCayiw6ZMKDnUGVPFhQ1j0Cg0ZvzSDWwuVm0QSX+AkdqBuRv/n3SB3ZTVW2M+lR6zU/aTtVTduqlNnVyv4CrhA==} peerDependencies: @@ -10346,10 +10220,6 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-selector@2.1.2: - resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} - engines: {node: '>= 12'} - filesize@10.1.6: resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} engines: {node: '>= 10.4.0'} @@ -10415,9 +10285,6 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - fix-webm-duration@1.0.6: - resolution: {integrity: sha512-zVAqi4gE+8ywxJuAyV/rlJVX6CMtvyapEbQx6jyoeX9TMjdqAlt/FdG5d7rXSSkDVzTvS0H7CtwzHcH/vh4FPA==} - flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -10682,12 +10549,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-find-and-replace@5.0.1: - resolution: {integrity: sha512-S12fTskO3Hf2IGCBWXs1UcXT8GEJ3jmvmPZJctkRwfl3a8jnGi8aFYT8kd2zcEH+VE0qcGgKF0ewt5BPAsfIhw==} - - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -10801,14 +10662,6 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@25.10.4: - resolution: {integrity: sha512-XsE/6eawy090meuFU0BTY9BtmWr1m9NSwLr0NK7/A04LA58wdAvDsi9WNOJ40Qb1E9NIPbvnVLZEN2fWDd3/3Q==} - peerDependencies: - typescript: ^5 - peerDependenciesMeta: - typescript: - optional: true - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -10873,9 +10726,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -10910,9 +10760,6 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} - intl-pluralrules@2.0.1: - resolution: {integrity: sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg==} - invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -11088,11 +10935,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -11654,12 +11496,6 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - - load-script@1.0.0: - resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -11690,9 +11526,6 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.deburr@4.1.0: - resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==} - lodash.flattendeep@4.4.0: resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} @@ -11720,9 +11553,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -11732,9 +11562,6 @@ packages: lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash.uniqby@4.7.0: - resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} - lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -12132,11 +11959,6 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - mime@4.1.0: - resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} - engines: {node: '>=16'} - hasBin: true - mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} @@ -12693,9 +12515,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path@0.12.7: - resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -12958,9 +12777,6 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -13066,12 +12882,6 @@ packages: peerDependencies: react: ^19.2.4 - react-dropzone@14.4.1: - resolution: {integrity: sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==} - engines: {node: '>= 10.13'} - peerDependencies: - react: '>= 16.8 || 18.0.0' - react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -13081,11 +12891,6 @@ packages: peerDependencies: react: '>=17.0.0' - react-image-gallery@1.2.12: - resolution: {integrity: sha512-JIh85lh0Av/yewseGJb/ycg00Y/weQiZEC/BQueC2Z5jnYILGB6mkxnrOevNhsM2NdZJpvcDekCluhy6uzEoTA==} - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13101,12 +12906,6 @@ packages: '@types/react': '>=18' react: '>=18' - react-markdown@9.1.0: - resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - react-native-appsflyer@6.17.9: resolution: {integrity: sha512-oEddwSsVL8D3ki8ayWZV34GyORAxvL1BXq3mL1xB8Hdfg+xxLyjAXSvWbj0t3E3NJ2KrgLRf/hbTlsPltfo/Uw==} @@ -13136,12 +12935,6 @@ packages: react: '*' react-native: '*' - react-native-lightbox@0.7.0: - resolution: {integrity: sha512-HS3T4WlCd0Gb3us2d6Jse5m6KjNhngnKm35Wapq30WtQa9s+/VMmtuktbGPGaWtswcDyOj6qByeJBw9W80iPCA==} - - react-native-markdown-package@1.8.2: - resolution: {integrity: sha512-F3z/p0XfY6Nu9NlXQx1pYcPdz7Y37NRcAKTN+yb9nwRi8BW75mdc3uaBrM13PDVUlL0hbfTL7FuoAdSbsyB5vg==} - react-native-marked@8.0.1: resolution: {integrity: sha512-lUAM/w9AxY54PP2BKHnDiarJ1+8s9R8HzkjnIrCkZO1fOSAdFn0XsAVMM4fGOP8QM4wIZcJhhvaW/5K7oGy5aQ==} engines: {node: '>=18'} @@ -13181,11 +12974,6 @@ packages: react: '*' react-native: '*' - react-native-url-polyfill@2.0.0: - resolution: {integrity: sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==} - peerDependencies: - react-native: '*' - react-native-worklets@0.7.2: resolution: {integrity: sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog==} peerDependencies: @@ -13204,11 +12992,6 @@ packages: '@types/react': optional: true - react-player@2.10.1: - resolution: {integrity: sha512-ova0jY1Y1lqLYxOehkzbNEju4rFXYVkr5rdGD71nsiG4UKPzRXQPTd3xjoDssheoMNjZ51mjT5ysTrdQ2tEvsg==} - peerDependencies: - react: '>=16.6.0' - react-reconciler@0.33.0: resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} engines: {node: '>=0.10.0'} @@ -13261,25 +13044,12 @@ packages: '@types/react': optional: true - react-textarea-autosize@8.5.9: - resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} - engines: {node: '>=10'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-turnstile@1.1.5: resolution: {integrity: sha512-VTL5OeHAatzCEVQxAZox70/TPmhKxEbNgtr++dg+8zm9QrWKuoU9E0+7gqmycOSCDZuJFzvMMLKQb5PVUPLV6w==} peerDependencies: react: '>= 16.13.1' react-dom: '>= 16.13.1' - react-virtuoso@2.19.1: - resolution: {integrity: sha512-zF6MAwujNGy2nJWCx/Df92ay/RnV2Kj4glUZfdyadI4suAn0kAZHB1BeI7yPFVp2iSccLzFlszhakWyr+fJ4Dw==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16 || >=17 || >= 18' - react-dom: '>=16 || >=17 || >= 18' - react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -13741,9 +13511,6 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-markdown@0.7.3: - resolution: {integrity: sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==} - simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} @@ -13892,90 +13659,6 @@ packages: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} - stream-chat-expo@8.13.7: - resolution: {integrity: sha512-gYHDqiLjTTJx2HRNgYU8SD7mt6keseJOe3NizHueBL/83kGf0iioHr9xOyqjIJ/OKIighZnqUwgOVQNMfoKNEQ==} - peerDependencies: - expo: '>=51.0.0' - expo-audio: '*' - expo-av: '*' - expo-clipboard: '*' - expo-document-picker: '*' - expo-file-system: '*' - expo-haptics: '*' - expo-image-manipulator: '*' - expo-image-picker: '*' - expo-media-library: '*' - expo-sharing: '*' - expo-video: '*' - peerDependenciesMeta: - expo-audio: - optional: true - expo-av: - optional: true - expo-clipboard: - optional: true - expo-document-picker: - optional: true - expo-file-system: - optional: true - expo-haptics: - optional: true - expo-image-picker: - optional: true - expo-media-library: - optional: true - expo-sharing: - optional: true - expo-video: - optional: true - - stream-chat-react-native-core@8.13.7: - resolution: {integrity: sha512-77lgyDArQaH04lcgJ0uUhNc0fm8oGvTFDs5Tjb7nn9ym+A4yUV6q4mKEtPpgqat+fhGvEBy6vnRG2F1Qx3Vr7A==} - peerDependencies: - '@emoji-mart/data': '>=1.1.0' - '@op-engineering/op-sqlite': '>=14.0.0' - '@react-native-community/netinfo': '>=11.3.1' - '@shopify/flash-list': '>=2.1.0' - emoji-mart: '>=5.4.0' - react-native: '>=0.73.0' - react-native-gesture-handler: '>=2.18.0' - react-native-reanimated: '>=3.16.0' - react-native-safe-area-context: '>=5.4.1' - react-native-svg: '>=15.8.0' - peerDependenciesMeta: - '@emoji-mart/data': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@shopify/flash-list': - optional: true - emoji-mart: - optional: true - - stream-chat-react@13.14.2: - resolution: {integrity: sha512-2q6BuvHryfEzq6N8vs2e8b1iW4O7Aa72fMkhXqsGuP0jT6Vl8x4E+yEIHLlUho6jXDcGQGirqgETuRX3X53odw==} - peerDependencies: - '@breezystack/lamejs': ^1.2.7 - '@emoji-mart/data': ^1.1.0 - '@emoji-mart/react': ^1.1.0 - emoji-mart: ^5.4.0 - react: ^19.0.0 || ^18.0.0 || ^17.0.0 - react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 - stream-chat: ^9.27.2 - peerDependenciesMeta: - '@breezystack/lamejs': - optional: true - '@emoji-mart/data': - optional: true - '@emoji-mart/react': - optional: true - emoji-mart: - optional: true - - stream-chat@9.38.0: - resolution: {integrity: sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg==} - engines: {node: '>=18'} - stream-http@3.2.0: resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} @@ -14162,9 +13845,6 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} - tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -14515,9 +14195,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-builder@4.0.0: - resolution: {integrity: sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -14583,38 +14260,11 @@ packages: '@types/react': optional: true - use-composed-ref@1.4.0: - resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-isomorphic-layout-effect@1.2.1: - resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - use-latest-callback@0.2.6: resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} peerDependencies: react: '>=16.8' - use-latest@1.3.0: - resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -14637,9 +14287,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.10.4: - resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} @@ -14855,10 +14502,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -14903,10 +14546,6 @@ packages: whatwg-url-minimum@0.1.1: resolution: {integrity: sha512-u2FNVjFVFZhdjb502KzXy1gKn1mEisQRJssmSJT8CPhZdZa0AP6VCbWlXERKyGu0l09t0k50FiDiralpGhBxgA==} - whatwg-url-without-unicode@8.0.0-3: - resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} - engines: {node: '>=10'} - whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -16635,8 +16274,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@braintree/sanitize-url@6.0.4': {} - '@chat-adapter/shared@4.20.1': dependencies: chat: 4.20.1 @@ -17472,33 +17109,8 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@floating-ui/react@0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@floating-ui/utils': 0.2.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - tabbable: 6.4.0 - '@floating-ui/utils@0.2.11': {} - '@gorhom/bottom-sheet@5.1.8(patch_hash=c5eaae9a28f5662f32d66e0129609680309eddc44720d6a2b1e02bbc2b5dd11f)(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)': - dependencies: - '@gorhom/portal': 1.0.14(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - invariant: 2.2.4 - react: 19.2.0 - react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) - react-native-gesture-handler: 2.30.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - react-native-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - optionalDependencies: - '@types/react': 19.2.14 - - '@gorhom/portal@1.0.14(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)': - dependencies: - nanoid: 3.3.11 - react: 19.2.0 - react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) - '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -19773,42 +19385,6 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-aria/focus@3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/interactions@3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.15 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/ssr@3.9.10(react@19.2.4)': - dependencies: - '@swc/helpers': 0.5.15 - react: 19.2.4 - - '@react-aria/utils@3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.15 - clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - '@react-native-community/netinfo@11.5.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)': dependencies: react: 19.2.0 @@ -20003,19 +19579,6 @@ snapshots: dependencies: nanoid: 3.3.11 - '@react-stately/flags@3.1.2': - dependencies: - '@swc/helpers': 0.5.15 - - '@react-stately/utils@3.11.0(react@19.2.4)': - dependencies: - '@swc/helpers': 0.5.15 - react: 19.2.4 - - '@react-types/shared@3.33.1(react@19.2.4)': - dependencies: - react: 19.2.4 - '@redis/bloom@5.11.0(@redis/client@5.11.0)': dependencies: '@redis/client': 5.11.0 @@ -21571,15 +21134,6 @@ snapshots: dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@stream-io/escape-string-regexp@5.0.1': - optional: true - - '@stream-io/transliterate@1.5.5': - dependencies: - '@stream-io/escape-string-regexp': 5.0.1 - lodash.deburr: 4.1.0 - optional: true - '@streamparser/json@0.0.22': {} '@stripe/stripe-js@5.10.0': {} @@ -22198,13 +21752,6 @@ snapshots: '@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@virtuoso.dev/react-urx@0.2.13(react@19.2.4)': - dependencies: - '@virtuoso.dev/urx': 0.2.13 - react: 19.2.4 - - '@virtuoso.dev/urx@0.2.13': {} - '@vitest/coverage-v8@4.1.0(vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -22673,8 +22220,6 @@ snapshots: asynckit@0.4.0: {} - attr-accept@2.2.5: {} - auto-bind@5.0.1: {} available-typed-arrays@1.0.7: @@ -23646,8 +23191,6 @@ snapshots: date-fns@4.1.0: {} - dayjs@1.11.13: {} - dayjs@1.11.20: {} debounce@1.2.1: {} @@ -24002,8 +23545,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - emojis-list@3.0.0: {} encodeurl@1.0.2: {} @@ -24406,11 +23947,6 @@ snapshots: dependencies: expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(bufferutil@4.1.0)(expo-router@55.0.11)(react-dom@19.2.4(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - expo-image-manipulator@55.0.14(expo@55.0.12): - dependencies: - expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(bufferutil@4.1.0)(expo-router@55.0.11)(react-dom@19.2.4(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - expo-image-loader: 55.0.0(expo@55.0.12) - expo-image-picker@55.0.17(expo@55.0.12): dependencies: expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(bufferutil@4.1.0)(expo-router@55.0.11)(react-dom@19.2.4(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -24752,10 +24288,6 @@ snapshots: fflate@0.8.2: {} - file-selector@2.1.2: - dependencies: - tslib: 2.8.1 - filesize@10.1.6: {} filing-cabinet@5.2.0: @@ -24850,8 +24382,6 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - fix-webm-duration@1.0.6: {} - flat-cache@3.2.0: dependencies: flatted: 3.4.1 @@ -25132,17 +24662,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-find-and-replace@5.0.1: - dependencies: - '@types/hast': 3.0.4 - escape-string-regexp: 5.0.0 - hast-util-is-element: 3.0.0 - unist-util-visit-parents: 6.0.2 - - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.8 @@ -25307,12 +24826,6 @@ snapshots: husky@9.1.7: {} - i18next@25.10.4(typescript@5.9.3): - dependencies: - '@babel/runtime': 7.29.2 - optionalDependencies: - typescript: 5.9.3 - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -25362,8 +24875,6 @@ snapshots: indent-string@5.0.0: {} - inherits@2.0.3: {} - inherits@2.0.4: {} ini@1.3.8: {} @@ -25411,8 +24922,6 @@ snapshots: interpret@3.1.1: {} - intl-pluralrules@2.0.1: {} - invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -25546,10 +25055,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@5.0.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): - dependencies: - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - istanbul-lib-coverage@3.2.2: {} istanbul-lib-hook@3.0.0: @@ -25746,25 +25251,6 @@ snapshots: - supports-color - ts-node - jest-cli@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) - jest-util: 30.3.0 - jest-validate: 30.3.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest-config@29.7.0(@types/node@24.12.0): dependencies: '@babel/core': 7.29.0 @@ -26416,19 +25902,6 @@ snapshots: - supports-color - ts-node - jest@30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.4)) - '@jest/types': 30.3.0 - import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@25.5.0)(esbuild-register@3.6.0(esbuild@0.27.4)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jimp-compact@0.16.1: {} jiti@2.6.1: {} @@ -26657,10 +26130,6 @@ snapshots: dependencies: uc.micro: 2.1.0 - linkifyjs@4.3.2: {} - - load-script@1.0.0: {} - loader-runner@4.3.1: {} loader-utils@2.0.4: @@ -26687,9 +26156,6 @@ snapshots: lodash.debounce@4.0.8: {} - lodash.deburr@4.1.0: - optional: true - lodash.flattendeep@4.4.0: {} lodash.includes@4.3.0: {} @@ -26708,16 +26174,12 @@ snapshots: lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} lodash.snakecase@4.1.1: {} lodash.throttle@4.1.1: {} - lodash.uniqby@4.7.0: {} - lodash@4.17.23: {} log-symbols@2.2.0: @@ -27487,8 +26949,6 @@ snapshots: mime@3.0.0: {} - mime@4.1.0: {} - mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -28152,11 +27612,6 @@ snapshots: path-type@4.0.0: {} - path@0.12.7: - dependencies: - process: 0.11.10 - util: 0.10.4 - pathe@2.0.3: {} pathval@2.0.1: {} @@ -28428,12 +27883,6 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - property-information@7.1.0: {} protobufjs@7.5.4: @@ -28577,23 +28026,12 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-dropzone@14.4.1(react@19.2.4): - dependencies: - attr-accept: 2.2.5 - file-selector: 2.1.2 - prop-types: 15.8.1 - react: 19.2.4 - react-fast-compare@3.2.2: {} react-freeze@1.0.4(react@19.2.0): dependencies: react: 19.2.0 - react-image-gallery@1.2.12(react@19.2.4): - dependencies: - react: 19.2.4 - react-is@16.13.1: {} react-is@18.3.1: {} @@ -28618,24 +28056,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.2.14 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.1 - react: 19.2.4 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-native-appsflyer@6.17.9: {} react-native-css@3.0.6(@expo/metro-config@55.0.14(bufferutil@4.1.0)(expo@55.0.12)(typescript@5.9.3)(utf-8-validate@6.0.6))(lightningcss@1.30.1)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0): @@ -28670,16 +28090,6 @@ snapshots: react: 19.2.0 react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) - react-native-lightbox@0.7.0: - dependencies: - prop-types: 15.8.1 - - react-native-markdown-package@1.8.2: - dependencies: - lodash: 4.17.23 - react-native-lightbox: 0.7.0 - simple-markdown: 0.7.3 - react-native-marked@8.0.1(react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0): dependencies: '@jsamr/counter-style': 2.0.2 @@ -28726,11 +28136,6 @@ snapshots: react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-url-polyfill@2.0.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)): - dependencies: - react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) - whatwg-url-without-unicode: 8.0.0-3 - react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0): dependencies: '@babel/core': 7.29.0 @@ -28798,15 +28203,6 @@ snapshots: - supports-color - utf-8-validate - react-player@2.10.1(react@19.2.4): - dependencies: - deepmerge: 4.3.1 - load-script: 1.0.0 - memoize-one: 5.2.1 - prop-types: 15.8.1 - react: 19.2.4 - react-fast-compare: 3.2.2 - react-reconciler@0.33.0(react@19.2.4): dependencies: react: 19.2.4 @@ -28877,27 +28273,11 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - react: 19.2.4 - use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4) - use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - react-turnstile@1.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-virtuoso@2.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@virtuoso.dev/react-urx': 0.2.13(react@19.2.4) - '@virtuoso.dev/urx': 0.2.13 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react@19.2.0: {} react@19.2.4: {} @@ -29542,10 +28922,6 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - simple-markdown@0.7.3: - dependencies: - '@types/react': 19.2.14 - simple-plist@1.3.1: dependencies: bplist-creator: 0.1.0 @@ -29717,133 +29093,6 @@ snapshots: stream-buffers@2.2.0: {} - stream-chat-expo@8.13.7(e673e8bffb1896cc06607271df6a38dc): - dependencies: - expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(bufferutil@4.1.0)(expo-router@55.0.11)(react-dom@19.2.4(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - expo-image-manipulator: 55.0.14(expo@55.0.12) - mime: 4.1.0 - stream-chat-react-native-core: 8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(f6393708cf819e255e90d2b806f318a8) - optionalDependencies: - expo-audio: 55.0.12(expo-asset@55.0.13(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0)(typescript@5.9.3))(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - expo-clipboard: 55.0.12(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - expo-document-picker: 55.0.12(expo@55.0.12) - expo-file-system: 55.0.15(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)) - expo-haptics: 55.0.13(expo@55.0.12) - expo-image-picker: 55.0.17(expo@55.0.12) - expo-sharing: 55.0.17(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - expo-video: 55.0.14(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - transitivePeerDependencies: - - '@emoji-mart/data' - - '@op-engineering/op-sqlite' - - '@react-native-community/netinfo' - - '@shopify/flash-list' - - '@types/react' - - '@types/react-native' - - bufferutil - - debug - - emoji-mart - - react - - react-native - - react-native-gesture-handler - - react-native-reanimated - - react-native-safe-area-context - - react-native-svg - - typescript - - utf-8-validate - - stream-chat-react-native-core@8.13.7(patch_hash=6fa2fe7a3ddb0ab3312ee1adc4e07d3fc9c46f7bc5bb861675bef4078808b8c0)(f6393708cf819e255e90d2b806f318a8): - dependencies: - '@gorhom/bottom-sheet': 5.1.8(patch_hash=c5eaae9a28f5662f32d66e0129609680309eddc44720d6a2b1e02bbc2b5dd11f)(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - '@react-native-community/netinfo': 11.5.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - '@ungap/structured-clone': 1.3.0 - dayjs: 1.11.13 - emoji-regex: 10.6.0 - i18next: 25.10.4(typescript@5.9.3) - intl-pluralrules: 2.0.1 - linkifyjs: 4.3.2 - lodash-es: 4.17.23 - mime-types: 2.1.35 - path: 0.12.7 - react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6) - react-native-gesture-handler: 2.30.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - react-native-markdown-package: 1.8.2 - react-native-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - react-native-safe-area-context: 5.6.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - react-native-svg: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - react-native-url-polyfill: 2.0.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6)) - stream-chat: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - use-sync-external-store: 1.6.0(react@19.2.0) - optionalDependencies: - '@emoji-mart/data': 1.2.1 - '@shopify/flash-list': 2.0.2(@babel/runtime@7.29.2)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@6.0.6))(react@19.2.0) - emoji-mart: 5.6.0 - transitivePeerDependencies: - - '@types/react' - - '@types/react-native' - - bufferutil - - debug - - react - - typescript - - utf-8-validate - - stream-chat-react@13.14.2(@emoji-mart/data@1.2.1)(@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.4))(@types/react@19.2.14)(emoji-mart@5.6.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(stream-chat@9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3): - dependencies: - '@braintree/sanitize-url': 6.0.4 - '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - clsx: 2.1.1 - dayjs: 1.11.20 - emoji-regex: 9.2.2 - fix-webm-duration: 1.0.6 - hast-util-find-and-replace: 5.0.1 - i18next: 25.10.4(typescript@5.9.3) - linkifyjs: 4.3.2 - lodash.debounce: 4.0.8 - lodash.mergewith: 4.6.2 - lodash.throttle: 4.1.1 - lodash.uniqby: 4.7.0 - nanoid: 3.3.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-dropzone: 14.4.1(react@19.2.4) - react-fast-compare: 3.2.2 - react-image-gallery: 1.2.12(react@19.2.4) - react-markdown: 9.1.0(@types/react@19.2.14)(react@19.2.4) - react-player: 2.10.1(react@19.2.4) - react-textarea-autosize: 8.5.9(@types/react@19.2.14)(react@19.2.4) - react-virtuoso: 2.19.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - remark-gfm: 4.0.1 - stream-chat: 9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - tslib: 2.8.1 - unist-builder: 4.0.0 - unist-util-visit: 5.1.0 - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - '@emoji-mart/data': 1.2.1 - '@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.4) - '@stream-io/transliterate': 1.5.5 - emoji-mart: 5.6.0 - transitivePeerDependencies: - - '@types/react' - - supports-color - - typescript - - stream-chat@9.38.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): - dependencies: - '@types/jsonwebtoken': 9.0.10 - '@types/ws': 8.18.1 - axios: 1.15.0 - base64-js: 1.5.1 - form-data: 4.0.5 - isomorphic-ws: 5.0.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - jsonwebtoken: 9.0.3 - linkifyjs: 4.3.2 - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - transitivePeerDependencies: - - bufferutil - - debug - - utf-8-validate - stream-http@3.2.0: dependencies: builtin-status-codes: 3.0.0 @@ -30028,8 +29277,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tabbable@6.4.0: {} - tagged-tag@1.0.0: {} tailwind-merge@3.5.0: {} @@ -30372,10 +29619,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-builder@4.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -30471,29 +29714,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - use-latest-callback@0.2.6(react@19.2.0): dependencies: react: 19.2.0 - use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.0): dependencies: detect-node-es: 1.1.0 @@ -30524,10 +29748,6 @@ snapshots: util-deprecate@1.0.2: {} - util@0.10.4: - dependencies: - inherits: 2.0.3 - util@0.12.5: dependencies: inherits: 2.0.4 @@ -30896,8 +30116,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@5.0.0: {} - webidl-conversions@7.0.0: {} webpack-bundle-analyzer@4.10.1(bufferutil@4.1.0)(utf-8-validate@6.0.6): @@ -31049,12 +30267,6 @@ snapshots: whatwg-url-minimum@0.1.1: {} - whatwg-url-without-unicode@8.0.0-3: - dependencies: - buffer: 5.7.1 - punycode: 2.3.1 - webidl-conversions: 5.0.0 - whatwg-url@14.2.0: dependencies: tr46: 5.1.1 diff --git a/services/kiloclaw/controller/src/config-writer.test.ts b/services/kiloclaw/controller/src/config-writer.test.ts index fe6437ee99..3cf8836666 100644 --- a/services/kiloclaw/controller/src/config-writer.test.ts +++ b/services/kiloclaw/controller/src/config-writer.test.ts @@ -786,44 +786,6 @@ describe('generateBaseConfig', () => { expect(config.channels.slack).toBeUndefined(); }); - // ─── Stream Chat (default channel) ─────────────────────────────────────── - - it('configures Stream Chat channel and plugin when all three vars are set', () => { - const { deps } = fakeDeps(); - const env = { - ...minimalEnv(), - STREAM_CHAT_API_KEY: 'sc-api-key', - STREAM_CHAT_BOT_USER_ID: 'bot-sandbox-abc', - STREAM_CHAT_BOT_USER_TOKEN: 'sc-bot-token', - }; - const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); - - expect(config.channels.streamchat.apiKey).toBe('sc-api-key'); - expect(config.channels.streamchat.botUserId).toBe('bot-sandbox-abc'); - expect(config.channels.streamchat.botUserToken).toBe('sc-bot-token'); - expect(config.channels.streamchat.botUserName).toBe('KiloClaw'); - expect(config.channels.streamchat.enabled).toBe(true); - expect(config.plugins.entries['openclaw-channel-streamchat'].enabled).toBe(true); - expect(config.plugins.load.paths).toContain( - '/usr/local/lib/node_modules/@wunderchat/openclaw-channel-streamchat' - ); - }); - - it('does not configure Stream Chat when any of the three required vars is missing', () => { - const cases = [ - { STREAM_CHAT_API_KEY: 'key', STREAM_CHAT_BOT_USER_ID: 'bot' }, - { STREAM_CHAT_API_KEY: 'key', STREAM_CHAT_BOT_USER_TOKEN: 'token' }, - { STREAM_CHAT_BOT_USER_ID: 'bot', STREAM_CHAT_BOT_USER_TOKEN: 'token' }, - ]; - - for (const partial of cases) { - const { deps } = fakeDeps(); - const env = { ...minimalEnv(), ...partial }; - const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); - expect(config.channels.streamchat).toBeUndefined(); - } - }); - // ─── Kilo Chat ─────────────────────────────────────────────────────────── it('always configures kilo-chat channel and plugin', () => { @@ -861,30 +823,6 @@ describe('generateBaseConfig', () => { expect(config.session.dmScope).toBe('per-peer'); }); - it('does not duplicate the plugin path on repeated generateBaseConfig calls', () => { - const existing = JSON.stringify({ - channels: { streamchat: { apiKey: 'old-key', enabled: true } }, - plugins: { - load: { - paths: ['/usr/local/lib/node_modules/@wunderchat/openclaw-channel-streamchat'], - }, - entries: { 'openclaw-channel-streamchat': { enabled: true } }, - }, - }); - const { deps } = fakeDeps(existing); - const env = { - ...minimalEnv(), - STREAM_CHAT_API_KEY: 'sc-api-key', - STREAM_CHAT_BOT_USER_ID: 'bot-sandbox-abc', - STREAM_CHAT_BOT_USER_TOKEN: 'sc-bot-token', - }; - const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); - - const pluginPath = '/usr/local/lib/node_modules/@wunderchat/openclaw-channel-streamchat'; - const paths = config.plugins.load.paths as string[]; - expect(paths.filter(p => p === pluginPath)).toHaveLength(1); - }); - it('does not set gateway auth when OPENCLAW_GATEWAY_TOKEN is missing', () => { const { deps } = fakeDeps(); const env = { ...minimalEnv() }; diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index 0d392e1b6e..6c165fded8 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -476,34 +476,6 @@ export function generateBaseConfig( config.plugins.entries.slack.enabled = true; } - // Stream Chat default channel (auto-provisioned at provision time) - if (env.STREAM_CHAT_API_KEY && env.STREAM_CHAT_BOT_USER_ID && env.STREAM_CHAT_BOT_USER_TOKEN) { - config.channels.streamchat = config.channels.streamchat ?? {}; - config.channels.streamchat.apiKey = env.STREAM_CHAT_API_KEY; - config.channels.streamchat.botUserId = env.STREAM_CHAT_BOT_USER_ID; - config.channels.streamchat.botUserToken = env.STREAM_CHAT_BOT_USER_TOKEN; - config.channels.streamchat.botUserName = 'KiloClaw'; - config.channels.streamchat.enabled = true; - - config.plugins = config.plugins ?? {}; - config.plugins.load = config.plugins.load ?? {}; - config.plugins.load.paths = Array.isArray(config.plugins.load.paths) - ? config.plugins.load.paths - : []; - const pluginPath = '/usr/local/lib/node_modules/@wunderchat/openclaw-channel-streamchat'; - if (!(config.plugins.load.paths as string[]).includes(pluginPath)) { - (config.plugins.load.paths as string[]).push(pluginPath); - } - - config.plugins.entries = config.plugins.entries ?? {}; - // Entry key must match the plugin's manifest id (openclaw.plugin.json). - // The fork's manifest declares id "openclaw-channel-streamchat" to align - // with the idHint that OpenClaw derives from the package name. - const scEntry = 'openclaw-channel-streamchat'; - config.plugins.entries[scEntry] = config.plugins.entries[scEntry] ?? {}; - config.plugins.entries[scEntry].enabled = true; - } - // Session — default DM scope to per-channel-peer so each channel+peer // combination gets its own session. OpenClaw's onboard sets this for new // instances, but legacy instances may not have it. diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts index a8de59e3aa..75e5bd71f4 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance.test.ts @@ -128,18 +128,6 @@ vi.mock('../utils/encryption', async () => { }; }); -// -- Mock stream-chat client -- -vi.mock('../stream-chat/client', () => ({ - setupDefaultStreamChatChannel: vi.fn().mockResolvedValue({ - apiKey: 'sc-api-key', - botUserId: 'bot-sandbox-1', - botUserToken: 'sc-bot-token', - channelId: 'default-sandbox-1', - }), - createShortLivedUserToken: vi.fn().mockResolvedValue('short-lived-token'), - deactivateStreamChatUsers: vi.fn().mockResolvedValue(undefined), -})); - import { KiloClawInstance } from './kiloclaw-instance'; import { buildChannelConfigPatch } from './kiloclaw-instance/channel-config'; import * as flyClient from '../fly/client'; @@ -149,7 +137,6 @@ import * as gatewayEnv from '../gateway/env'; import * as regions from './regions'; import { resolveLatestVersion } from '../lib/image-version'; import { selectImageVersionForInstance } from '../lib/version-rollout'; -import { setupDefaultStreamChatChannel } from '../stream-chat/client'; import { verifyKiloToken } from '@kilocode/worker-utils'; import { ALARM_INTERVAL_RUNNING_MS, @@ -9196,215 +9183,3 @@ describe('tryMarkInstanceReady', () => { expect(storage._store.get('instanceReadyEmailSent')).toBe(true); }); }); - -// ============================================================================ -// Stream Chat backfill -// ============================================================================ - -describe('Stream Chat backfill on provision', () => { - beforeEach(() => { - (flyClient.createVolumeWithFallback as Mock).mockResolvedValue({ - id: 'vol-1', - region: 'iad', - }); - (flyClient.getVolume as Mock).mockResolvedValue({ id: 'vol-1', region: 'iad' }); - (flyClient.createMachine as Mock).mockResolvedValue({ id: 'machine-1', region: 'iad' }); - (flyClient.waitForState as Mock).mockResolvedValue(undefined); - (setupDefaultStreamChatChannel as Mock).mockClear(); - }); - - it('provisions Stream Chat on first provision when env vars are present', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage, waitUntilPromises } = createInstance(undefined, env); - - await instance.provision('user-1', {}); - await Promise.all(waitUntilPromises); - - expect(setupDefaultStreamChatChannel).toHaveBeenCalledOnce(); - expect(storage._store.get('streamChatApiKey')).toBe('sc-api-key'); - expect(storage._store.get('streamChatBotUserId')).toBe('bot-sandbox-1'); - expect(storage._store.get('streamChatBotUserToken')).toBe('sc-bot-token'); - expect(storage._store.get('streamChatChannelId')).toBe('default-sandbox-1'); - }); - - it('backfills Stream Chat on re-provision when DO state has no credentials', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage } = createInstance(undefined, env); - await seedRunning(storage); - - (setupDefaultStreamChatChannel as Mock).mockClear(); - await instance.provision('user-1', { kilocodeApiKey: 'new-key' }); - - expect(setupDefaultStreamChatChannel).toHaveBeenCalledOnce(); - expect(storage._store.get('streamChatApiKey')).toBe('sc-api-key'); - expect(storage._store.get('streamChatBotUserId')).toBe('bot-sandbox-1'); - expect(storage._store.get('streamChatBotUserToken')).toBe('sc-bot-token'); - expect(storage._store.get('streamChatChannelId')).toBe('default-sandbox-1'); - }); - - it('skips Stream Chat setup on re-provision when credentials already exist', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage } = createInstance(undefined, env); - await seedRunning(storage, { - streamChatApiKey: 'existing-key', - streamChatBotUserId: 'existing-bot', - streamChatBotUserToken: 'existing-token', - streamChatChannelId: 'existing-channel', - }); - - (setupDefaultStreamChatChannel as Mock).mockClear(); - await instance.provision('user-1', { kilocodeApiKey: 'new-key' }); - - expect(setupDefaultStreamChatChannel).not.toHaveBeenCalled(); - expect(storage._store.get('streamChatApiKey')).toBe('existing-key'); - }); - - it('skips Stream Chat when worker env vars are missing', async () => { - const { instance, storage, waitUntilPromises } = createInstance(); - // Default env does not have STREAM_CHAT_API_KEY / STREAM_CHAT_API_SECRET - - await instance.provision('user-1', {}); - await Promise.all(waitUntilPromises); - - expect(setupDefaultStreamChatChannel).not.toHaveBeenCalled(); - expect(storage._store.get('streamChatApiKey')).toBeUndefined(); - }); - - it('continues provisioning when Stream Chat setup fails (non-fatal)', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage, waitUntilPromises } = createInstance(undefined, env); - - (setupDefaultStreamChatChannel as Mock).mockRejectedValueOnce( - new Error('Stream Chat API down') - ); - - await instance.provision('user-1', {}); - await Promise.all(waitUntilPromises); - - // Provision succeeded despite Stream Chat failure - expect(storage._store.get('status')).toBeTruthy(); - expect(storage._store.get('streamChatApiKey')).toBeUndefined(); - }); -}); - -describe('Stream Chat backfill on restartMachine', () => { - beforeEach(() => { - (flyClient.updateMachine as Mock).mockResolvedValue({ instance_id: 'inst-1' }); - (flyClient.waitForState as Mock).mockResolvedValue(undefined); - (flyClient.getMachine as Mock).mockResolvedValue({ - id: 'machine-1', - state: 'started', - config: { guest: { cpus: 1, memory_mb: 256, cpu_kind: 'shared' } }, - }); - (setupDefaultStreamChatChannel as Mock).mockClear(); - vi.stubGlobal( - 'fetch', - vi.fn().mockImplementation((url: string) => { - if (typeof url === 'string' && url.includes('/_kilo/gateway/status')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ state: 'running' }), - }); - } - return Promise.resolve({ ok: true, status: 200 }); - }) - ); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it('backfills Stream Chat on restart when DO state has no credentials', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage, waitUntilPromises } = createInstance(undefined, env); - await seedRunning(storage); - - const result = await instance.restartMachine(); - expect(result.success).toBe(true); - await Promise.all(waitUntilPromises); - - expect(setupDefaultStreamChatChannel).toHaveBeenCalledOnce(); - expect(storage._store.get('streamChatApiKey')).toBe('sc-api-key'); - expect(storage._store.get('streamChatBotUserId')).toBe('bot-sandbox-1'); - expect(storage._store.get('streamChatBotUserToken')).toBe('sc-bot-token'); - expect(storage._store.get('streamChatChannelId')).toBe('default-sandbox-1'); - }); - - it('skips Stream Chat backfill on restart when credentials already exist', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage, waitUntilPromises } = createInstance(undefined, env); - await seedRunning(storage, { - streamChatApiKey: 'existing-key', - streamChatBotUserId: 'existing-bot', - streamChatBotUserToken: 'existing-token', - streamChatChannelId: 'existing-channel', - }); - - const result = await instance.restartMachine(); - expect(result.success).toBe(true); - await Promise.all(waitUntilPromises); - - expect(setupDefaultStreamChatChannel).not.toHaveBeenCalled(); - expect(storage._store.get('streamChatApiKey')).toBe('existing-key'); - }); - - it('continues restart when Stream Chat backfill fails (non-fatal)', async () => { - const env = createFakeEnv(); - Object.assign(env, { - STREAM_CHAT_API_KEY: 'sc-key', - STREAM_CHAT_API_SECRET: 'sc-secret', - }); - const { instance, storage, waitUntilPromises } = createInstance(undefined, env); - await seedRunning(storage); - - (setupDefaultStreamChatChannel as Mock).mockRejectedValueOnce( - new Error('Stream Chat API down') - ); - - const result = await instance.restartMachine(); - expect(result.success).toBe(true); - await Promise.all(waitUntilPromises); - - // Restart still completes — Stream Chat failure is non-fatal - expect(storage._store.get('streamChatApiKey')).toBeUndefined(); - // Machine was still updated - expect(flyClient.updateMachine).toHaveBeenCalled(); - }); - - it('skips Stream Chat backfill when worker env vars are missing', async () => { - const { instance, storage, waitUntilPromises } = createInstance(); - await seedRunning(storage); - - const result = await instance.restartMachine(); - expect(result.success).toBe(true); - await Promise.all(waitUntilPromises); - - expect(setupDefaultStreamChatChannel).not.toHaveBeenCalled(); - }); -}); diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts index 1621529121..3eaa488c72 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts @@ -215,17 +215,6 @@ export async function buildUserEnvVars( plainEnv.KILOCLAW_GMAIL_LAST_HISTORY_ID = state.gmailLastHistoryId; } - // Stream Chat default channel (auto-provisioned at first provision). - // API key and bot user ID are plaintext; bot user token is sensitive. - if (state.streamChatApiKey && state.streamChatBotUserId && state.streamChatBotUserToken) { - plainEnv.STREAM_CHAT_API_KEY = state.streamChatApiKey; - plainEnv.STREAM_CHAT_BOT_USER_ID = state.streamChatBotUserId; - sensitive.STREAM_CHAT_BOT_USER_TOKEN = state.streamChatBotUserToken; - if (state.streamChatChannelId) { - plainEnv.STREAM_CHAT_DEFAULT_CHANNEL_ID = state.streamChatChannelId; - } - } - // Get the env encryption key from the App DO, creating it if needed. // Instance-keyed DOs get per-instance apps, legacy DOs get per-user apps. // Pass the Instance DO's known flyAppName so the App DO can adopt it if needed diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts index 8570fcf1b1..ed69915d98 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance/index.ts @@ -105,11 +105,6 @@ import { runUnexpectedStopRecoveryInBackground, type RecoveryRuntime, } from './recovery'; -import { - setupDefaultStreamChatChannel, - createShortLivedUserToken, - deactivateStreamChatUsers, -} from '../../stream-chat/client'; import { writeEvent, safeInstanceIdFromSandboxId } from '../../utils/analytics'; import type { KiloClawEventData, KiloClawEventName } from '../../utils/analytics'; import { getProviderAdapter, resolveDefaultProvider } from '../../providers'; @@ -804,47 +799,6 @@ export class KiloClawInstance extends DurableObject { }); } - // Set up the default Stream Chat channel on first provision (best-effort). - // The bot and channel are created server-side here so the API secret never - // reaches the Fly Machine. Failure is non-fatal: the instance will start - // without the Stream Chat channel rather than blocking provisioning. - // Set up or backfill the default Stream Chat channel (best-effort). - // On first provision (isNew) this creates the channel from scratch. - // On re-provision (!isNew) this backfills instances created before the - // feature was added. setupDefaultStreamChatChannel is idempotent - // (upsert users, getOrCreate channel). Failure is non-fatal. - if ( - !this.s.streamChatApiKey && - this.env.STREAM_CHAT_API_KEY && - this.env.STREAM_CHAT_API_SECRET - ) { - try { - const streamChat = await setupDefaultStreamChatChannel( - this.env.STREAM_CHAT_API_KEY, - this.env.STREAM_CHAT_API_SECRET, - sandboxId - ); - this.s.streamChatApiKey = streamChat.apiKey; - this.s.streamChatBotUserId = streamChat.botUserId; - this.s.streamChatBotUserToken = streamChat.botUserToken; - this.s.streamChatChannelId = streamChat.channelId; - await this.persist({ - streamChatApiKey: streamChat.apiKey, - streamChatBotUserId: streamChat.botUserId, - streamChatBotUserToken: streamChat.botUserToken, - streamChatChannelId: streamChat.channelId, - }); - console.log( - `[DO] Stream Chat channel ${isNew ? 'provisioned' : 'backfilled'}:`, - streamChat.channelId - ); - } catch (err) { - doWarn(this.s, 'Stream Chat channel setup failed (non-fatal)', { - error: toLoggable(err), - }); - } - } - if (isNew) { await this.scheduleAlarm(); } @@ -2268,22 +2222,6 @@ export class KiloClawInstance extends DurableObject { value: machineUptimeMs, }); - // Best-effort: deactivate Stream Chat users so any captured tokens become useless. - // Failure is non-fatal — worst case is the same as pre-deactivation behavior. - if (this.env.STREAM_CHAT_API_KEY && this.env.STREAM_CHAT_API_SECRET && this.s.sandboxId) { - try { - await deactivateStreamChatUsers( - this.env.STREAM_CHAT_API_KEY, - this.env.STREAM_CHAT_API_SECRET, - [this.s.sandboxId, `bot-${this.s.sandboxId}`] - ); - } catch (err) { - doWarn(this.s, 'Stream Chat user deactivation failed (non-fatal)', { - error: toLoggable(err), - }); - } - } - // Best-effort: clean up kilo-chat data (conversations, messages, memberships) // for this sandbox. Failure is non-fatal — orphaned data is unreachable. if (this.env.KILO_CHAT && this.s.sandboxId) { @@ -2490,38 +2428,6 @@ export class KiloClawInstance extends DurableObject { }; } - async getStreamChatCredentials(): Promise<{ - apiKey: string; - userId: string; - userToken: string; - channelId: string; - } | null> { - await this.loadState(); - - if ( - !this.s.streamChatApiKey || - !this.env.STREAM_CHAT_API_SECRET || - !this.s.streamChatChannelId || - !this.s.sandboxId - ) { - return null; - } - - // Mint a short-lived token on every request so that revoked users lose - // access when the token expires, without requiring an app-secret rotation. - const userToken = await createShortLivedUserToken( - this.env.STREAM_CHAT_API_SECRET, - this.s.sandboxId - ); - - return { - apiKey: this.s.streamChatApiKey, - userId: this.s.sandboxId, - userToken, - channelId: this.s.streamChatChannelId, - }; - } - async getDebugState(): Promise<{ userId: string | null; sandboxId: string | null; @@ -3332,40 +3238,6 @@ export class KiloClawInstance extends DurableObject { throw new Error('No machine exists'); } - // Backfill Stream Chat for instances created before the feature was added. - // setupDefaultStreamChatChannel is idempotent (upsert users, getOrCreate channel). - if ( - !this.s.streamChatApiKey && - this.env.STREAM_CHAT_API_KEY && - this.env.STREAM_CHAT_API_SECRET && - this.s.sandboxId - ) { - try { - const streamChat = await setupDefaultStreamChatChannel( - this.env.STREAM_CHAT_API_KEY, - this.env.STREAM_CHAT_API_SECRET, - this.s.sandboxId - ); - this.s.streamChatApiKey = streamChat.apiKey; - this.s.streamChatBotUserId = streamChat.botUserId; - this.s.streamChatBotUserToken = streamChat.botUserToken; - this.s.streamChatChannelId = streamChat.channelId; - await this.persist({ - streamChatApiKey: streamChat.apiKey, - streamChatBotUserId: streamChat.botUserId, - streamChatBotUserToken: streamChat.botUserToken, - streamChatChannelId: streamChat.channelId, - }); - doLog(this.s, 'Stream Chat backfilled on restart', { - channelId: streamChat.channelId, - }); - } catch (err) { - doWarn(this.s, 'Stream Chat backfill failed on restart (non-fatal)', { - error: toLoggable(err), - }); - } - } - const { envVars, bootstrapEnv, minSecretsVersion } = await buildUserEnvVars( this.env, this.ctx, diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance/state.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance/state.ts index 3c195fc511..7c386f1219 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance/state.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance/state.ts @@ -333,10 +333,6 @@ export async function loadState(ctx: DurableObjectState, s: InstanceMutableState // to avoid spurious emails after deploy. s.instanceReadyEmailSent = 'instanceReadyEmailSent' in raw ? d.instanceReadyEmailSent : true; s.customSecretMeta = d.customSecretMeta; - s.streamChatApiKey = d.streamChatApiKey; - s.streamChatBotUserId = d.streamChatBotUserId; - s.streamChatBotUserToken = d.streamChatBotUserToken; - s.streamChatChannelId = d.streamChatChannelId; s.vectorMemoryEnabled = d.vectorMemoryEnabled; s.vectorMemoryModel = d.vectorMemoryModel; s.dreamingEnabled = d.dreamingEnabled; @@ -432,10 +428,6 @@ export function resetMutableState(s: InstanceMutableState): void { s.preRestoreStatus = null; s.pendingRestoreVolumeId = null; s.instanceReadyEmailSent = false; - s.streamChatApiKey = null; - s.streamChatBotUserId = null; - s.streamChatBotUserToken = null; - s.streamChatChannelId = null; s.vectorMemoryEnabled = false; s.vectorMemoryModel = null; s.dreamingEnabled = false; @@ -526,10 +518,6 @@ export function createMutableState(): InstanceMutableState { pendingRestoreVolumeId: null, instanceReadyEmailSent: false, customSecretMeta: null, - streamChatApiKey: null, - streamChatBotUserId: null, - streamChatBotUserToken: null, - streamChatChannelId: null, vectorMemoryEnabled: false, vectorMemoryModel: null, dreamingEnabled: false, diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance/types.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance/types.ts index d66b412948..1053fbf8df 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance/types.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance/types.ts @@ -132,11 +132,6 @@ export type InstanceMutableState = { pendingRestoreVolumeId: string | null; instanceReadyEmailSent: boolean; customSecretMeta: PersistedState['customSecretMeta']; - // Stream Chat default channel (auto-provisioned) - streamChatApiKey: string | null; - streamChatBotUserId: string | null; - streamChatBotUserToken: string | null; - streamChatChannelId: string | null; vectorMemoryEnabled: boolean; vectorMemoryModel: string | null; dreamingEnabled: boolean; diff --git a/services/kiloclaw/src/gateway/env.ts b/services/kiloclaw/src/gateway/env.ts index c31fe64232..eb02f4ef0e 100644 --- a/services/kiloclaw/src/gateway/env.ts +++ b/services/kiloclaw/src/gateway/env.ts @@ -82,8 +82,6 @@ export type EnvVarsBuild = { const SENSITIVE_KEYS = new Set([ 'KILOCODE_API_KEY', 'OPENCLAW_GATEWAY_TOKEN', - // Stream Chat bot token is auto-provisioned and must stay encrypted in transit - 'STREAM_CHAT_BOT_USER_TOKEN', ...ALL_SECRET_ENV_VARS, ...INTERNAL_SENSITIVE_ENV_VARS, ]); diff --git a/services/kiloclaw/src/routes/kiloclaw.ts b/services/kiloclaw/src/routes/kiloclaw.ts index 720a0935bd..3eb08a44be 100644 --- a/services/kiloclaw/src/routes/kiloclaw.ts +++ b/services/kiloclaw/src/routes/kiloclaw.ts @@ -91,32 +91,6 @@ kiloclaw.get('/status', c => }) ); -// GET /api/kiloclaw/chat-credentials -- Stream Chat credentials for the user's channel -kiloclaw.get('/chat-credentials', c => - instrumented(c, 'GET /api/kiloclaw/chat-credentials', async () => { - const userId = c.get('userId'); - const raw = c.req.query('instanceId'); - if (raw && !InstanceIdParam.safeParse(raw).success) { - return c.json({ error: 'Invalid instance ID' }, 400); - } - const instanceId = raw || undefined; - const doKey = instanceId ?? userId; - const stub = c.env.KILOCLAW_INSTANCE.get(c.env.KILOCLAW_INSTANCE.idFromName(doKey)); - - // When accessing by instanceId, verify the authenticated user owns this instance. - if (instanceId) { - const status = await stub.getStatus(); - if (status.userId !== userId) { - return c.json({ error: 'Access denied' }, 403); - } - } - - const creds = await stub.getStreamChatCredentials(); - - return c.json(creds); - }) -); - /** * Derive per-entry configured status from the catalog. * diff --git a/services/kiloclaw/src/routes/platform.ts b/services/kiloclaw/src/routes/platform.ts index c0c4323ef2..3c095c0bf3 100644 --- a/services/kiloclaw/src/routes/platform.ts +++ b/services/kiloclaw/src/routes/platform.ts @@ -49,7 +49,6 @@ import { deriveGatewayToken } from '../auth/gateway-token'; import { sandboxIdFromUserId } from '../auth/sandbox-id'; import { writeEvent } from '../utils/analytics'; import { deriveHttpEventName } from '../middleware/analytics'; -import { sendMessage } from '../stream-chat/client'; import { assertAvailableProvider } from '../providers'; import type { ProviderCapability } from '../providers/types'; import { @@ -2747,31 +2746,6 @@ platform.get('/status', async c => { } }); -// GET /api/platform/stream-chat-credentials?userId=...&instanceId=... -platform.get('/stream-chat-credentials', async c => { - const userId = setValidatedQueryUserId(c); - if (!userId) { - return c.json({ error: 'userId query parameter is required' }, 400); - } - const iidResult = parseInstanceIdQuery(c); - if ('error' in iidResult) return iidResult.error; - const { instanceId } = iidResult; - - try { - const creds = await withResolvedDORetry( - c.env, - userId, - instanceId, - stub => stub.getStreamChatCredentials(), - 'getStreamChatCredentials' - ); - return c.json(creds); - } catch (err) { - const { message, status } = sanitizeError(err, 'stream-chat-credentials'); - return jsonError(message, status); - } -}); - const MAX_INBOUND_EMAIL_TITLE_SLUG_LENGTH = 80; const InboundEmailSchema = z.object({ @@ -3051,74 +3025,6 @@ platform.post('/inbound-email', async c => { } }); -// POST /api/platform/send-chat-message -// Send a message to a KiloClaw instance's Stream Chat channel as the human user. -// The OpenClaw bot picks it up and responds as if the user typed it. -const SendChatMessageSchema = z.object({ - userId: z.string().min(1), - instanceId: z.string().uuid().optional(), - message: z.string().min(1).max(32_000), -}); - -platform.post('/send-chat-message', async c => { - const body: unknown = await c.req.json().catch(() => null); - const parsed = SendChatMessageSchema.safeParse(body); - if (!parsed.success) { - return jsonError('Invalid request body: userId and message are required', 400); - } - - const { userId, instanceId, message } = parsed.data; - c.set('userId', userId); - - const apiKey = c.env.STREAM_CHAT_API_KEY; - const apiSecret = c.env.STREAM_CHAT_API_SECRET; - if (!apiKey || !apiSecret) { - return jsonError('Stream Chat is not configured', 503); - } - - try { - // Use instanceId as the DO key when available (matches how other endpoints resolve DOs). - // Falls back to userId for backward compatibility with triggers that predate instanceId. - const creds = await withResolvedDORetry( - c.env, - userId, - instanceId, - stub => stub.getStreamChatCredentials(), - 'getStreamChatCredentials' - ); - - if (!creds) { - return jsonError('Stream Chat is not set up for this instance', 404); - } - - await sendMessage(apiKey, apiSecret, creds.channelId, creds.userId, message); - - writeEvent(c.env, { - event: 'instance.webhook_chat_message_sent', - delivery: 'http', - route: '/api/platform/send-chat-message', - userId, - instanceId: instanceId ?? undefined, - channelId: creds.channelId, - }); - - return c.json({ success: true, channelId: creds.channelId }); - } catch (err) { - const { message: errMsg, status } = sanitizeError(err, 'send-chat-message'); - - writeEvent(c.env, { - event: 'instance.webhook_chat_message_failed', - delivery: 'http', - route: '/api/platform/send-chat-message', - userId, - instanceId: instanceId ?? undefined, - error: errMsg, - }); - - return jsonError(errMsg, status); - } -}); - // GET /api/platform/debug-status?userId=...&instanceId=... // Internal/admin-only debug status that includes DO destroy internals. platform.get('/debug-status', async c => { diff --git a/services/kiloclaw/src/schemas/instance-config.ts b/services/kiloclaw/src/schemas/instance-config.ts index e0d478e0f1..21bacb38bf 100644 --- a/services/kiloclaw/src/schemas/instance-config.ts +++ b/services/kiloclaw/src/schemas/instance-config.ts @@ -413,12 +413,6 @@ export const PersistedStateSchema = z.object({ // Metadata for custom (non-catalog) secrets: env var name → { configPath? }. // configPath is a JSON dot-notation path for patching into openclaw.json at boot. customSecretMeta: z.record(z.string(), CustomSecretMetaSchema).nullable().default(null), - // Stream Chat default channel (auto-provisioned on first instance creation). - // Null on existing instances (pre-Stream Chat) and when STREAM_CHAT_API_KEY is not set. - streamChatApiKey: z.string().nullable().default(null), - streamChatBotUserId: z.string().nullable().default(null), - streamChatBotUserToken: z.string().nullable().default(null), - streamChatChannelId: z.string().nullable().default(null), // Vector memory: whether the builtin embedding-backed memory search is enabled. vectorMemoryEnabled: z.boolean().default(false), // Vector memory: embedding model ID (e.g. "mistralai/mistral-embed-2312"). diff --git a/services/kiloclaw/src/stream-chat/client.test.ts b/services/kiloclaw/src/stream-chat/client.test.ts deleted file mode 100644 index ce0f495bcb..0000000000 --- a/services/kiloclaw/src/stream-chat/client.test.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - createServerToken, - createUserToken, - createShortLivedUserToken, - upsertStreamChatUsers, - getOrCreateStreamChatChannel, - deactivateStreamChatUsers, - reactivateStreamChatUsers, - setupDefaultStreamChatChannel, - sendMessage, -} from './client'; - -// Decode a JWT payload without verifying signature (for test assertions only). -function decodeJwtPayload(token: string): Record { - const [, payload] = token.split('.'); - return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record; -} - -describe('createServerToken', () => { - it('produces a JWT with server: true in the payload', async () => { - const token = await createServerToken('test-secret'); - expect(token.split('.')).toHaveLength(3); - const payload = decodeJwtPayload(token); - expect(payload.server).toBe(true); - }); - - it('produces different tokens for different secrets', async () => { - const t1 = await createServerToken('secret-a'); - const t2 = await createServerToken('secret-b'); - expect(t1).not.toBe(t2); - }); -}); - -describe('createUserToken', () => { - it('produces a JWT with user_id in the payload', async () => { - const token = await createUserToken('test-secret', 'user-123'); - const payload = decodeJwtPayload(token); - expect(payload.user_id).toBe('user-123'); - }); - - it('produces different tokens for different user IDs', async () => { - const t1 = await createUserToken('secret', 'user-a'); - const t2 = await createUserToken('secret', 'user-b'); - expect(t1).not.toBe(t2); - }); -}); - -describe('createShortLivedUserToken', () => { - it('produces a JWT with user_id, iat, and exp in the payload', async () => { - const token = await createShortLivedUserToken('test-secret', 'user-456'); - const payload = decodeJwtPayload(token); - expect(payload.user_id).toBe('user-456'); - expect(payload.iat).toEqual(expect.any(Number)); - expect(payload.exp).toEqual(expect.any(Number)); - }); - - it('sets exp roughly 6 hours after iat', async () => { - const token = await createShortLivedUserToken('test-secret', 'user-ttl'); - const payload = decodeJwtPayload(token); - const iat = payload.iat as number; - const exp = payload.exp as number; - const sixHoursInSeconds = 6 * 60 * 60; - // Allow 5 seconds of tolerance for test execution time - expect(exp - iat).toBeGreaterThanOrEqual(sixHoursInSeconds - 5); - expect(exp - iat).toBeLessThanOrEqual(sixHoursInSeconds + 5); - }); - - it('produces different tokens for different user IDs', async () => { - const t1 = await createShortLivedUserToken('secret', 'user-a'); - const t2 = await createShortLivedUserToken('secret', 'user-b'); - expect(t1).not.toBe(t2); - }); -}); - -describe('upsertStreamChatUsers', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - it('sends a POST to /users with correct headers and body', async () => { - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }); - - await upsertStreamChatUsers('my-api-key', 'server-jwt', [ - { id: 'user-1', name: 'User One' }, - { id: 'bot-1', name: 'Bot One', role: 'admin' }, - ]); - - expect(mockFetch).toHaveBeenCalledOnce(); - const [url, opts] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - expect(url).toBe('https://chat.stream-io-api.com/users?api_key=my-api-key'); - expect(opts.method).toBe('POST'); - expect(opts.headers['Stream-Auth-Type']).toBe('jwt'); - expect(opts.headers['Authorization']).toBe('server-jwt'); - const body = JSON.parse(opts.body as string) as { users: Record }; - expect(body.users['user-1']).toMatchObject({ id: 'user-1', name: 'User One' }); - expect(body.users['bot-1']).toMatchObject({ id: 'bot-1', name: 'Bot One', role: 'admin' }); - }); - - it('throws on HTTP error with status and body in the message', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 403, - text: async () => 'Unauthorized', - }); - - await expect(upsertStreamChatUsers('key', 'jwt', [{ id: 'x', name: 'X' }])).rejects.toThrow( - '403' - ); - }); -}); - -describe('getOrCreateStreamChatChannel', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - it('POSTs to /channels/{type}/{id}/query with correct payload', async () => { - mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }); - - await getOrCreateStreamChatChannel('my-key', 'server-jwt', 'messaging', 'chan-123', { - created_by_id: 'user-1', - members: ['user-1', 'bot-1'], - name: 'Test Channel', - }); - - expect(mockFetch).toHaveBeenCalledOnce(); - const [url, opts] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - expect(url).toBe( - 'https://chat.stream-io-api.com/channels/messaging/chan-123/query?api_key=my-key' - ); - expect(opts.method).toBe('POST'); - const body = JSON.parse(opts.body as string) as { data: unknown }; - expect(body.data).toMatchObject({ - created_by_id: 'user-1', - members: ['user-1', 'bot-1'], - name: 'Test Channel', - }); - }); - - it('throws on HTTP error', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 429, - text: async () => 'Rate limited', - }); - - await expect( - getOrCreateStreamChatChannel('key', 'jwt', 'messaging', 'chan-1', { - created_by_id: 'u', - members: ['u', 'b'], - }) - ).rejects.toThrow('429'); - }); -}); - -describe('deactivateStreamChatUsers', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - it('POSTs to /api/v2/users/{id}/deactivate for each user', async () => { - mockFetch.mockResolvedValue({ ok: true, status: 200 }); - - await deactivateStreamChatUsers('my-key', 'my-secret', ['user-1', 'bot-user-1']); - - expect(mockFetch).toHaveBeenCalledTimes(2); - - const [url1, opts1] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - expect(url1).toBe( - 'https://chat.stream-io-api.com/api/v2/users/user-1/deactivate?api_key=my-key' - ); - expect(opts1.method).toBe('POST'); - expect(opts1.headers['Stream-Auth-Type']).toBe('jwt'); - - const [url2] = mockFetch.mock.calls[1] as [string, unknown]; - expect(url2).toBe( - 'https://chat.stream-io-api.com/api/v2/users/bot-user-1/deactivate?api_key=my-key' - ); - }); - - it('silently ignores 404 responses', async () => { - mockFetch.mockResolvedValue({ ok: false, status: 404, text: async () => 'Not Found' }); - - await expect( - deactivateStreamChatUsers('key', 'secret', ['nonexistent']) - ).resolves.toBeUndefined(); - }); - - it('throws on non-404 HTTP errors', async () => { - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - text: async () => 'Internal Server Error', - }); - - await expect(deactivateStreamChatUsers('key', 'secret', ['user-1'])).rejects.toThrow('500'); - }); -}); - -describe('reactivateStreamChatUsers', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - it('POSTs to /api/v2/users/{id}/reactivate for each user', async () => { - mockFetch.mockResolvedValue({ ok: true, status: 200 }); - - await reactivateStreamChatUsers('my-key', 'my-secret', ['user-1', 'bot-user-1']); - - expect(mockFetch).toHaveBeenCalledTimes(2); - - const [url1, opts1] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - expect(url1).toBe( - 'https://chat.stream-io-api.com/api/v2/users/user-1/reactivate?api_key=my-key' - ); - expect(opts1.method).toBe('POST'); - expect(opts1.headers['Stream-Auth-Type']).toBe('jwt'); - - const [url2] = mockFetch.mock.calls[1] as [string, unknown]; - expect(url2).toBe( - 'https://chat.stream-io-api.com/api/v2/users/bot-user-1/reactivate?api_key=my-key' - ); - }); - - it('silently ignores 404 responses', async () => { - mockFetch.mockResolvedValue({ ok: false, status: 404, text: async () => 'Not Found' }); - - await expect( - reactivateStreamChatUsers('key', 'secret', ['nonexistent']) - ).resolves.toBeUndefined(); - }); - - it('throws on non-404 HTTP errors', async () => { - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - text: async () => 'Internal Server Error', - }); - - await expect(reactivateStreamChatUsers('key', 'secret', ['user-1'])).rejects.toThrow('500'); - }); -}); - -describe('setupDefaultStreamChatChannel', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - function mockOk() { - mockFetch.mockResolvedValue({ ok: true, status: 200 }); - } - - it('reactivates, upserts, creates channel, and returns correct IDs and bot token', async () => { - mockOk(); - const result = await setupDefaultStreamChatChannel('api-key', 'api-secret', 'sandbox-abc'); - - // 4 fetch calls: 2x reactivate + upsertUsers + getOrCreateChannel - expect(mockFetch).toHaveBeenCalledTimes(4); - - // First two calls are reactivate (human + bot) - const [reactivateUrl1] = mockFetch.mock.calls[0] as [string, unknown]; - expect(reactivateUrl1).toContain('/api/v2/users/sandbox-abc/reactivate'); - const [reactivateUrl2] = mockFetch.mock.calls[1] as [string, unknown]; - expect(reactivateUrl2).toContain('/api/v2/users/bot-sandbox-abc/reactivate'); - - expect(result.apiKey).toBe('api-key'); - expect(result.botUserId).toBe('bot-sandbox-abc'); - expect(result.channelId).toBe('default-sandbox-abc'); - - // Bot token should be a valid JWT; human user token is no longer returned - const botPayload = decodeJwtPayload(result.botUserToken); - expect(botPayload.user_id).toBe('bot-sandbox-abc'); - expect(result).not.toHaveProperty('userToken'); - }); - - it('uses correct channel type (messaging)', async () => { - mockOk(); - await setupDefaultStreamChatChannel('key', 'secret', 'sandbox-xyz'); - - // Channel creation is the 4th call (after 2 reactivate + 1 upsert) - const [channelUrl] = mockFetch.mock.calls[3] as [string, unknown]; - expect(channelUrl).toContain('/channels/messaging/'); - expect(channelUrl).toContain('default-sandbox-xyz'); - }); - - it('throws if upsertUsers fails', async () => { - // First two calls (reactivate) succeed, third (upsert) fails - mockFetch - .mockResolvedValueOnce({ ok: true, status: 200 }) // reactivate human - .mockResolvedValueOnce({ ok: true, status: 200 }) // reactivate bot - .mockResolvedValueOnce({ - ok: false, - status: 500, - text: async () => 'Internal Server Error', - }); - - await expect(setupDefaultStreamChatChannel('key', 'secret', 'sandbox-fail')).rejects.toThrow( - '500' - ); - }); - - it('throws if getOrCreateChannel fails', async () => { - mockFetch - .mockResolvedValueOnce({ ok: true, status: 200 }) // reactivate human - .mockResolvedValueOnce({ ok: true, status: 200 }) // reactivate bot - .mockResolvedValueOnce({ ok: true, status: 200 }) // upsertUsers succeeds - .mockResolvedValueOnce({ - ok: false, - status: 503, - text: async () => 'Service Unavailable', - }); - - await expect(setupDefaultStreamChatChannel('key', 'secret', 'sandbox-fail2')).rejects.toThrow( - '503' - ); - }); - - it('tolerates reactivate 404 (first provision, users do not exist yet)', async () => { - // Reactivate returns 404, upsert and channel creation succeed - mockFetch - .mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'Not Found' }) // reactivate human - .mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'Not Found' }) // reactivate bot - .mockResolvedValueOnce({ ok: true, status: 200 }) // upsertUsers - .mockResolvedValueOnce({ ok: true, status: 200 }); // getOrCreateChannel - - const result = await setupDefaultStreamChatChannel('api-key', 'api-secret', 'sandbox-new'); - expect(result.apiKey).toBe('api-key'); - expect(result.botUserId).toBe('bot-sandbox-new'); - }); -}); - -describe('sendMessage', () => { - const mockFetch = vi.fn(); - - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - mockFetch.mockReset(); - }); - - it('POSTs to /channels/messaging/{channelId}/message with correct payload', async () => { - mockFetch.mockResolvedValueOnce({ ok: true, status: 201 }); - - await sendMessage( - 'my-api-key', - 'my-api-secret', - 'default-sandbox-abc', - 'sandbox-abc', - 'Hello bot!' - ); - - expect(mockFetch).toHaveBeenCalledOnce(); - const [url, opts] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - expect(url).toBe( - 'https://chat.stream-io-api.com/channels/messaging/default-sandbox-abc/message?api_key=my-api-key' - ); - expect(opts.method).toBe('POST'); - expect(opts.headers['Content-Type']).toBe('application/json'); - expect(opts.headers['Stream-Auth-Type']).toBe('jwt'); - // Authorization header should be a server JWT - expect(opts.headers['Authorization']).toBeDefined(); - expect(opts.headers['Authorization'].split('.')).toHaveLength(3); - - const body = JSON.parse(opts.body as string) as { message: { text: string; user_id: string } }; - expect(body.message.text).toBe('Hello bot!'); - expect(body.message.user_id).toBe('sandbox-abc'); - }); - - it('uses a server token (server: true) for authentication', async () => { - mockFetch.mockResolvedValueOnce({ ok: true, status: 201 }); - - await sendMessage('key', 'secret', 'chan-1', 'user-1', 'test'); - - const [, opts] = mockFetch.mock.calls[0] as [ - string, - RequestInit & { headers: Record }, - ]; - const payload = decodeJwtPayload(opts.headers['Authorization']); - expect(payload.server).toBe(true); - }); - - it('throws on HTTP error with status and body in the message', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 403, - text: async () => 'User is deactivated', - }); - - await expect(sendMessage('key', 'secret', 'chan-1', 'user-1', 'test')).rejects.toThrow( - 'Stream Chat sendMessage failed (403): User is deactivated' - ); - }); - - it('preserves HTTP status on the thrown error for upstream handling', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - text: async () => 'Channel not found', - }); - - try { - await sendMessage('key', 'secret', 'chan-1', 'user-1', 'test'); - expect.unreachable('should have thrown'); - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error & { status: number }).status).toBe(404); - } - }); - - it('handles unreadable error body gracefully', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - text: async () => { - throw new Error('body read error'); - }, - }); - - await expect(sendMessage('key', 'secret', 'chan-1', 'user-1', 'test')).rejects.toThrow( - 'Stream Chat sendMessage failed (500): (unreadable)' - ); - }); -}); diff --git a/services/kiloclaw/src/stream-chat/client.ts b/services/kiloclaw/src/stream-chat/client.ts deleted file mode 100644 index f0324b3730..0000000000 --- a/services/kiloclaw/src/stream-chat/client.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Lightweight Stream Chat server-side client for Cloudflare Workers. - * - * Uses fetch + jose for token generation. Does NOT depend on the `stream-chat` - * npm package which requires Node.js APIs incompatible with CF Workers. - * - * Stream Chat REST API base: https://chat.stream-io-api.com - * Auth: api_key query param + Authorization: header - */ -import { SignJWT } from 'jose'; - -const STREAM_CHAT_API_BASE = 'https://chat.stream-io-api.com'; - -/** - * Result of provisioning a Stream Chat default channel for a new KiloClaw instance. - * Does NOT include a human user token — those are minted on demand with a short TTL - * via {@link createShortLivedUserToken}. - */ -export type StreamChatSetup = { - apiKey: string; - /** Bot user ID: `bot-{sandboxId}` */ - botUserId: string; - /** Permanent JWT for the bot user (used by the openclaw-channel-streamchat plugin) */ - botUserToken: string; - /** Default channel ID: `default-{sandboxId}` */ - channelId: string; -}; - -/** - * Generate a Stream Chat server-side JWT. - * Used for admin operations (creating users, channels) from the CF Worker. - * Payload: `{ server: true }` — gives full API access. - */ -export async function createServerToken(apiSecret: string): Promise { - const secretBytes = new TextEncoder().encode(apiSecret); - return new SignJWT({ server: true }).setProtectedHeader({ alg: 'HS256' }).sign(secretBytes); -} - -/** - * Generate a permanent Stream Chat user JWT for bot authentication. - * Payload: `{ user_id: userId }` — scoped to a single user, no expiry. - * For human/browser tokens use {@link createShortLivedUserToken} instead. - */ -export async function createUserToken(apiSecret: string, userId: string): Promise { - const secretBytes = new TextEncoder().encode(apiSecret); - return new SignJWT({ user_id: userId }).setProtectedHeader({ alg: 'HS256' }).sign(secretBytes); -} - -/** Default TTL for browser-facing Stream Chat user tokens. */ -export const USER_TOKEN_TTL = '6h'; - -/** - * Generate a short-lived Stream Chat user JWT for browser authentication. - * Payload: `{ user_id: userId }` with `iat` and `exp` claims. - * The token expires after {@link USER_TOKEN_TTL} so that revoked users lose - * access without requiring an app-secret rotation. - */ -export async function createShortLivedUserToken( - apiSecret: string, - userId: string -): Promise { - const secretBytes = new TextEncoder().encode(apiSecret); - return new SignJWT({ user_id: userId }) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime(USER_TOKEN_TTL) - .sign(secretBytes); -} - -/** - * Upsert one or more Stream Chat users via the server API. - * Creates the user if it doesn't exist; updates fields if it does. - */ -export async function upsertStreamChatUsers( - apiKey: string, - serverToken: string, - users: ReadonlyArray<{ id: string; name: string; role?: string }> -): Promise { - const usersMap: Record = {}; - for (const user of users) { - usersMap[user.id] = user; - } - - const res = await fetch(`${STREAM_CHAT_API_BASE}/users?api_key=${apiKey}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: serverToken, - }, - body: JSON.stringify({ users: usersMap }), - }); - - if (!res.ok) { - const body = await res.text().catch(() => '(unreadable)'); - throw new Error(`Stream Chat upsertUsers failed (${res.status}): ${body}`); - } -} - -/** - * Get or create a Stream Chat channel. - * Idempotent: safe to call on an existing channel. - */ -export async function getOrCreateStreamChatChannel( - apiKey: string, - serverToken: string, - channelType: string, - channelId: string, - data: { created_by_id: string; members: string[]; name?: string } -): Promise { - const res = await fetch( - `${STREAM_CHAT_API_BASE}/channels/${channelType}/${channelId}/query?api_key=${apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: serverToken, - }, - body: JSON.stringify({ data }), - } - ); - - if (!res.ok) { - const body = await res.text().catch(() => '(unreadable)'); - throw new Error(`Stream Chat getOrCreateChannel failed (${res.status}): ${body}`); - } -} - -/** - * Deactivate one or more Stream Chat users via the server API. - * Deactivated users cannot connect to Stream Chat or send/receive messages, - * making any previously issued tokens useless. - * Silently ignores 404 (user not found). Attempts all users before throwing - * so that a transient failure for one user doesn't leave others active. - */ -export async function deactivateStreamChatUsers( - apiKey: string, - apiSecret: string, - userIds: readonly string[] -): Promise { - const serverToken = await createServerToken(apiSecret); - const errors: Error[] = []; - for (const userId of userIds) { - const res = await fetch( - `${STREAM_CHAT_API_BASE}/api/v2/users/${encodeURIComponent(userId)}/deactivate?api_key=${apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: serverToken, - }, - body: JSON.stringify({}), - } - ); - // 404 = user never existed, safe to ignore - if (!res.ok && res.status !== 404) { - const body = await res.text().catch(() => '(unreadable)'); - errors.push( - new Error(`Stream Chat deactivateUser failed for ${userId} (${res.status}): ${body}`) - ); - } - } - if (errors.length === 1) throw errors[0]; - if (errors.length > 1) { - throw new AggregateError(errors, 'Stream Chat deactivateUsers had failures'); - } -} - -/** - * Reactivate one or more previously deactivated Stream Chat users. - * Called during re-provision to ensure users can connect again. - * Silently ignores 404 (user not found). Attempts all users before throwing - * so that a transient failure for one user doesn't leave others deactivated. - */ -export async function reactivateStreamChatUsers( - apiKey: string, - apiSecret: string, - userIds: readonly string[] -): Promise { - const serverToken = await createServerToken(apiSecret); - const errors: Error[] = []; - for (const userId of userIds) { - const res = await fetch( - `${STREAM_CHAT_API_BASE}/api/v2/users/${encodeURIComponent(userId)}/reactivate?api_key=${apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: serverToken, - }, - body: JSON.stringify({}), - } - ); - // 404 = user never existed, safe to ignore - if (!res.ok && res.status !== 404) { - const body = await res.text().catch(() => '(unreadable)'); - errors.push( - new Error(`Stream Chat reactivateUser failed for ${userId} (${res.status}): ${body}`) - ); - } - } - if (errors.length === 1) throw errors[0]; - if (errors.length > 1) { - throw new AggregateError(errors, 'Stream Chat reactivateUsers had failures'); - } -} - -/** - * Provision the default Stream Chat channel for a new KiloClaw instance. - * - * Creates (or re-uses if already existing): - * - A human user with ID `{sandboxId}` - * - A per-instance bot user with ID `bot-{sandboxId}` - * - A messaging channel `default-{sandboxId}` with both as members - * - * Returns tokens and IDs needed to configure the machine and optionally the browser client. - */ -export async function setupDefaultStreamChatChannel( - apiKey: string, - apiSecret: string, - sandboxId: string -): Promise { - const serverToken = await createServerToken(apiSecret); - - const humanUserId = sandboxId; - const botUserId = `bot-${sandboxId}`; - const channelId = `default-${sandboxId}`; - - // Reactivate users in case they were deactivated by a prior destroy. - // This is a no-op for first-time provisioning (404s are silently ignored). - await reactivateStreamChatUsers(apiKey, apiSecret, [humanUserId, botUserId]); - - // Create/upsert both users - await upsertStreamChatUsers(apiKey, serverToken, [ - { id: humanUserId, name: 'User' }, - { id: botUserId, name: 'KiloClaw', role: 'admin' }, - ]); - - // Create the default channel with both members - await getOrCreateStreamChatChannel(apiKey, serverToken, 'messaging', channelId, { - created_by_id: humanUserId, - members: [humanUserId, botUserId], - name: 'KiloClaw', - }); - - // Generate a permanent token for the bot user only. - // Human user tokens are minted on demand with a short TTL (see createShortLivedUserToken). - const botUserToken = await createUserToken(apiSecret, botUserId); - - return { apiKey, botUserId, botUserToken, channelId }; -} - -/** - * Send a message to a Stream Chat channel on behalf of a user. - * - * Used to programmatically inject messages into a KiloClaw instance's chat - * channel. The message appears as if the user typed it, so the OpenClaw bot - * (listening via the openclaw-channel-streamchat plugin) processes and responds. - * - * @param apiKey Stream Chat API key - * @param apiSecret Stream Chat API secret (used to mint a server JWT) - * @param channelId Target channel ID, e.g. `default-{sandboxId}` - * @param userId The user ID to send the message as (typically the sandboxId) - * @param text Plain-text message content - */ -export async function sendMessage( - apiKey: string, - apiSecret: string, - channelId: string, - userId: string, - text: string -): Promise { - const serverToken = await createServerToken(apiSecret); - - const res = await fetch( - `${STREAM_CHAT_API_BASE}/channels/messaging/${channelId}/message?api_key=${apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Stream-Auth-Type': 'jwt', - Authorization: serverToken, - }, - body: JSON.stringify({ - message: { text, user_id: userId }, - }), - } - ); - - if (!res.ok) { - const body = await res.text().catch(() => '(unreadable)'); - throw Object.assign(new Error(`Stream Chat sendMessage failed (${res.status}): ${body}`), { - status: res.status, - }); - } -} diff --git a/services/kiloclaw/src/types.ts b/services/kiloclaw/src/types.ts index a235877de6..e6fc74eab7 100644 --- a/services/kiloclaw/src/types.ts +++ b/services/kiloclaw/src/types.ts @@ -76,10 +76,6 @@ export type KiloClawEnv = { // Developer identity (development only, auto-populated by dev-start from `fly auth whoami`) DEV_CREATOR?: string; - // Stream Chat (default channel for new instances) - STREAM_CHAT_API_KEY?: string; - STREAM_CHAT_API_SECRET?: string; - // OpenClaw gateway configuration OPENCLAW_ALLOWED_ORIGINS?: string; KILOCLAW_CHECKIN_URL?: string; diff --git a/services/kiloclaw/worker-configuration.d.ts b/services/kiloclaw/worker-configuration.d.ts index b315fcdf88..b4e9784cff 100644 --- a/services/kiloclaw/worker-configuration.d.ts +++ b/services/kiloclaw/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 3a95699adda0e86ca43ba58928903a17) +// Generated by Wrangler by running `wrangler types` (hash: bf2dc9695d3fc36b376b6ca04e7fee27) // Runtime types generated with workerd@1.20260312.1 2025-05-06 nodejs_compat declare namespace Cloudflare { interface GlobalProps { @@ -12,47 +12,44 @@ declare namespace Cloudflare { KILOCLAW_AE: AnalyticsEngineDataset; KILOCLAW_CONTROLLER_AE: AnalyticsEngineDataset; SNAPSHOT_RESTORE_QUEUE: Queue; - KILOCHAT_BASE_URL: "https://chat.kiloapps.io"; + NF_TEAM_ID: "kilo-prod"; + NF_REGION: "us-central"; + NF_DEPLOYMENT_PLAN: "nf-compute-200"; + NF_EDGE_HEADER_NAME: "x-kiloclaw-northflank-edge-prod"; + NF_IMAGE_PATH_TEMPLATE: "ghcr.io/kilo-org/kiloclaw:{tag}"; + NF_IMAGE_CREDENTIALS_ID: "kiloclaw"; REQUIRE_PROXY_TOKEN: "true"; PROACTIVE_REFRESH_THRESHOLD_HOURS: "72"; NEXTAUTH_SECRET: string; - KILOCLAW_INTERNAL_API_SECRET: string; + INTERNAL_API_SECRET: string; GATEWAY_TOKEN_SECRET: string; WORKER_ENV: string; - KILOCODE_API_BASE_URL: string; - FLY_REGISTRY_APP: string; - FLY_ORG_SLUG: string; + BACKEND_API_URL: string; FLY_API_TOKEN: string; - FLY_APP_NAME: string; + FLY_ORG_SLUG: string; + FLY_REGISTRY_APP: string; FLY_REGION: string; + FLY_IMAGE_TAG: string; + OPENCLAW_VERSION: string; + FLY_APP_NAME: string; OPENCLAW_ALLOWED_ORIGINS: string; - AGENT_ENV_VARS_PRIVATE_KEY: string; - DEV_CREATOR: string; - BACKEND_API_URL: string; + NEXT_PUBLIC_POSTHOG_KEY: string; STREAM_CHAT_API_KEY: string; STREAM_CHAT_API_SECRET: string; + KILOCHAT_API_TOKEN: string; + KILOCHAT_WEBHOOK_SECRET: string; + KILOCHAT_BASE_URL: string; + KILOCLAW_DEFAULT_PROVIDER: string; KILOCLAW_CHECKIN_URL: string; - NEXT_PUBLIC_POSTHOG_KEY: string; - FLY_IMAGE_TAG: string; + KILOCODE_API_BASE_URL: string; FLY_IMAGE_DIGEST: string; - OPENCLAW_VERSION: string; FLY_IMAGE_CONTENT_HASH: string; + KILOCLAW_INTERNAL_API_SECRET: string; DOCKER_LOCAL_API_BASE: string; DOCKER_LOCAL_IMAGE: string; DOCKER_LOCAL_PORT_RANGE: string; - NF_API_TOKEN: string; - NF_API_BASE: string; - NF_TEAM_ID: string; - NF_REGION: string; - NF_DEPLOYMENT_PLAN: string; - NF_STORAGE_CLASS_NAME: string; - NF_STORAGE_ACCESS_MODE: string; - NF_VOLUME_SIZE_MB: string; - NF_EPHEMERAL_STORAGE_MB: string; - NF_EDGE_HEADER_NAME: string; - NF_EDGE_HEADER_VALUE: string; - NF_IMAGE_PATH_TEMPLATE: string; - NF_IMAGE_CREDENTIALS_ID: string; + DEV_CREATOR: string; + GOOGLE_WORKSPACE_OAUTH_REDIRECT_URI: string; KILOCLAW_INSTANCE: DurableObjectNamespace; KILOCLAW_APP: DurableObjectNamespace; KILOCLAW_REGISTRY: DurableObjectNamespace; @@ -65,7 +62,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types