diff --git a/apps/expo/app/(app)/(tabs)/profile/index.tsx b/apps/expo/app/(app)/(tabs)/profile/index.tsx index 13605fadb4..be08387b8c 100644 --- a/apps/expo/app/(app)/(tabs)/profile/index.tsx +++ b/apps/expo/app/(app)/(tabs)/profile/index.tsx @@ -1,9 +1,7 @@ import { clientEnvs } from '@packrat/env/expo-client'; import { isString } from '@packrat/guards'; -import type { AlertMethods } from '@packrat/ui/nativewindui'; import { ActivityIndicator, - Alert as AlertComponent, Avatar, AvatarFallback, AvatarImage, @@ -18,6 +16,7 @@ import { import AsyncStorage from '@react-native-async-storage/async-storage'; import { AndroidTabBarInsetFix } from 'expo-app/components/AndroidTabBarInsetFix'; import { Icon } from 'expo-app/components/Icon'; +import { isLoadingAtom, suppressSignOutNavAtom } from 'expo-app/features/auth/atoms/authAtoms'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useAuth } from 'expo-app/features/auth/hooks/useAuth'; import { useUser } from 'expo-app/features/auth/hooks/useUser'; @@ -33,8 +32,8 @@ import { testIds } from 'expo-app/lib/testIds'; import { buildPackTemplateItemImageUrl } from 'expo-app/lib/utils/buildPackTemplateItemImageUrl'; import * as FileSystem from 'expo-file-system/legacy'; import { Link, router, Stack } from 'expo-router'; -import * as Updates from 'expo-updates'; -import { useRef, useState } from 'react'; +import { useSetAtom } from 'jotai'; +import { useState } from 'react'; import { Alert, Linking, Platform, Pressable, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -246,44 +245,47 @@ function ListHeaderComponent() { function ListFooterComponent() { const { signOut } = useAuth(); - const { colors } = useColorScheme(); const { t } = useTranslation(); + const setIsLoading = useSetAtom(isLoadingAtom); + const setSuppressSignOutNav = useSetAtom(suppressSignOutNavAtom); - const alertRef = useRef(null); const [isSigningOut, setIsSigningOut] = useState(false); const handleSignOut = async () => { - try { - setIsSigningOut(true); - await signOut(); - alertRef.current?.alert({ - title: t('auth.loggedOut'), - message: t('auth.loggedOutMessage'), - materialIcon: { name: 'check-circle-outline', color: colors.green }, - buttons: [ - { - text: t('auth.stayLoggedOut'), - style: 'cancel', - onPress: async () => { - await AsyncStorage.setItem('skipped_login', 'true'); - await Updates.reloadAsync(); - }, + setIsSigningOut(true); + await signOut(); + setIsSigningOut(false); + + // signOut() has completed: auth cleared, spinner showing, auto-navigation + // suppressed. Ask the user what to do next. + Alert.alert( + t('auth.loggedOut'), + t('auth.loggedOutMessage'), + [ + { + text: t('auth.stayLoggedOut'), + onPress: async () => { + // Clear spinner first so AppLayout doesn't show auth screen, + // then release the suppress flag and navigate home as guest. + setIsLoading(false); + setSuppressSignOutNav(false); + await AsyncStorage.setItem('skipped_login', 'true'); + router.replace('/'); }, - { - text: t('auth.signInAgain'), - style: 'default', - onPress: async () => { - await AsyncStorage.setItem('skipped_login', 'false'); - await Updates.reloadAsync(); - }, + }, + { + text: t('auth.signInAgain'), + style: 'destructive', + onPress: () => { + // Release suppress while isLoadingAtom is still true — AppLayout's + // useEffect sees isLoadingGlobal=true && !isAuthed and navigates to + // /auth via the NativeTabs-safe useEffect path. + setSuppressSignOutNav(false); }, - ], - }); - } catch (error) { - console.error('Logout failed:', error); - } finally { - setIsSigningOut(false); - } + }, + ], + { cancelable: false }, + ); }; return ( @@ -294,22 +296,13 @@ function ListFooterComponent() { disabled={isSigningOut} onPress={() => { if (hasUnsyncedChanges()) { - alertRef.current?.alert({ - title: t('profile.syncInProgress'), - message: t('profile.syncMessage'), - materialIcon: { name: 'repeat' }, - buttons: [ - { - text: t('common.cancel'), - style: 'cancel', - }, - { - text: t('auth.proceedLogOut'), - style: 'destructive', - onPress: handleSignOut, - }, - ], - }); + // Use native Alert on both platforms so the dialog buttons are + // accessible to automated testing tools (custom portal-based + // dialogs are not surfaced in XCTest/UIAutomator accessibility trees). + Alert.alert(t('profile.syncInProgress'), t('profile.syncMessage'), [ + { text: t('common.cancel'), style: 'cancel' }, + { text: t('auth.logOut'), style: 'destructive', onPress: handleSignOut }, + ]); return; } handleSignOut(); @@ -324,7 +317,6 @@ function ListFooterComponent() { {t('auth.logOut')} )} - diff --git a/apps/expo/app/(app)/_layout.tsx b/apps/expo/app/(app)/_layout.tsx index c6b2be62fb..81ed80ec82 100644 --- a/apps/expo/app/(app)/_layout.tsx +++ b/apps/expo/app/(app)/_layout.tsx @@ -1,7 +1,13 @@ +import { use$ } from '@legendapp/state/react'; import { ActivityIndicator } from '@packrat/ui/nativewindui'; import { ThemeToggle } from 'expo-app/components/ThemeToggle'; -import { needsReauthAtom } from 'expo-app/features/auth/atoms/authAtoms'; +import { + isLoadingAtom, + needsReauthAtom, + suppressSignOutNavAtom, +} from 'expo-app/features/auth/atoms/authAtoms'; import { useAuthInit } from 'expo-app/features/auth/hooks/useAuthInit'; +import { isAuthed } from 'expo-app/features/auth/store'; import { getPackTemplateDetailOptions } from 'expo-app/features/pack-templates/utils/getPackTemplateDetailOptions'; import { getPackTemplateItemDetailOptions } from 'expo-app/features/pack-templates/utils/getPackTemplateItemDetailOptions'; import SyncBanner from 'expo-app/features/packs/components/SyncBanner'; @@ -10,10 +16,12 @@ import { getPackItemDetailOptions } from 'expo-app/features/packs/utils/getPackI import { getTripDetailOptions } from 'expo-app/features/trips/utils/getTripDetailOptions'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import type { TranslationFunction } from 'expo-app/lib/i18n/types'; +import { testIds } from 'expo-app/lib/testIds'; import 'expo-dev-client'; -import { Stack } from 'expo-router'; +import { type Href, router, Stack, useRouter } from 'expo-router'; import { useAtomValue } from 'jotai'; -import { View } from 'react-native'; +import { useEffect, useRef } from 'react'; +import { Pressable, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export { @@ -23,11 +31,46 @@ export { export default function AppLayout() { const isLoading = useAuthInit(); + const isAuthedValue = use$(isAuthed); const { t } = useTranslation(); const needsReauth = useAtomValue(needsReauthAtom); + const isLoadingGlobal = useAtomValue(isLoadingAtom); + const suppressSignOutNav = useAtomValue(suppressSignOutNavAtom); const insets = useSafeAreaInsets(); + // Latches true once we dispatch router.replace('/auth') on sign-out. + // Keeps the spinner rendered until AppLayout unmounts so that + // auth/index.tsx resetting isLoadingAtom=false never causes AppLayout + // to re-render its Stack mid-transition. If the Stack re-initialized + // while the root navigator was still committing the replace, it would + // re-register with React Navigation and override the in-flight navigation, + // landing the user back on the Trips/Profile screen instead of auth. + const hasNavigatedToAuthRef = useRef(false); - if (isLoading) { + useEffect(() => { + // suppressSignOutNav is true while profile/handleSignOut is showing the + // post-sign-out prompt; skip auto-navigation until the user picks an option. + if (isLoadingGlobal && !isAuthedValue && !suppressSignOutNav) { + hasNavigatedToAuthRef.current = true; + // safe-cast: '/auth' is a compile-time string literal recognised by expo-router + router.replace('/auth' as Href); + } + }, [isLoadingGlobal, isAuthedValue, suppressSignOutNav]); + + // If the user has re-authenticated while AppLayout stayed mounted (Expo Router + // keeps the (app) screen in the stack during the auth transition), clear the + // sign-out latch so the spinner doesn't stay on indefinitely. + if (isAuthedValue && hasNavigatedToAuthRef.current) { + hasNavigatedToAuthRef.current = false; + } + + // Show spinner when: (a) auth initialising on cold start, OR (b) a sign-out + // is in progress (isLoadingAtom=true) AND the user is no longer authenticated. + // The spinner unmounts NativeTabs so the useEffect above can dispatch to the + // root Stack. The !isAuthedValue guard keeps the Stack visible during re-auth + // sign-in, where isLoadingAtom is also true but the user is still authed. + // hasNavigatedToAuthRef keeps the spinner until AppLayout actually unmounts + // after the router.replace('/auth') transition completes. + if (isLoading || (isLoadingGlobal && !isAuthedValue) || hasNavigatedToAuthRef.current) { return ( @@ -285,12 +328,19 @@ const getSettingsOptions = (t: TranslationFunction) => headerRight: () => , }) as const; -const getTripNewOptions = (t: TranslationFunction) => - ({ - title: t('trips.createTrip'), - presentation: 'modal', - animation: 'slide_from_bottom', - }) as const; +const getTripNewOptions = (t: TranslationFunction) => ({ + title: t('trips.createTrip'), + presentation: 'modal' as const, + animation: 'slide_from_bottom' as const, + headerLeft: () => { + const router = useRouter(); + return ( + router.back()} className="px-2"> + {t('common.cancel')} + + ); + }, +}); const getTripEditOptions = (t: TranslationFunction) => ({ @@ -311,12 +361,19 @@ const CONSENT_MODAL_OPTIONS = { animation: 'fade_from_bottom', // for android } as const; -const getPackNewOptions = (t: TranslationFunction) => - ({ - title: t('packs.createPack'), - presentation: 'modal', - animation: 'fade_from_bottom', // for android - }) as const; +const getPackNewOptions = (t: TranslationFunction) => ({ + title: t('packs.createPack'), + presentation: 'modal' as const, + animation: 'fade_from_bottom' as const, + headerLeft: () => { + const router = useRouter(); + return ( + router.back()} className="px-2"> + {t('common.cancel')} + + ); + }, +}); const getItemNewOptions = (t: TranslationFunction) => ({ diff --git a/apps/expo/app/(app)/ai-chat.tsx b/apps/expo/app/(app)/ai-chat.tsx index 0088688673..24b10ed0c3 100644 --- a/apps/expo/app/(app)/ai-chat.tsx +++ b/apps/expo/app/(app)/ai-chat.tsx @@ -20,9 +20,9 @@ import { LocationContext } from 'expo-app/features/ai/components/LocationContext import { CustomChatTransport } from 'expo-app/features/ai/lib/CustomChatTransport'; import { getLocalModel, initLocalModel } from 'expo-app/features/ai/lib/localModelManager'; import { createLocalTools } from 'expo-app/features/ai/lib/tools'; -import { tokenAtom } from 'expo-app/features/auth/atoms/authAtoms'; import { useActiveLocation } from 'expo-app/features/weather/hooks'; import type { WeatherLocation } from 'expo-app/features/weather/types'; +import { authClient } from 'expo-app/lib/auth-client'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { getContextualGreeting, getContextualSuggestions } from 'expo-app/utils/chatContextHelpers'; @@ -90,7 +90,8 @@ export default function AIChat() { const locationRef = React.useRef(context.location); locationRef.current = context.location; - const token = useAtomValue(tokenAtom); + const { data: _authSession } = authClient.useSession(); + const token = _authSession?.session?.token ?? null; const [input, setInput] = React.useState(''); const [lastUserMessage, setLastUserMessage] = React.useState(''); const [previousMessages, setPreviousMessages] = React.useState([]); diff --git a/apps/expo/app/(app)/current-pack/[id].tsx b/apps/expo/app/(app)/current-pack/[id].tsx index 2a37709625..db114c3fe7 100644 --- a/apps/expo/app/(app)/current-pack/[id].tsx +++ b/apps/expo/app/(app)/current-pack/[id].tsx @@ -155,7 +155,7 @@ export default function CurrentPackScreen() { {t('packs.lastUpdated', { - time: getRelativeTime(pack.localUpdatedAt ?? pack.updatedAt, t as any), + time: getRelativeTime(pack.localUpdatedAt ?? pack.updatedAt, t), })} diff --git a/apps/expo/app/(app)/feed/[id].tsx b/apps/expo/app/(app)/feed/[id].tsx index c928127dc5..59fcfdee8c 100644 --- a/apps/expo/app/(app)/feed/[id].tsx +++ b/apps/expo/app/(app)/feed/[id].tsx @@ -8,7 +8,7 @@ import { ActivityIndicator, View } from 'react-native'; export default function PostDetailRoute() { const { id } = useLocalSearchParams<{ id: string }>(); - const currentUserId = userStore.id.peek() as number | undefined; + const currentUserId = userStore.id.peek() as string | undefined; const { data: post, isLoading } = useQuery({ queryKey: ['feed', Number(id)], diff --git a/apps/expo/app/(app)/recent-packs.tsx b/apps/expo/app/(app)/recent-packs.tsx index 6ab73c53fb..20fa2db13a 100644 --- a/apps/expo/app/(app)/recent-packs.tsx +++ b/apps/expo/app/(app)/recent-packs.tsx @@ -34,7 +34,7 @@ function RecentPackCard({ pack }: { pack: Pack }) { {pack.totalWeight ?? 0} g - {getRelativeTime(pack.localCreatedAt ?? pack.createdAt, t as any)} + {getRelativeTime(pack.localCreatedAt ?? pack.createdAt, t)} @@ -45,7 +45,7 @@ function RecentPackCard({ pack }: { pack: Pack }) { {t('packs.lastUpdated', { - time: getRelativeTime(pack.localUpdatedAt ?? pack.updatedAt, t as any), + time: getRelativeTime(pack.localUpdatedAt ?? pack.updatedAt, t), })} diff --git a/apps/expo/app/auth/(login)/reset-password.tsx b/apps/expo/app/auth/(login)/reset-password.tsx index dc553a4d91..6571fab95b 100644 --- a/apps/expo/app/auth/(login)/reset-password.tsx +++ b/apps/expo/app/auth/(login)/reset-password.tsx @@ -136,7 +136,7 @@ export default function ResetPasswordScreen() { setIsLoading(true); // Call the API to reset the password - await resetPassword(params.email, { code: params.code, newPassword: value.password }); + await resetPassword(params.email, { token: params.code, newPassword: value.password }); // Show success message and navigate to login Alert.alert(t('common.success'), t('auth.resetPasswordSuccess'), [ diff --git a/apps/expo/features/auth/atoms/authAtoms.ts b/apps/expo/features/auth/atoms/authAtoms.ts index 0eb302de7f..11668da28c 100644 --- a/apps/expo/features/auth/atoms/authAtoms.ts +++ b/apps/expo/features/auth/atoms/authAtoms.ts @@ -1,25 +1,8 @@ -import kvStorage from 'expo-app/lib/kvStorage'; import { atom } from 'jotai'; -import { atomWithStorage } from 'jotai/utils'; -// User type definition -export type User = { - id: number; - email: string; - firstName?: string; - lastName?: string; - emailVerified: boolean; -}; - -// Token storage atom -export const tokenAtom = atomWithStorage('access_token', null, kvStorage); - -export const refreshTokenAtom = atomWithStorage('refresh_token', null, kvStorage); - -// Loading state atom export const isLoadingAtom = atom(false); - export const redirectToAtom = atom('/'); - -// Re-authentication state export const needsReauthAtom = atom(false); +// Prevents AppLayout's useEffect from auto-navigating to /auth during the +// sign-out flow so the profile screen can show a post-sign-out prompt first. +export const suppressSignOutNavAtom = atom(false); diff --git a/apps/expo/features/auth/hooks/useAuthActions.ts b/apps/expo/features/auth/hooks/useAuthActions.ts index d7892300c1..fbe6802081 100644 --- a/apps/expo/features/auth/hooks/useAuthActions.ts +++ b/apps/expo/features/auth/hooks/useAuthActions.ts @@ -1,4 +1,3 @@ -import { isObject } from '@packrat/guards'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { GoogleSignin, @@ -7,9 +6,10 @@ import { } from '@react-native-google-signin/google-signin'; import { userStore } from 'expo-app/features/auth/store'; import type { User } from 'expo-app/features/profile/types'; -import { apiClient } from 'expo-app/lib/api/packrat'; +import { authClient } from 'expo-app/lib/auth-client'; import { t } from 'expo-app/lib/i18n'; import ImageCacheManager from 'expo-app/lib/utils/ImageCacheManager'; +import { queryClient } from 'expo-app/providers/TanstackProvider'; import * as AppleAuthentication from 'expo-apple-authentication'; import { type Href, router } from 'expo-router'; import Storage from 'expo-sqlite/kv-store'; @@ -19,8 +19,7 @@ import { isLoadingAtom, needsReauthAtom, redirectToAtom, - refreshTokenAtom, - tokenAtom, + suppressSignOutNavAtom, } from '../atoms/authAtoms'; function redirect(route: string) { @@ -28,52 +27,50 @@ function redirect(route: string) { const parsedRoute: Href = JSON.parse(route); return router.dismissTo(parsedRoute); } catch { - // safe-cast: route is a plain string path from redirectToAtom (atom); - // Expo Router's Href accepts string paths directly. - router.dismissTo(route as Href); + router.dismissTo(route as Href); // safe-cast: Href = string | HrefObject; string literal branch failed JSON.parse so plain string is the correct type here } } -function extractAuthError(value: unknown, fallback: string): string { - if (isObject(value) && 'error' in value) { - // safe-cast: value is an object (checked above); indexed access to extract error field - return String((value as Record).error) || fallback; - } - return fallback; +function mapToUser(raw: Record): User { + const name = String(raw.name ?? ''); + const spaceIdx = name.indexOf(' '); + return { + id: String(raw.id ?? ''), + email: String(raw.email ?? ''), + firstName: spaceIdx >= 0 ? name.slice(0, spaceIdx) : name, + lastName: spaceIdx >= 0 ? name.slice(spaceIdx + 1) : '', + role: (raw.role as 'USER' | 'ADMIN') ?? 'USER', + avatarUrl: (raw.image as string | null) ?? null, + preferredWeightUnit: (raw.preferredWeightUnit as User['preferredWeightUnit']) ?? 'g', + }; } export function useAuthActions() { - const setToken = useSetAtom(tokenAtom); - const setRefreshToken = useSetAtom(refreshTokenAtom); - const refreshToken = useAtomValue(refreshTokenAtom); const setIsLoading = useSetAtom(isLoadingAtom); const redirectTo = useAtomValue(redirectToAtom); const setNeedsReauth = useSetAtom(needsReauthAtom); + const setSuppressSignOutNav = useSetAtom(suppressSignOutNavAtom); const clearLocalData = async () => { + queryClient.clear(); const allKeys = await Storage.getAllKeys(); await Promise.all(allKeys.map((key) => Storage.removeItem(key))); - await AsyncStorage.clear(); - await ImageCacheManager.clearCache(); }; + const applySession = (user: Record) => { + userStore.set(mapToUser(user)); + setNeedsReauth(false); + redirect(redirectTo); + }; + const signIn = async (email: string, password: string) => { setIsLoading(true); try { - const { data, error } = await apiClient.auth.login.post({ email, password }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToSignIn'))); - } - - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary - userStore.set(data.user as unknown as User); - - setNeedsReauth(false); - redirect(redirectTo); + const { data, error } = await authClient.signIn.email({ email, password }); + if (error) throw new Error(error.message ?? 'Sign in failed'); + applySession(data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } catch (error) { console.error('Sign in error:', error); throw error; @@ -83,29 +80,20 @@ export function useAuthActions() { }; const signInWithGoogle = async () => { + setIsLoading(true); try { - setIsLoading(true); - await GoogleSignin.hasPlayServices(); - const _userInfo = await GoogleSignin.signIn(); + await GoogleSignin.signIn(); const { idToken } = await GoogleSignin.getTokens(); - if (!idToken) { - throw new Error(t('auth.noIdTokenFromGoogle')); - } + if (!idToken) throw new Error(t('auth.noIdTokenFromGoogle')); - const { data, error } = await apiClient.auth.google.post({ idToken }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToSignInWithGoogle'))); - } - - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary - userStore.set(data.user as unknown as User); - - setNeedsReauth(false); - redirect(redirectTo); + const { data, error } = await authClient.signIn.social({ + provider: 'google', + idToken: { token: idToken }, + }); + if (error) throw new Error(error.message ?? 'Google sign in failed'); + if (data && 'user' in data && data.user) applySession(data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } catch (error) { setIsLoading(false); @@ -118,19 +106,15 @@ export function useAuthActions() { } else { console.error('Google sign in error:', error); } - throw error; } }; const signInWithApple = async () => { + setIsLoading(true); try { - setIsLoading(true); - const isAvailable = await AppleAuthentication.isAvailableAsync(); - if (!isAvailable) { - throw new Error(t('auth.appleSignInNotAvailable')); - } + if (!isAvailable) throw new Error(t('auth.appleSignInNotAvailable')); const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ @@ -139,21 +123,12 @@ export function useAuthActions() { ], }); - const { data, error } = await apiClient.auth.apple.post({ - identityToken: credential.identityToken ?? '', - authorizationCode: credential.authorizationCode ?? '', + const { data, error } = await authClient.signIn.social({ + provider: 'apple', + idToken: { token: credential.identityToken ?? '' }, }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToSignInWithApple'))); - } - - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary - userStore.set(data.user as unknown as User); - - setNeedsReauth(false); - redirect(redirectTo); + if (error) throw new Error(error.message ?? 'Apple sign in failed'); + if (data && 'user' in data && data.user) applySession(data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } catch (error) { console.error('Apple sign in error:', error); throw error; @@ -175,15 +150,9 @@ export function useAuthActions() { }) => { setIsLoading(true); try { - const { error } = await apiClient.auth.register.post({ - email, - password, - firstName, - lastName, - }); - if (error) { - throw new Error(extractAuthError(error.value, t('auth.registrationFailed'))); - } + const name = [firstName, lastName].filter(Boolean).join(' ') || email; + const { error } = await authClient.signUp.email({ email, password, name }); + if (error) throw new Error(error.message ?? 'Sign up failed'); } catch (error) { console.error('Registration error:', error instanceof Error ? error.message : String(error)); throw error; @@ -193,105 +162,68 @@ export function useAuthActions() { }; const signOut = async () => { + // Suppress AppLayout's auto-navigation to /auth so the profile screen can + // show a post-sign-out prompt and handle navigation itself. + setSuppressSignOutNav(true); setIsLoading(true); try { const isSignedIn = await GoogleSignin.hasPreviousSignIn(); - if (isSignedIn) { - await GoogleSignin.signOut(); - } - - if (refreshToken) { - await apiClient.auth.logout.post({ refreshToken }); - } + if (isSignedIn) await GoogleSignin.signOut(); + await authClient.signOut(); } catch (error) { console.error('Sign out error:', error); } finally { - setToken(null); - setRefreshToken(null); + userStore.set(null); await clearLocalData(); setNeedsReauth(false); - setIsLoading(false); + // isLoadingAtom intentionally left true — the caller (profile/handleSignOut) + // shows a post-sign-out Alert and is responsible for clearing it and + // navigating (either to '/' for guest mode or to /auth via releasing + // suppressSignOutNav while isLoadingAtom is still true). } }; const forgotPassword = async (email: string) => { - try { - const { data, error } = await apiClient.auth['forgot-password'].post({ email }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToProcessRequest'))); - } - return data; - } catch (error) { - console.error('Forgot password error:', error); - throw error; - } + const { error } = await authClient.requestPasswordReset({ + email, + redirectTo: 'packrat://reset-password', + }); + if (error) throw new Error(error.message ?? 'Forgot password failed'); }; - const resetPassword = async (email: string, opts: { code: string; newPassword: string }) => { - const { code, newPassword } = opts; - try { - const { data, error } = await apiClient.auth['reset-password'].post({ - email, - code, - newPassword, - }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.resetPasswordFailed'))); - } - return data; - } catch (error) { - console.error('Reset password error:', error); - throw error; - } + const resetPassword = async (_email: string, opts: { token: string; newPassword: string }) => { + const { error } = await authClient.resetPassword({ + token: opts.token, + newPassword: opts.newPassword, + }); + if (error) throw new Error(error.message ?? 'Reset password failed'); }; - const verifyEmail = async (email: string, code: string) => { - try { - const { data, error } = await apiClient.auth['verify-email'].post({ email, code }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToVerifyEmail'))); - } + const verifyEmail = async (_email: string, token: string) => { + const { data, error } = await authClient.verifyEmail({ query: { token } }); + if (error) throw new Error(error.message ?? 'Email verification failed'); - if (data.accessToken && data.refreshToken && data.user) { - await Storage.setItem('access_token', data.accessToken); - await Storage.setItem('refresh_token', data.refreshToken); - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - // safe-cast: Treaty response type differs from local User type; Zod-validated at API boundary - userStore.set(data.user as unknown as User); - redirect(redirectTo); - } - - return data; - } catch (error) { - console.error('Email verification error:', error); - throw error; + const session = await authClient.getSession(); + if (session.data?.user) { + applySession(session.data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } + return data; }; const resendVerificationEmail = async (email: string) => { - try { - const { data, error } = await apiClient.auth['resend-verification'].post({ email }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToResendVerificationEmail'))); - } - return data; - } catch (error) { - console.error('Resend verification email error:', error); - throw error; - } + const { error } = await authClient.sendVerificationEmail({ + email, + callbackURL: 'packrat://verify-email', + }); + if (error) throw new Error(error.message ?? 'Failed to resend verification email'); }; const deleteAccount = async () => { setIsLoading(true); try { - const { error } = await apiClient.auth.delete(); - if (error) { - throw new Error(String(error.value ?? t('auth.failedToDeleteAccount'))); - } - - setToken(null); - setRefreshToken(null); + const { error } = await authClient.deleteUser(); + if (error) throw new Error(error.message ?? 'Delete account failed'); + userStore.set(null); await clearLocalData(); await Updates.reloadAsync(); } catch (error) { diff --git a/apps/expo/features/auth/hooks/useAuthInit.ts b/apps/expo/features/auth/hooks/useAuthInit.ts index aa790aec35..0aa8653852 100644 --- a/apps/expo/features/auth/hooks/useAuthInit.ts +++ b/apps/expo/features/auth/hooks/useAuthInit.ts @@ -1,18 +1,51 @@ +import { when } from '@legendapp/state'; import { clientEnvs } from '@packrat/env/expo-client'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { GoogleSignin } from '@react-native-google-signin/google-signin'; -import { store } from 'expo-app/atoms/store'; +import { userStore, userSyncState } from 'expo-app/features/auth/store'; +import { authClient } from 'expo-app/lib/auth-client'; import { router } from 'expo-router'; import Storage from 'expo-sqlite/kv-store'; import { useEffect, useState } from 'react'; import { Platform } from 'react-native'; -import { tokenAtom } from '../atoms/authAtoms'; -import { isAuthed } from '../store'; + +const AUTH_VERSION_KEY = 'auth_version'; +const CURRENT_AUTH_VERSION = 'v2'; + +async function runVersionGateMigration() { + const authVersion = await AsyncStorage.getItem(AUTH_VERSION_KEY); + if (authVersion === CURRENT_AUTH_VERSION) return; + + // Clear legacy integer-ID tokens from v1 auth system + await Storage.removeItem('access_token'); + await Storage.removeItem('refresh_token'); + await AsyncStorage.setItem(AUTH_VERSION_KEY, CURRENT_AUTH_VERSION); +} + +function applySessionUser(sessionUser: Record) { + userStore.set({ + id: String(sessionUser.id ?? ''), + email: String(sessionUser.email ?? ''), + firstName: String(sessionUser.name ?? '').split(' ')[0] ?? '', + lastName: + String(sessionUser.name ?? '') + .split(' ') + .slice(1) + .join(' ') ?? '', + role: (sessionUser.role as 'USER' | 'ADMIN') ?? 'USER', // safe-cast: Better Auth client type omits additionalFields; role is present at runtime + avatarUrl: (sessionUser.image as string | null) ?? null, + preferredWeightUnit: 'g', + }); +} + +function isDefinitiveAuthFailure(error: unknown): boolean { + const status = (error as { status?: number })?.status; + return status === 401 || status === 403; +} export function useAuthInit() { const [isLoading, setIsLoading] = useState(true); - // Initialize Google Sign-In useEffect(() => { GoogleSignin.configure({ webClientId: clientEnvs.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID, @@ -25,37 +58,72 @@ export function useAuthInit() { }); }, []); - // Check for existing session or skipped login on app load useEffect(() => { const initializeAuth = async () => { + await runVersionGateMigration(); + + // Wait for SQLite persist to hydrate userStore before reading cached user + await when(userSyncState.isPersistLoaded); + + const cachedUser = userStore.get(); + const hasSkippedLogin = await AsyncStorage.getItem('skipped_login'); + + if (cachedUser || hasSkippedLogin === 'true') { + // Unblock UI immediately — the app is offline-first + setIsLoading(false); + + // Guests have no session to refresh; skip the check entirely + if (!cachedUser) return; + + // Refresh session in the background; only sign out on a definitive auth + // rejection (401/403), never on network failures (user may be offline) + authClient + .getSession() + .then(({ data: session, error }) => { + if (error) { + if (isDefinitiveAuthFailure(error)) { + userStore.set(null); + router.replace('/auth'); + } + return; + } + if (session?.user) { + // safe-cast: widening Better Auth User to Record for runtime additional-field access + applySessionUser(session.user as Record); + } else { + // Server confirmed the session is gone + userStore.set(null); + router.replace('/auth'); + } + }) + .catch((error) => { + if (isDefinitiveAuthFailure(error)) { + userStore.set(null); + router.replace('/auth'); + } + // Network/transient error — keep cached user silently + }); + return; + } + + // No cached user — must reach the server to establish a session try { - setIsLoading(true); - - // Check if user has skipped login before - const hasSkippedLogin = await AsyncStorage.getItem('skipped_login'); - - // Get stored token - const accessToken = await Storage.getItem('access_token'); - - // If user has session or hasSkippedLogin before, continue to app - if (accessToken || hasSkippedLogin === 'true') { - if (accessToken) { - isAuthed.set(true); - // Hydrate tokenAtom so components (e.g. AI chat) get the correct - // token without relying on the sync SQLite read (unavailable on web). - store.set(tokenAtom, accessToken); - } - setIsLoading(false); + const { data: session, error } = await authClient + .getSession() + .catch((err) => ({ data: null, error: err as unknown })); + + if (!error && session?.user) { + // safe-cast: widening Better Auth User to Record for runtime additional-field access + applySessionUser(session.user as Record); return; - } else { - // No tokens and hasn't skipped login. It's first time - show auth screen - router.replace({ - pathname: '/auth', - params: { showSkipLoginBtn: 'true', redirectTo: '/' }, - }); } + + router.replace({ + pathname: '/auth', + params: { showSkipLoginBtn: 'true', redirectTo: '/' }, + }); } catch (error) { - console.error('Failed to load user session:', error); + console.error('Failed to initialize auth:', error); router.replace('/auth'); } finally { setIsLoading(false); diff --git a/apps/expo/features/auth/store/index.ts b/apps/expo/features/auth/store/index.ts index c93a671031..e531c6b9be 100644 --- a/apps/expo/features/auth/store/index.ts +++ b/apps/expo/features/auth/store/index.ts @@ -1,6 +1,10 @@ export * from './user'; -import { observable } from '@legendapp/state'; +import { observable, observe } from '@legendapp/state'; import { userStore } from './user'; -export const isAuthed = observable(() => userStore.get() !== null); +export const isAuthed = observable(false); + +observe(() => { + isAuthed.set(userStore.get() !== null); +}); diff --git a/apps/expo/features/catalog/components/CatalogBrowserModal.tsx b/apps/expo/features/catalog/components/CatalogBrowserModal.tsx index 38a40684fe..9c8293468a 100644 --- a/apps/expo/features/catalog/components/CatalogBrowserModal.tsx +++ b/apps/expo/features/catalog/components/CatalogBrowserModal.tsx @@ -165,7 +165,7 @@ export function CatalogBrowserModal({ const { data: popularData, isLoading: isPopularLoading } = usePopularCatalogItems(8); // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema - const popularItems = (popularData?.items ?? []) as CatalogItem[]; + const popularItems = (popularData?.items ?? []) as unknown as CatalogItem[]; const { data: paginatedData, @@ -189,11 +189,9 @@ export function CatalogBrowserModal({ } = useVectorSearch({ query: debouncedSearchValue, limit: 20 }); // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema - const items = ( - isSearching - ? searchResult?.items || [] - : paginatedData?.pages.flatMap((page) => page.items) || [] - ) as CatalogItem[]; // safe-cast: treaty response shape matches CatalogItem[] + const items = (isSearching + ? searchResult?.items || [] + : paginatedData?.pages.flatMap((page) => page.items) || []) as unknown as CatalogItem[]; // safe-cast: treaty response shape matches CatalogItem[] const isLoading = isSearching ? isSearchLoading : isPaginatedLoading; const error = isSearching ? searchError : paginatedError; diff --git a/apps/expo/features/catalog/lib/normalizeDescription.ts b/apps/expo/features/catalog/lib/normalizeDescription.ts index bfeed22434..9a6d68d035 100644 --- a/apps/expo/features/catalog/lib/normalizeDescription.ts +++ b/apps/expo/features/catalog/lib/normalizeDescription.ts @@ -3,7 +3,7 @@ const DETAILS_ARRAY_RE = /^Details:\s*(\[[\s\S]*\])$/; export function normalizeDescription(description: string | null | undefined): string | null { if (!description) return null; const match = description.match(DETAILS_ARRAY_RE); - if (match && match[1]) { + if (match?.[1]) { try { const items = JSON.parse(match[1]) as string[]; return items.join('. '); diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index 9f011afe5d..88c53fb2ef 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -52,7 +52,6 @@ function CatalogItemsScreen() { const { data: paginatedData, isLoading: isPaginatedLoading, - isRefetching, refetch, fetchNextPage, hasNextPage, @@ -74,8 +73,8 @@ function CatalogItemsScreen() { const paginatedItems: CatalogItem[] = // safe-cast: treaty response shape matches CatalogItem[] as validated by the API schema - ((paginatedData?.pages.flatMap((page) => page.items) ?? []) as CatalogItem[]).filter((item) => - Boolean(item?.id), + ((paginatedData?.pages.flatMap((page) => page.items) ?? []) as unknown as CatalogItem[]).filter( + (item) => Boolean(item?.id), ); const totalItems = paginatedData?.pages[0]?.totalCount ?? 0; diff --git a/apps/expo/features/feed/components/CommentItem.tsx b/apps/expo/features/feed/components/CommentItem.tsx index 44aab46fa7..b3f29b8629 100644 --- a/apps/expo/features/feed/components/CommentItem.tsx +++ b/apps/expo/features/feed/components/CommentItem.tsx @@ -9,7 +9,7 @@ interface CommentItemProps { comment: Comment; onLike: (commentId: number) => void; onDelete?: (commentId: number) => void; - currentUserId?: number; + currentUserId?: string; } export const CommentItem: React.FC = ({ diff --git a/apps/expo/features/feed/components/PostCard.tsx b/apps/expo/features/feed/components/PostCard.tsx index 66dc234f8f..52388de2ee 100644 --- a/apps/expo/features/feed/components/PostCard.tsx +++ b/apps/expo/features/feed/components/PostCard.tsx @@ -21,7 +21,7 @@ interface PostCardProps { post: Post; onLike: (postId: number) => void; onDelete?: (postId: number) => void; - currentUserId?: number; + currentUserId?: string; } export const PostCard: React.FC = ({ post, onLike, onDelete, currentUserId }) => { diff --git a/apps/expo/features/feed/screens/FeedScreen.tsx b/apps/expo/features/feed/screens/FeedScreen.tsx index 43067d4019..1ddf4fffb7 100644 --- a/apps/expo/features/feed/screens/FeedScreen.tsx +++ b/apps/expo/features/feed/screens/FeedScreen.tsx @@ -14,7 +14,7 @@ export const FeedScreen = () => { const { t } = useTranslation(); const { colors } = useColorScheme(); const router = useRouter(); - const currentUserId = userStore.id.peek() as number | undefined; + const currentUserId = userStore.id.peek() as string | undefined; const { data, isLoading, isRefetching, refetch, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed(); diff --git a/apps/expo/features/feed/screens/PostDetailScreen.tsx b/apps/expo/features/feed/screens/PostDetailScreen.tsx index 890aa55f62..5d74b81ed4 100644 --- a/apps/expo/features/feed/screens/PostDetailScreen.tsx +++ b/apps/expo/features/feed/screens/PostDetailScreen.tsx @@ -29,7 +29,7 @@ import { buildPostImageUrl } from '../utils'; interface PostDetailScreenProps { post: Post; - currentUserId?: number; + currentUserId?: string; } export const PostDetailScreen = ({ post, currentUserId }: PostDetailScreenProps) => { diff --git a/apps/expo/features/feed/types.ts b/apps/expo/features/feed/types.ts index a1cb5cbff6..11a2323b0d 100644 --- a/apps/expo/features/feed/types.ts +++ b/apps/expo/features/feed/types.ts @@ -1,12 +1,12 @@ export interface PostAuthor { - id: number; + id: string; firstName: string | null; lastName: string | null; } export interface Post { id: number; - userId: number; + userId: string; caption: string | null; images: string[]; createdAt: string; @@ -28,7 +28,7 @@ export interface FeedResponse { export interface Comment { id: number; postId: number; - userId: number; + userId: string; content: string; parentCommentId: number | null; createdAt: string; diff --git a/apps/expo/features/feed/utils/__tests__/feedUtils.test.ts b/apps/expo/features/feed/utils/__tests__/feedUtils.test.ts index bcc717f084..2ad9cd6e15 100644 --- a/apps/expo/features/feed/utils/__tests__/feedUtils.test.ts +++ b/apps/expo/features/feed/utils/__tests__/feedUtils.test.ts @@ -19,7 +19,7 @@ import { buildPostImageUrl, formatAuthorName, formatRelativeDate } from '../inde const basePost: Post = { id: 1, - userId: 42, + userId: 'user-42', caption: 'hello', images: [], createdAt: '2024-01-01T00:00:00.000Z', @@ -32,7 +32,7 @@ const basePost: Post = { const baseComment: Comment = { id: 1, postId: 1, - userId: 42, + userId: 'user-42', content: 'nice', parentCommentId: null, createdAt: '2024-01-01T00:00:00.000Z', @@ -73,7 +73,7 @@ describe('feed/utils', () => { it('returns "first last" when both names are present', () => { const post: Post = { ...basePost, - author: { id: 1, firstName: 'Ada', lastName: 'Lovelace' }, + author: { id: 'author-1', firstName: 'Ada', lastName: 'Lovelace' }, }; expect(formatAuthorName(post)).toBe('Ada Lovelace'); }); @@ -81,7 +81,7 @@ describe('feed/utils', () => { it('returns just the first name when only firstName is present', () => { const post: Post = { ...basePost, - author: { id: 1, firstName: 'Ada', lastName: null }, + author: { id: 'author-1', firstName: 'Ada', lastName: null }, }; expect(formatAuthorName(post)).toBe('Ada'); }); @@ -89,7 +89,7 @@ describe('feed/utils', () => { it('returns just the last name when only lastName is present', () => { const post: Post = { ...basePost, - author: { id: 1, firstName: null, lastName: 'Lovelace' }, + author: { id: 'author-1', firstName: null, lastName: 'Lovelace' }, }; expect(formatAuthorName(post)).toBe('Lovelace'); }); @@ -97,7 +97,7 @@ describe('feed/utils', () => { it('returns "User" when the author exists but both names are null', () => { const post: Post = { ...basePost, - author: { id: 1, firstName: null, lastName: null }, + author: { id: 'author-1', firstName: null, lastName: null }, }; expect(formatAuthorName(post)).toBe('User'); }); @@ -105,7 +105,7 @@ describe('feed/utils', () => { it('also works for Comment entities', () => { const comment: Comment = { ...baseComment, - author: { id: 1, firstName: 'Grace', lastName: 'Hopper' }, + author: { id: 'author-1', firstName: 'Grace', lastName: 'Hopper' }, }; expect(formatAuthorName(comment)).toBe('Grace Hopper'); }); diff --git a/apps/expo/features/guides/screens/GuidesListScreen.tsx b/apps/expo/features/guides/screens/GuidesListScreen.tsx index 171a247400..9322bd8d08 100644 --- a/apps/expo/features/guides/screens/GuidesListScreen.tsx +++ b/apps/expo/features/guides/screens/GuidesListScreen.tsx @@ -31,7 +31,6 @@ export const GuidesListScreen = () => { const { data: guidesData, isLoading: isLoadingGuides, - isRefetching: isRefetchingGuides, refetch: refetchGuides, fetchNextPage: fetchNextPageGuides, hasNextPage: hasNextPageGuides, @@ -46,7 +45,6 @@ export const GuidesListScreen = () => { const { data: searchData, isLoading: isSearching, - isRefetching: isRefetchingSearch, refetch: refetchSearch, fetchNextPage: fetchNextPageSearch, hasNextPage: hasNextPageSearch, @@ -62,7 +60,6 @@ export const GuidesListScreen = () => { const isSearchMode = searchQuery.length > 0; const data = isSearchMode ? searchData : guidesData; const isLoading = isSearchMode ? isSearching : isLoadingGuides; - const isRefetching = isSearchMode ? isRefetchingSearch : isRefetchingGuides; const refetch = isSearchMode ? refetchSearch : refetchGuides; const fetchNextPage = isSearchMode ? fetchNextPageSearch : fetchNextPageGuides; const hasNextPage = isSearchMode ? hasNextPageSearch : hasNextPageGuides; diff --git a/apps/expo/features/profile/types.ts b/apps/expo/features/profile/types.ts index f487d58a38..9a8b5544d4 100644 --- a/apps/expo/features/profile/types.ts +++ b/apps/expo/features/profile/types.ts @@ -1,7 +1,7 @@ import type { WeightUnit } from '../packs/types'; export interface User { - id: number; + id: string; email: string; firstName: string; lastName: string; diff --git a/apps/expo/features/trips/types.ts b/apps/expo/features/trips/types.ts index 2d59bb5b50..98ce506161 100644 --- a/apps/expo/features/trips/types.ts +++ b/apps/expo/features/trips/types.ts @@ -17,7 +17,7 @@ export interface Trip { startDate?: string; endDate?: string; - userId?: number; + userId?: string; packId?: Pack['id']; deleted: boolean; createdAt?: string; diff --git a/apps/expo/lib/api/packrat.ts b/apps/expo/lib/api/packrat.ts index 2311830eef..05dac97f7a 100644 --- a/apps/expo/lib/api/packrat.ts +++ b/apps/expo/lib/api/packrat.ts @@ -1,38 +1,19 @@ import { createApiClient } from '@packrat/api-client'; import { clientEnvs } from '@packrat/env/expo-client'; import { store } from 'expo-app/atoms/store'; -import { - needsReauthAtom, - refreshTokenAtom, - tokenAtom, -} from 'expo-app/features/auth/atoms/authAtoms'; -import Storage from 'expo-sqlite/kv-store'; +import { needsReauthAtom } from 'expo-app/features/auth/atoms/authAtoms'; +import { authClient } from 'expo-app/lib/auth-client'; -/** - * Typed Treaty-backed PackRat API client for the Expo app. - * - * Session state (token persistence, reauth signal) lives here — the - * `@packrat/api-client` package is transport-only and accepts injected - * auth hooks so guides and landing can reuse it with their own storage - * layers. - * - * Usage: - * ```ts - * import { apiClient } from 'expo-app/lib/api/packrat'; - * const { data, error } = await apiClient.catalog.get({ query: { limit: 10 } }); - * ``` - */ export const apiClient = createApiClient({ baseUrl: clientEnvs.EXPO_PUBLIC_API_URL, auth: { - getAccessToken: () => Storage.getItem('access_token'), - getRefreshToken: () => Storage.getItem('refresh_token'), - onAccessTokenRefreshed: async (accessToken) => { - await store.set(tokenAtom, accessToken); - }, - onRefreshTokenRefreshed: async (refreshToken) => { - await store.set(refreshTokenAtom, refreshToken); + getAccessToken: async () => { + const { data } = await authClient.getSession(); + return data?.session?.token ?? null; }, + // Better Auth manages session renewal internally — no separate refresh token flow. + getRefreshToken: () => null, + onAccessTokenRefreshed: () => {}, onNeedsReauth: () => { store.set(needsReauthAtom, true); }, diff --git a/apps/expo/lib/auth-client.ts b/apps/expo/lib/auth-client.ts new file mode 100644 index 0000000000..b685923235 --- /dev/null +++ b/apps/expo/lib/auth-client.ts @@ -0,0 +1,25 @@ +import { expoClient } from '@better-auth/expo/client'; +import { clientEnvs } from '@packrat/env/expo-client'; +import { createAuthClient } from 'better-auth/react'; +import * as SecureStore from 'expo-secure-store'; + +export const authClient = createAuthClient({ + baseURL: clientEnvs.EXPO_PUBLIC_API_URL, + plugins: [ + expoClient({ + scheme: 'packrat', + storagePrefix: 'packrat', + storage: { + setItem: (key: string, value: string) => SecureStore.setItem(key, value), + getItem: (key: string) => SecureStore.getItem(key), + }, + }), + ], +}); + +export type BetterAuthSession = typeof authClient.$Infer.Session; + +export async function getActiveToken(): Promise { + const { data } = await authClient.getSession(); + return data?.session?.token ?? null; +} diff --git a/apps/expo/lib/hooks/useAuthenticatedQueryToolkit.ts b/apps/expo/lib/hooks/useAuthenticatedQueryToolkit.ts index 90d6fe0f4f..10dbdc0d29 100644 --- a/apps/expo/lib/hooks/useAuthenticatedQueryToolkit.ts +++ b/apps/expo/lib/hooks/useAuthenticatedQueryToolkit.ts @@ -1,9 +1,8 @@ -import { tokenAtom } from 'expo-app/features/auth/atoms/authAtoms'; -import { useAtomValue } from 'jotai'; +import { authClient } from 'expo-app/lib/auth-client'; export const useAuthenticatedQueryToolkit = () => { - const accessToken = useAtomValue(tokenAtom); - const isQueryEnabledWithAccessToken = Boolean(accessToken); + const { data: session } = authClient.useSession(); + const isQueryEnabledWithAccessToken = Boolean(session?.session?.token); return { isQueryEnabledWithAccessToken, diff --git a/apps/expo/lib/testIds.ts b/apps/expo/lib/testIds.ts index 55072dd94e..c359db4a50 100644 --- a/apps/expo/lib/testIds.ts +++ b/apps/expo/lib/testIds.ts @@ -32,6 +32,7 @@ export const testIds = Object.freeze({ // ── Packs ───────────────────────────────────────────────────────────────── packs: Object.freeze({ createBtn: 'create-pack-button', // keep Maestro value + cancelBtn: 'cancel-pack-form-button', // keep Maestro value nameInput: 'packs:name-input', descriptionInput: 'packs:description-input', submitBtn: 'submit-pack-button', // keep Maestro value @@ -67,6 +68,7 @@ export const testIds = Object.freeze({ // ── Trips ───────────────────────────────────────────────────────────────── trips: Object.freeze({ createBtn: 'create-trip-button', // keep Maestro value + cancelBtn: 'cancel-trip-form-button', // keep Maestro value nameInput: 'trips:name-input', descriptionInput: 'trips:description-input', submitBtn: 'submit-trip-button', // keep Maestro value diff --git a/apps/expo/package.json b/apps/expo/package.json index cbf86a55d9..6cd4510fc5 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@ai-sdk/react": "^3.0.170", + "@better-auth/expo": "^1.6.9", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^15.0.3", "@gorhom/bottom-sheet": "^5.1.2", @@ -81,6 +82,7 @@ "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.70.0", "ai": "catalog:", + "better-auth": "^1.6.9", "burnt": "^0.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -103,6 +105,7 @@ "expo-localization": "~55.0.13", "expo-location": "~55.1.8", "expo-navigation-bar": "~55.0.12", + "expo-network": "~55.0.13", "expo-router": "~55.0.13", "expo-secure-store": "~55.0.13", "expo-sqlite": "~55.0.15", diff --git a/apps/expo/providers/TanstackProvider.tsx b/apps/expo/providers/TanstackProvider.tsx index ea539738d5..acd19b334d 100644 --- a/apps/expo/providers/TanstackProvider.tsx +++ b/apps/expo/providers/TanstackProvider.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type React from 'react'; // Create a client -const queryClient = new QueryClient(); +export const queryClient = new QueryClient(); export function TanstackProvider({ children }: { children: React.ReactNode }) { return {children}; diff --git a/bun.lock b/bun.lock index 6212bb9517..9c06e72364 100644 --- a/bun.lock +++ b/bun.lock @@ -68,6 +68,7 @@ "version": "2.0.24", "dependencies": { "@ai-sdk/react": "^3.0.170", + "@better-auth/expo": "^1.6.9", "@expo/react-native-action-sheet": "^4.1.1", "@expo/vector-icons": "^15.0.3", "@gorhom/bottom-sheet": "^5.1.2", @@ -103,6 +104,7 @@ "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.70.0", "ai": "catalog:", + "better-auth": "^1.6.9", "burnt": "^0.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -125,6 +127,7 @@ "expo-localization": "~55.0.13", "expo-location": "~55.1.8", "expo-navigation-bar": "~55.0.12", + "expo-network": "~55.0.13", "expo-router": "~55.0.13", "expo-secure-store": "~55.0.13", "expo-sqlite": "~55.0.15", @@ -350,7 +353,7 @@ "@packrat/env": "workspace:*", "@packrat/guards": "workspace:*", "consola": "^3.4.2", - "magic-regexp": "^0.11.0", + "magic-regexp": "catalog:", "radash": "catalog:", "zod": "catalog:", }, @@ -401,12 +404,15 @@ "zod-openapi": "^5.4.6", }, "devDependencies": { + "@better-auth/drizzle-adapter": "^1.6.9", "@cloudflare/vitest-pool-workers": "0.8.71", "@cloudflare/workers-types": "^4.20250405.0", "@types/bun": "latest", "@types/pg": "^8.11.15", "@types/ws": "^8.5.14", "@vitest/coverage-v8": "~3.1.4", + "better-auth": "^1.6.9", + "better-auth-cloudflare": "^0.3.0", "concurrently": "^8.2.2", "drizzle-orm": "^0.45.2", "typed-htmx": "^0.3.1", @@ -484,9 +490,11 @@ "name": "@packrat/mcp", "version": "2.0.24", "dependencies": { + "@cloudflare/workers-oauth-provider": "^0.4.0", "@modelcontextprotocol/sdk": "^1.11.0", "@packrat/api-client": "workspace:*", "agents": "^0.11.0", + "magic-regexp": "catalog:", "zod": "catalog:", }, "devDependencies": { @@ -647,6 +655,7 @@ "chalk": "^5.6.2", "elysia": "^1.4.0", "hono": "^4.10.7", + "magic-regexp": "^0.11.0", "radash": "^12.1.1", "react": "19.2.0", "react-dom": "19.2.0", @@ -657,19 +666,19 @@ "zod": "^3.24.2", }, "packages": { - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hOtwiM6E/m1PgqHnx0/grvh0P/kjENHsN222OL5iYPQwfWmcX+26oFXM7HGL/UzonB+RORMkYAbskgAiANVwCQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qeq+SidYtzMrcf0fdw3L0QLmtXK+ErwdBzbxS4+0Q/2UP85Ges8RJJcbAj7SO8e2JbeJoM35BLqkeNy1o3wJvQ=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.57", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2kfzDQYYz0m/yRF4bre9s2j8wNiPdhfHzYZc3dxAOMlprvGmRZOvoZBx8chlJIFwL7xR2EFkvLagBvo2llCXPw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], - "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.32", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5kPyfDOHL72Mnz0unBiW3S/jHjNMo/frPW6dBTADX1SMbFB9Yvks4k1pjixIJc1m8YBulI5hV5yTvr7uUxpzxA=="], + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9UfV7ywpnxNLPI/hdheFPHXDdLG9vLqNoPSdRTPV+nPAX117zMtBmqD5KSvmXTjeF7IXpObUZ9bWzwMR/ewL1g=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], - "@ai-sdk/react": ["@ai-sdk/react@3.0.175", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.26", "ai": "6.0.173", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-1yweoUAecYfUmbV5o4bt8xhtw97yxkQgBnOIjER3BMmtBzhjuSPfqPShbttCo9d5ErGe1TdYnk5h9RdVn9l5Dw=="], + "@ai-sdk/react": ["@ai-sdk/react@3.0.170", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.23", "ai": "6.0.168", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-YUDn+mK0c8iUz14rCBf1A0zg6SV5b5aSVUz+azF1bdBd1SFXVI19dKYR+PQSpZY+0+z+zs252AAsacUqiO98Kw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -757,7 +766,7 @@ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], @@ -767,7 +776,7 @@ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.29.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], @@ -801,7 +810,7 @@ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="], @@ -943,6 +952,26 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@better-auth/core": ["@better-auth/core@1.6.9", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="], + + "@better-auth/expo": ["@better-auth/expo@1.6.9", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "better-call": "1.3.5", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.9", "better-auth": "^1.6.9", "expo-constants": ">=17.0.0", "expo-linking": ">=7.0.0", "expo-network": ">=8.0.7", "expo-web-browser": ">=14.0.0" }, "optionalPeers": ["expo-constants", "expo-linking", "expo-network", "expo-web-browser"] }, "sha512-ch8DRTAWvnn4k1mFvLxi5h9+pg/KWYXJEZ2XANXtdPp84H3qA0mxzFQ//OIuKxm0Ohuubc7WNQD1IsVlKP4/ew=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0" } }, "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], @@ -967,23 +996,25 @@ "@cloudflare/containers": ["@cloudflare/containers@0.0.30", "", {}, "sha512-i148xBgmyn/pje82ZIyuTr/Ae0BT/YWwa1/GTJcw6DxEjUHAzZLaBCiX446U9OeuJ2rBh/L/9FIzxX5iYNt1AQ=="], - "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.8.71", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^5.3.2", "miniflare": "4.20250906.0", "semver": "^7.7.1", "wrangler": "4.35.0", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-keu2HCLQfRNwbmLBCDXJgCFpANTaYnQpE01fBOo4CNwiWHUT7SZGN7w64RKiSWRHyYppStXBuE5Ng7F42+flpg=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260430.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ADohZUHf7NBvPp2PdZig2Opxx+hDkk3ve7jrTne3JRx9kDSB73zc4LzcEeEN8LKkbAcqZmvfRJfpChSlusu0lA=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260424.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-yFR1XaJbSDLg/qbwtrYaU2xwFXatIPKR5nrMQCN1q/m6+Qe/j6r+kCnFEvOJjMZOm9iCKsE6Qly5clgl4u32qw=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260430.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/DoYC/1wHs+YRZzzqSQg1/EHB4hiv1yV5U8FnmapRRIzVaPtnt+ApeOXeMrIdKidgKOI8TqQzgBU8xbIM7Cl4Q=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260424.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LqWKcE7x/9KyC2iQvKPeb20hKST3dYXDZlYTvFymgR1DfLS0OFOCzVGTloVNd7WqvK4SkdzBYfxo7QMIAeBK0w=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260430.1", "", { "os": "linux", "cpu": "x64" }, "sha512-koJhBWvEVZPKCVFtMLp2iMHlYr+lFCF47wGbnlKdHVlemV0zTxJEyHI8aLlrhPLhBmOmYLp46rXw09/qJkRIhQ=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260424.1", "", { "os": "linux", "cpu": "x64" }, "sha512-YlEBFbAYZHe/ylzl8WEYQEU/jr+0XMqXaST2oBk5oVjksdb1NGuJaggluCdZAzuJJ8UqdTmyhY5u/qrasbiFWA=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260430.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hMdapNAzNQZDXGGkg4Slydc3fRJP5FUZLJVVcZCW/+imhhJro9Z1rv5n/wfR+txKoSWhTYR8eOp8Pyi2bzLzlw=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260424.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qJ0X0m6cL8fWDUPDg8K4IxYZXNJI6XbeOihqjnqKbAClrjdPDn8VUSd+z2XiCQ5NylMtMrpa/skC9UfaR6mh8g=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260430.1", "", { "os": "win32", "cpu": "x64" }, "sha512-jS3ffixjb5USOwz4frw4WzCz0HrjVxkgyU3WiYb06N7hBAfN6eOrveAJ4QRef0+suK4V1vQFoB1oKdRBsXe9Dw=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260424.1", "", { "os": "win32", "cpu": "x64" }, "sha512-tZ7Z9qmYNAP6z1/+8r/zKbk8F8DZmpmwNzMeN+zkde2Wnhfr3FBqOkJXT/5zmli8HPoWrIXxSiyqcNDMy8V2Zg=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260501.1", "", {}, "sha512-B/VX2w3my/sCqxKyWOX7SxUpFC1uD8Gh7I2zbI1d3zA8p7Tx03AFsnuEx8lYLmcd8yONAA93YsAZb1wAaLK83w=="], + "@cloudflare/workers-oauth-provider": ["@cloudflare/workers-oauth-provider@0.4.0", "", {}, "sha512-UtbV8hjC2NloB+Ds6J6v/9HiG8rx8MbdeYGCyFwOACT5vANWzDL6SKo3W5UZymsXiameAgC7jAmtUx4cc+Qpaw=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260425.1", "", {}, "sha512-f6dlo3SsA+TNqjveavPDN73nxRfCOOd0iMdf8iEosgR/RJtQlrGwfr5L5Vf7x/5cpeeguxScKevuaMmdjpOECw=="], "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], @@ -1095,7 +1126,7 @@ "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.34", "", {}, "sha512-PdwETUhvu1gHF1e8eIyEHnBJLq/dRNoTrT5yhsGUfGyRxH5pbm54dF3+QPknxwMKj0M1trN7PSelYz+yzlt3lA=="], - "@expo/cli": ["@expo/cli@55.0.27", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.15", "@expo/config-plugins": "~55.0.8", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.13", "@expo/json-file": "^10.0.13", "@expo/log-box": "55.0.11", "@expo/metro": "~55.1.1", "@expo/metro-config": "~55.0.18", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.4", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.16", "@expo/require-utils": "^55.0.4", "@expo/router-server": "^55.0.15", "@expo/schema-utils": "^55.0.3", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.6", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^55.0.8", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-FF/qWyHikqvVd5GBDiLII2PRgToNGz5MjxHw76a7aufbe5kCRpAqAy7HRoio1PlF5g9UIYnFjs333a3fWTlgMw=="], + "@expo/cli": ["@expo/cli@55.0.26", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.15", "@expo/config-plugins": "~55.0.8", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.13", "@expo/json-file": "^10.0.13", "@expo/log-box": "55.0.11", "@expo/metro": "~55.1.0", "@expo/metro-config": "~55.0.17", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.4", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.16", "@expo/require-utils": "^55.0.4", "@expo/router-server": "^55.0.15", "@expo/schema-utils": "^55.0.3", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.6", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^55.0.8", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-Ud9gpeGMF5RIL42LXvCw3k3mWK8rf/P2wu+Yrzz9Do1kcFKZeT9Vy2D/xukjdr/Xw+ELba87ThOot17GsPiWjw=="], "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="], @@ -1123,9 +1154,9 @@ "@expo/log-box": ["@expo/log-box@55.0.11", "", { "dependencies": { "@expo/dom-webview": "^55.0.5", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-JQHFLWkskIbJi6cxYMjErx8lQqfFJilDQLKmdTO3m3YkdmN9GE/CrzjOfVlCG0DGEGZJ90br0pGKvGPdXNsHKw=="], - "@expo/metro": ["@expo/metro@55.1.1", "", { "dependencies": { "metro": "0.83.7", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-config": "0.83.7", "metro-core": "0.83.7", "metro-file-map": "0.83.7", "metro-minify-terser": "0.83.7", "metro-resolver": "0.83.7", "metro-runtime": "0.83.7", "metro-source-map": "0.83.7", "metro-symbolicate": "0.83.7", "metro-transform-plugins": "0.83.7", "metro-transform-worker": "0.83.7" } }, "sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg=="], + "@expo/metro": ["@expo/metro@55.1.0", "", { "dependencies": { "metro": "0.83.6", "metro-babel-transformer": "0.83.6", "metro-cache": "0.83.6", "metro-cache-key": "0.83.6", "metro-config": "0.83.6", "metro-core": "0.83.6", "metro-file-map": "0.83.6", "metro-minify-terser": "0.83.6", "metro-resolver": "0.83.6", "metro-runtime": "0.83.6", "metro-source-map": "0.83.6", "metro-symbolicate": "0.83.6", "metro-transform-plugins": "0.83.6", "metro-transform-worker": "0.83.6" } }, "sha512-bb/LOncsz9KiP6cHmMy0MCDG1COZOn+k+pRpDrvJUmxLdOOuniJSYyCc/Dgv1bR9E/6YR+fh3EXGg9MUrVNy4Q=="], - "@expo/metro-config": ["@expo/metro-config@55.0.18", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~55.0.15", "@expo/env": "~2.1.1", "@expo/json-file": "~10.0.13", "@expo/metro": "~55.1.1", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.32.0", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-XT7YHdQsUjdun0n4cyE5iXIyncDERIG4WesMBRUClr1RAurPwyg95BrbOBpq3E3uvwSBrAGfhu1w8VADqP4ZTQ=="], + "@expo/metro-config": ["@expo/metro-config@55.0.17", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~55.0.15", "@expo/env": "~2.1.1", "@expo/json-file": "~10.0.13", "@expo/metro": "~55.1.0", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.32.0", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "~8.4.32", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-o11VyNoRDXv0T5320D9cH+nSsrR/OMHTjtysKLIfDlidsBswDk1DMApPv9Kw0/gluArCSnbx8JC1G0Yh2Y4P3g=="], "@expo/metro-runtime": ["@expo/metro-runtime@55.0.10", "", { "dependencies": { "@expo/log-box": "55.0.11", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-7v+ldTvMWRa1ml83Jel9W2f8qT/NZZWrlHaEjf29nb72JTEO50+Xac9PWLo+X3LCDAAuyYuBGKYXOJwfqxV0fQ=="], @@ -1175,7 +1206,7 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], - "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.13", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "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-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-cMxyd9kIowMME9kw2wwXAuWrXUQnPkJQz7rDbOSBBomZ+PpV/C/tlO1UozBrAe2zs3tp9th3JMW21FI/y0VeuQ=="], + "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.10", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "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-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-MnFddmVOlaoash0d9g1ClqFqX+32h/sV3PNEFz9A8XCvUbZGQM9OG6HHAzTb+eQfUGA8DkaurI+wfpNFyzj5Yw=="], "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], @@ -1271,7 +1302,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@legendapp/state": ["@legendapp/state@3.0.0-beta.47", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "expo-sqlite": "^15.0.0" }, "optionalPeers": ["expo-sqlite"] }, "sha512-MPgPacXXSoAazAv7ulW/o0ZAtK4YHk3twvXZ241l2HqAHciHozb7tg5SMbEAc2HKUUfC3JBh+9+DXfMsYokLpQ=="], + "@legendapp/state": ["@legendapp/state@3.0.0-beta.46", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "expo-sqlite": "^15.0.0" }, "optionalPeers": ["expo-sqlite"] }, "sha512-TcCabsE9jPW2r0sKQbUet46L0hbWiupKoun9UUkcHyF/6Jec1RyJCmLrdgFPnYZ9HwupJKIRxJVlxNrg2tG3SQ=="], "@lhci/cli": ["@lhci/cli@0.14.0", "", { "dependencies": { "@lhci/utils": "0.14.0", "chrome-launcher": "^0.13.4", "compression": "^1.7.4", "debug": "^4.3.1", "express": "^4.17.1", "inquirer": "^6.3.1", "isomorphic-fetch": "^3.0.0", "lighthouse": "12.1.0", "lighthouse-logger": "1.2.0", "open": "^7.1.0", "proxy-agent": "^6.4.0", "tmp": "^0.1.0", "uuid": "^8.3.1", "yargs": "^15.4.1", "yargs-parser": "^13.1.2" }, "bin": { "lhci": "./src/cli.js" } }, "sha512-TxOH9pFBnmmN7Jmo2Aimxx5UhE8veqXpHfFJDMWsCVxkwh7mGxcAWchGl84mK139SZbbRmerqZ72c+h2nG9/QQ=="], @@ -1311,6 +1342,10 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.15", "", { "os": "win32", "cpu": "x64" }, "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA=="], + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1319,7 +1354,9 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "@packrat-ai/nativewindui": ["@packrat-ai/nativewindui@2.0.5", "https://npm.pkg.github.com/download/@packrat-ai/nativewindui/2.0.5/750854c71bd0e25b0c3a62bb925ef4134735fc18", { "peerDependencies": { "@expo/vector-icons": ">=15.0.0", "@gorhom/bottom-sheet": "^5.1.2", "@react-native-community/datetimepicker": "^8.4.0", "@react-native-community/slider": "^5.0.0", "@react-native-picker/picker": "^2.11.0", "@react-native-segmented-control/segmented-control": "^2.5.0", "@react-navigation/drawer": "^7.1.1", "@react-navigation/elements": "^2.3.1", "@react-navigation/native": "^7.0.14", "@rn-primitives/alert-dialog": "^1.1.0", "@rn-primitives/avatar": "^1.0.4", "@rn-primitives/checkbox": "^1.1.0", "@rn-primitives/context-menu": "^1.1.0", "@rn-primitives/dropdown-menu": "^1.1.0", "@rn-primitives/hooks": "^1.1.0", "@rn-primitives/portal": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@shopify/flash-list": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo-blur": "~55.0.0", "expo-device": "~55.0.0", "expo-glass-effect": "~55.0.0", "expo-haptics": "~55.0.0", "expo-image": "~55.0.0", "expo-linear-gradient": "~55.0.0", "expo-navigation-bar": "~55.0.0", "expo-router": "~55.0.0", "expo-symbols": "~55.0.0", "nativewind": "^4.2.3", "react": ">=19.2.0", "react-native": ">=0.83.0", "react-native-keyboard-controller": "^1.21.0", "react-native-reanimated": ">=4.2.0", "react-native-safe-area-context": ">=5.6.0", "react-native-screens": ">=4.23.0", "react-native-uitextview": "^1.1.4", "rn-icon-mapper": "^0.0.1", "tailwind-merge": "^2.2.1" } }, "sha512-Q/pw2+zLeUGzzUUgSDKk3qam9u2iN8Q/xlLqYJHrA+eHLdh7MbvTwvM1ETcmNwjUdpXNyNjewshDGpT73VKCng=="], @@ -1523,7 +1560,7 @@ "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.6", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-gNSFXeb4P7qHtauLvl+zESroULIyX6Ltpvau3dhwy/QmfanBv0KUcrIU/7aVXxtWcXgp+54oWJyu2LIrsZ9+LQ=="], - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.15.11", "", { "dependencies": { "@react-navigation/elements": "^2.9.15", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-+WtNbd6fJgbViDNjmBUUP7eTgGH+zBtrl3jHuNnfUfXTs9YGuI5q3SiHIc9a5gY3voBOxbOXEiHJyW4xea7nAw=="], + "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.15.10", "", { "dependencies": { "@react-navigation/elements": "^2.9.15", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-Ao/yYlrpr0cwYYGxt9FDMQk+tTSHNm4WTaszyhroINLdoEMuKH19k1tGFdYbRBKHJx1UIH8kD+EZTYW1w6LL3Q=="], "@react-navigation/core": ["@react-navigation/core@7.17.2", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA=="], @@ -1559,39 +1596,39 @@ "@rn-primitives/utils": ["@rn-primitives/utils@1.4.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-nMFZ99AGKakMRDAlfbsYUfqwKO0LItWtp58YTwxmNuGVhXG43/zIfyWWaB3FJeOL+hhcpUn0YR7C1Vsrg0FgvQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], "@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.3", "", { "dependencies": { "picomatch": "^4.0.4" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], @@ -1741,7 +1778,7 @@ "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.32", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/middleware-serde": "^4.2.20", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.7", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/service-error-classification": "^4.3.1", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.5", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/service-error-classification": "^4.3.0", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.4", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.20", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ=="], @@ -1759,7 +1796,7 @@ "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.3.1", "", { "dependencies": { "@smithy/types": "^4.14.1" } }, "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.3.0", "", { "dependencies": { "@smithy/types": "^4.14.1" } }, "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A=="], "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], @@ -1791,7 +1828,7 @@ "@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], - "@smithy/util-retry": ["@smithy/util-retry@4.3.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew=="], + "@smithy/util-retry": ["@smithy/util-retry@4.3.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.3.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A=="], "@smithy/util-stream": ["@smithy/util-stream@4.5.25", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA=="], @@ -1799,7 +1836,7 @@ "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.3.0", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.16", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ=="], "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], @@ -1825,15 +1862,15 @@ "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - "@tanstack/query-core": ["@tanstack/query-core@5.100.7", "", {}, "sha512-5R7i6ENJLhVeeJrrUz7jKBXUXv/BJrxf9FQJSkR13bPrb3zOcE8A0Z0PxYCcsKPOsiIlTibrBL/zZbtUO1TFyQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.100.1", "", {}, "sha512-awvQhOO/2TrSCHE5LKKsXcvvj6WSBncwEcMFCB/ez0Qs0b17iyyivoGArNV3HFfXryZwCpnb/olsaBBKrIbtSw=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.7", "", {}, "sha512-bgn0QV1kEGgHHRDaX6JHb+m7KHZ0r/TIyjARYhGhG/PBpbzR74qwpRhdjW+45CtwNseSU4HcqWJVDw1VozCU6w=="], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.1", "", {}, "sha512-jZLV2l7XjYxXCrXHj9pj15gZuY8Te+idoSPS2hIh3+SxOd20Gn0rfUoqEw9vc+us/b16hi0/DWqpzx9O1ZsyIQ=="], "@tanstack/react-form": ["@tanstack/react-form@1.29.1", "", { "dependencies": { "@tanstack/form-core": "1.29.1", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-hVHk4g0phd0HxRsv2ry6Xt8BqmalT55Q3cokhJBCC1St0hcGZhgwJJbohm9atao45BPG9e55DGvtbwExqZe35g=="], - "@tanstack/react-query": ["@tanstack/react-query@5.100.7", "", { "dependencies": { "@tanstack/query-core": "5.100.7" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-LoISYWz8dOOuQbeIctF8K6yi42TWtR1WPGpwGuRUpF3u79JVVIg/PVR0MQdIA0VSHqD/ydf/b7PhKTkg3I4fLQ=="], + "@tanstack/react-query": ["@tanstack/react-query@5.100.1", "", { "dependencies": { "@tanstack/query-core": "5.100.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-UgWRLhQKprC37SsO6y1zRabOqDmM2gsdTNPbqTT35yl7kOOhwXU4nyfOiGHXPwoEFJV1IpSk85hjIFjNFWVpzw=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.7", "", { "dependencies": { "@tanstack/query-devtools": "5.100.7" }, "peerDependencies": { "@tanstack/react-query": "^5.100.7", "react": "^18 || ^19" } }, "sha512-zBaluzWz1mPAe+bxk2M08wqWAbvyb8d+q8LecQ4TQYYgfDLHBzjAk8GvEP2Mr3sbMdUniC0p88D/Pol0aoKAKA=="], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.1", "", { "dependencies": { "@tanstack/query-devtools": "5.100.1" }, "peerDependencies": { "@tanstack/react-query": "^5.100.1", "react": "^18 || ^19" } }, "sha512-JuLinBUl/BlZhm0WVX83fJgE2a3YSbuEdxf3fgP+THg92hX7YfwuH5DzT35a6sL/rifZsPr0yJ9itB6jDOcdRg=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], @@ -1943,11 +1980,11 @@ "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.1", "@typescript-eslint/types": "^8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.0", "@typescript-eslint/types": "^8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA=="], @@ -1993,9 +2030,9 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "agents": ["agents@0.11.9", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.29.0", "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.29.0", "@rolldown/plugin-babel": "^0.2.3", "cron-schedule": "^6.0.0", "mimetext": "^3.0.28", "nanoid": "^5.1.9", "partyserver": "^0.5.5", "partysocket": "1.1.18", "yargs": "^18.0.0" }, "peerDependencies": { "@cloudflare/ai-chat": ">=0.5.2 <1.0.0", "@cloudflare/codemode": ">=0.3.4 <1.0.0", "@tanstack/ai": ">=0.10.2 <1.0.0", "@x402/core": "^2.0.0", "@x402/evm": "^2.0.0", "ai": "^6.0.0", "react": "^19.0.0", "vite": ">=6.0.0 <9.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@cloudflare/ai-chat", "@cloudflare/codemode", "@tanstack/ai", "@x402/core", "@x402/evm", "vite"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-La8kXl/zEr9tu17Xc5BXb5Xz5yfrH+Oh98nnWtj1OxteO1AB0i2R26w77pXCT0ffViLaE3RtgN2dOq8QGDTwsA=="], + "agents": ["agents@0.11.5", "", { "dependencies": { "@babel/plugin-proposal-decorators": "^7.29.0", "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/sdk": "1.29.0", "@rolldown/plugin-babel": "^0.2.3", "cron-schedule": "^6.0.0", "mimetext": "^3.0.28", "nanoid": "^5.1.9", "partyserver": "^0.4.1", "partysocket": "1.1.18", "yargs": "^18.0.0" }, "peerDependencies": { "@cloudflare/ai-chat": ">=0.0.8 <1.0.0", "@cloudflare/codemode": ">=0.0.7 <1.0.0", "@tanstack/ai": ">=0.10.2 <1.0.0", "@x402/core": "^2.0.0", "@x402/evm": "^2.0.0", "ai": "^6.0.0", "react": "^19.0.0", "vite": ">=6.0.0 <9.0.0", "zod": "^4.0.0" }, "optionalPeers": ["@cloudflare/ai-chat", "@cloudflare/codemode", "@tanstack/ai", "@x402/core", "@x402/evm", "vite"], "bin": { "agents": "dist/cli/index.js" } }, "sha512-1wPkA7OOfEdR4GKwaBmqdnZkOxutN2mCsolVU4ekg5QxrTLnC9Vz9LyZPcGqV2ldyfpUY7R73AUqtig5iYRLvQ=="], - "ai": ["ai@6.0.173", "", { "dependencies": { "@ai-sdk/gateway": "3.0.108", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-X2noFb82kouYQ4nlKGm1cR1Wvlp4Xit7fjJzwXv/msKQmtqkQn9NiXH6kCX4Gl4G+9DRWsJHIKANtWk8Ix1nTw=="], + "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -2085,7 +2122,7 @@ "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - "babel-preset-expo": ["babel-preset-expo@55.0.19", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.6", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.15", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-IaxT7xremfrW2HqtG7gWI7TUSJke/V+zDW1whLpmO06ZdKOfB5Qup7oICqBWqfbcBW3h57llWOMAn1cycvbsgQ=="], + "babel-preset-expo": ["babel-preset-expo@55.0.18", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.6", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.14", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-zmDwKxCFBTe4e/jQXuITRUZlbl8HTZOhsUlwcHGjwEUB0lKQfRdaSYXZckQ+jMOBC34MrOl3Cs7/6F6vNbj5Pw=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -2103,18 +2140,24 @@ "bare-stream": ["bare-stream@2.13.1", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow=="], - "bare-url": ["bare-url@2.4.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A=="], + "bare-url": ["bare-url@2.4.3", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="], "base-64": ["base-64@0.1.0", "", {}, "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA=="], "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + "better-auth": ["better-auth@1.6.9", "", { "dependencies": { "@better-auth/core": "1.6.9", "@better-auth/drizzle-adapter": "1.6.9", "@better-auth/kysely-adapter": "1.6.9", "@better-auth/memory-adapter": "1.6.9", "@better-auth/mongo-adapter": "1.6.9", "@better-auth/prisma-adapter": "1.6.9", "@better-auth/telemetry": "1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA=="], + + "better-auth-cloudflare": ["better-auth-cloudflare@0.3.0", "", { "dependencies": { "drizzle-orm": "^0.45.0", "mime": "^4.1.0", "zod": "^4.3.0" }, "peerDependencies": { "@better-auth/drizzle-adapter": "^1.5.0", "@cloudflare/workers-types": "^4.0.0", "better-auth": "^1.5.0" } }, "sha512-u0TrMbFhHNL2IFzkCbCQYyA/beeBSivdL+vfrNywYnsVrQO1qT5CC/yKhnRdrkwXLeNi9tCeoSwWoygTMSl0Yg=="], + + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], @@ -2177,7 +2220,7 @@ "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001790", "", {}, "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2385,7 +2428,7 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "devalue": ["devalue@5.8.0", "", {}, "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg=="], + "devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -2425,7 +2468,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.348", "", {}, "sha512-QC2X59nRlycQQMc4ZXjSVBX+tSgJfgRtcrYHbIZLgOV2dCvefoQGegLR7lLXKgpPpSuVmJU19LMzGrSa2C7k3Q=="], + "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], @@ -2471,7 +2514,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], + "es-toolkit": ["es-toolkit@1.46.0", "", {}, "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -2551,7 +2594,7 @@ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - "expo": ["expo@55.0.19", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.27", "@expo/config": "~55.0.15", "@expo/config-plugins": "~55.0.8", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.6", "@expo/local-build-cache-provider": "55.0.11", "@expo/log-box": "55.0.11", "@expo/metro": "~55.1.1", "@expo/metro-config": "55.0.18", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.19", "expo-asset": "~55.0.16", "expo-constants": "~55.0.15", "expo-file-system": "~55.0.17", "expo-font": "~55.0.6", "expo-keep-awake": "~55.0.7", "expo-modules-autolinking": "55.0.19", "expo-modules-core": "55.0.24", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-8nTbChg2vy7aNsX5F7KiSb552YP7dc4eD89+UjCKlFPQg4Dw7RyjYuXgFBU7ADw2JjTHl848jFLyT6nvqNROgg=="], + "expo": ["expo@55.0.18", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.26", "@expo/config": "~55.0.15", "@expo/config-plugins": "~55.0.8", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.6", "@expo/local-build-cache-provider": "55.0.11", "@expo/log-box": "55.0.11", "@expo/metro": "~55.1.0", "@expo/metro-config": "55.0.17", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.18", "expo-asset": "~55.0.16", "expo-constants": "~55.0.15", "expo-file-system": "~55.0.17", "expo-font": "~55.0.6", "expo-keep-awake": "~55.0.6", "expo-modules-autolinking": "55.0.19", "expo-modules-core": "55.0.23", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-J3LVgN8ygERC0pmSjXfW2W/jlT18+VBek6vB9DBJiCNyrGKpSE4Kv9BH7VooiIMEizwwzsgDgXbDRWBS14IaKA=="], "expo-apple-authentication": ["expo-apple-authentication@55.0.13", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Qvh3DmhXqhtWOe7BC9e7UVApR3XS1qE7+68tVLqb3KI/sET7QV9KT5JgOJogWmmCJVxA/kaot0M136yvW1pdWA=="], @@ -2563,11 +2606,11 @@ "expo-constants": ["expo-constants@55.0.15", "", { "dependencies": { "@expo/env": "~2.1.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-w394fcZLJjeKN+9ZnJzL/HiarE1nwZFDa+3S9frevh6Ur+MAAs9QDrcXhDrV8T3xqRzzYaqsP6Z8TFZ4efWN1A=="], - "expo-dev-client": ["expo-dev-client@55.0.30", "", { "dependencies": { "expo-dev-launcher": "55.0.31", "expo-dev-menu": "55.0.26", "expo-dev-menu-interface": "55.0.2", "expo-manifests": "~55.0.16", "expo-updates-interface": "~55.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-guDTu5MsI7C5TWi4d6PwG6CsfPeEDVu9V0eljwOLUC96MAwzc0Kw9/IgqGywrom5zBk8JCXv1dAZbUO+Ik83MQ=="], + "expo-dev-client": ["expo-dev-client@55.0.28", "", { "dependencies": { "expo-dev-launcher": "55.0.29", "expo-dev-menu": "55.0.24", "expo-dev-menu-interface": "55.0.2", "expo-manifests": "~55.0.16", "expo-updates-interface": "~55.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-QZK6Ylx8Jg7lhOOHCxwC10g+i34ggMBAqV497JXFqla1tuuYiEw1poNJS5pD/60ZLe8kyy5PYPB4E9ezDHA9yQ=="], - "expo-dev-launcher": ["expo-dev-launcher@55.0.31", "", { "dependencies": { "@expo/schema-utils": "^55.0.3", "expo-dev-menu": "55.0.26", "expo-manifests": "~55.0.16" }, "peerDependencies": { "expo": "*" } }, "sha512-jCWpW8+hzyv7xI4fIx+bGg84PQGzohNnBXdyazrU0J0BZCseguNCaxuU9LTcNTP/PGMJxZFEUFmU/Siojtdl/w=="], + "expo-dev-launcher": ["expo-dev-launcher@55.0.29", "", { "dependencies": { "@expo/schema-utils": "^55.0.3", "expo-dev-menu": "55.0.24", "expo-manifests": "~55.0.16" }, "peerDependencies": { "expo": "*" } }, "sha512-Rusz6VfVUAXPArkQhnxC5yY70RCfGNZv+06qCGIkm2boQ3wOiSUwJic8oIt7kW6yD2rkpm24q/7F/6r5joPfng=="], - "expo-dev-menu": ["expo-dev-menu@55.0.26", "", { "dependencies": { "expo-dev-menu-interface": "55.0.2" }, "peerDependencies": { "expo": "*" } }, "sha512-NXZumkYIycz77IY/o7qI9Ow+qb/qkq6aQ4eqO7tUJMCyBNVIfwfrb3Qm9ANhZlDT0yrk8FcH7zYmtoJbfwRr1Q=="], + "expo-dev-menu": ["expo-dev-menu@55.0.24", "", { "dependencies": { "expo-dev-menu-interface": "55.0.2" }, "peerDependencies": { "expo": "*" } }, "sha512-/J93rADODlKpmaN9uywTd/RMywPDeUo/bAnrZNxlHrFUuO1VCGqYLhacITg2zebU8hucaou8pa8zVsTQaUCv6w=="], "expo-dev-menu-interface": ["expo-dev-menu-interface@55.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-DomUNvGzY/xliwnMdbAYY780sCv19N7zIbifc0ClcoCzJZpNSCkvJ2qGIFRPyM/7DmqmlHGCKi8di7kYYLKNEg=="], @@ -2591,7 +2634,7 @@ "expo-json-utils": ["expo-json-utils@55.0.2", "", {}, "sha512-QJMOZOPOG7CTnKcrdVaiummn2va1MCO56z++eyWkDv3GBRODldM6MFMDf/jTREWthFc2Nxo6TuyWRrEV9S6n/Q=="], - "expo-keep-awake": ["expo-keep-awake@55.0.7", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-QBWOEu8FkPBGYc0h0rsCkSTMJNBEKgzVsmLuQpO7V79V9sPR052k3Iiu/G8Kzmny2enyHYYed8RY+CUsip/SeQ=="], + "expo-keep-awake": ["expo-keep-awake@55.0.6", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-acJjeHqkNxMVckEcJhGQeIksqqsarscSHJtT559bNgyiM4r14dViQ66su7bb6qDVeBt0K7z3glXI1dHVck1Zgg=="], "expo-linear-gradient": ["expo-linear-gradient@55.0.13", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-Qz2T4jpkA15RIk29DBqI1TwW+8O9AN8MyC4TJPbh/5UnihH0yNNz3waplUO8Szh5OZ3czTGvtPQU4ysF3RDxwQ=="], @@ -2605,10 +2648,12 @@ "expo-modules-autolinking": ["expo-modules-autolinking@55.0.19", "", { "dependencies": { "@expo/require-utils": "^55.0.4", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-rHO1NZC/bxcKTLzkn6WYm9ErzS6qp7Kgb1NM2YxXJAYRWHwW/M7NZXyj6swWiKxyhRpcdoppRpjrz1sBuYGAjg=="], - "expo-modules-core": ["expo-modules-core@55.0.24", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-1FztZjelwf3xQZpD6+LFo6IKjnGF/PMVXYkv9aC3EybMl/ZbXji35cfhy9W5uR/bwQ7L+SVqvd5A00XOoIiO8Q=="], + "expo-modules-core": ["expo-modules-core@55.0.23", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-IGWT5N9MoV4zgWyrv686bElnKhzhE7E6pSazhaBNh3vgViAah5nnAz2o5h5YoUMR2B+ZTdHumRbGHN6gHLgwPA=="], "expo-navigation-bar": ["expo-navigation-bar@55.0.12", "", { "dependencies": { "debug": "^4.3.2", "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-G7olnyAqGd7I3hLFAgP4WdcZFMD9pV6UY79P7EHyRdMuRZrYJfDdwcelyYB2+tekOdQEktZ3WlLVK+uS7f7TYw=="], + "expo-network": ["expo-network@55.0.13", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-7u+npCmCPRpVrjkUlQtUetPnTN1gRyj7z13bBM5w9w1AHMb4PfoxtIys5EB9ukzNYBg/gaZ/y5dtxomGpc6BKw=="], + "expo-router": ["expo-router@55.0.13", "", { "dependencies": { "@expo/metro-runtime": "^55.0.10", "@expo/schema-utils": "^55.0.3", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@react-navigation/native-stack": "^7.14.5", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.10", "expo-image": "^55.0.9", "expo-server": "^55.0.8", "expo-symbols": "^55.0.7", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.11", "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.15", "expo-linking": "^55.0.14", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-cIBR5RmQtbr+b535mlbMhmm7lweVZXFtjzJOgJTutoxIApRztl816kFRFNesnVyqQ0LZrEU0a6vqa3i0wdlRQw=="], "expo-secure-store": ["expo-secure-store@55.0.13", "", { "peerDependencies": { "expo": "*" } }, "sha512-I6r0JNO1Fd4o0Gu7Ixiic7s89lqgdUHq17uBH9y1f/AntoyKn71TdtYJH82RgfsBbu5qNVzrwImmvlANyOlITQ=="], @@ -2829,7 +2874,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hono": ["hono@4.12.16", "", {}, "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="], + "hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="], "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -3065,6 +3110,8 @@ "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], + "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="], + "lan-network": ["lan-network@0.2.1", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A=="], "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="], @@ -3163,7 +3210,7 @@ "lru_map": ["lru_map@0.3.3", "", {}, "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ=="], - "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], + "lucide-react": ["lucide-react@1.11.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g=="], "magic-regexp": ["magic-regexp@0.11.0", "", { "dependencies": { "magic-string": "^0.30.21", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "unplugin": "^3.0.0" } }, "sha512-LG77Z/gVnwz7oaDpD4heX6ryl+lcr4l1B2gnP4MMvt2pGhGC1Dfj7dl1pXpP4ih+VQFLuAadeKVa+lARAzfW+Q=="], @@ -3233,33 +3280,33 @@ "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], - "metro": ["metro@0.83.7", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-config": "0.83.7", "metro-core": "0.83.7", "metro-file-map": "0.83.7", "metro-resolver": "0.83.7", "metro-runtime": "0.83.7", "metro-source-map": "0.83.7", "metro-symbolicate": "0.83.7", "metro-transform-plugins": "0.83.7", "metro-transform-worker": "0.83.7", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ=="], + "metro": ["metro@0.83.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.6", "metro-cache": "0.83.6", "metro-cache-key": "0.83.6", "metro-config": "0.83.6", "metro-core": "0.83.6", "metro-file-map": "0.83.6", "metro-resolver": "0.83.6", "metro-runtime": "0.83.6", "metro-source-map": "0.83.6", "metro-symbolicate": "0.83.6", "metro-transform-plugins": "0.83.6", "metro-transform-worker": "0.83.6", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-pbdndsAZ2F/ceopDdhVbttpa/hfLzXPJ/husc+QvQ33R0D9UXJKzTn5+OzOXx4bpQNtAKF2bY88cCI3Zl44xDQ=="], - "metro-babel-transformer": ["metro-babel-transformer@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.83.7", "nullthrows": "^1.1.1" } }, "sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA=="], + "metro-babel-transformer": ["metro-babel-transformer@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.83.6", "nullthrows": "^1.1.1" } }, "sha512-1AnuazBpzY3meRMr04WUw14kRBkV0W3Ez+AA75FAeNpRyWNN5S3M3PHLUbZw7IXq7ZeOzceyRsHStaFrnWd+8w=="], - "metro-cache": ["metro-cache@0.83.7", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.7" } }, "sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg=="], + "metro-cache": ["metro-cache@0.83.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.6" } }, "sha512-DpvZE32feNkqfZkI4Fic7YI/Kw8QP9wdl1rC4YKPrA77wQbI9vXbxjmfkCT/EGwBTFOPKqvIXo+H3BNe93YyiQ=="], - "metro-cache-key": ["metro-cache-key@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg=="], + "metro-cache-key": ["metro-cache-key@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-5gdK4PVpgNOHi7xCGrgesNP1AuOA2TiPqpcirGXZi4RLLzX1VMowpkgTVtBfpQQCqWoosQF9yrSo9/KDQg1eBg=="], - "metro-config": ["metro-config@0.83.7", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.7", "metro-cache": "0.83.7", "metro-core": "0.83.7", "metro-runtime": "0.83.7", "yaml": "^2.6.1" } }, "sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q=="], + "metro-config": ["metro-config@0.83.6", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.6", "metro-cache": "0.83.6", "metro-core": "0.83.6", "metro-runtime": "0.83.6", "yaml": "^2.6.1" } }, "sha512-G5622400uNtnAMlppEA5zkFAZltEf7DSGhOu09BkisCxOlVMWfdosD/oPyh4f2YVQsc1MBYyp4w6OzbExTYarg=="], - "metro-core": ["metro-core@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.7" } }, "sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg=="], + "metro-core": ["metro-core@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.6" } }, "sha512-l+yQ2fuIgR//wszUlMrrAa9+Z+kbKazd0QOh0VQY7jC4ghb7yZBBSla/UMYRBZZ6fPg9IM+wD3+h+37a5f9etw=="], - "metro-file-map": ["metro-file-map@0.83.7", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw=="], + "metro-file-map": ["metro-file-map@0.83.6", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-Jg3oN604C7GWbQwFAUXt8KsbMXeKfsxbZ5HFy4XFM3ggTS+ja9QgUmq9B613kgXv3G4M6rwiI6cvh9TRly4x3w=="], - "metro-minify-terser": ["metro-minify-terser@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ=="], + "metro-minify-terser": ["metro-minify-terser@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Vx3/Ne9Q+EIEDLfKzZUOtn/rxSNa/QjlYxc42nvK4Mg8mB6XUgd3LXX5ZZVq7lzQgehgEqLrbgShJPGfeF8PnQ=="], - "metro-resolver": ["metro-resolver@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A=="], + "metro-resolver": ["metro-resolver@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-lAwR/FsT1uJ5iCt4AIsN3boKfJ88aN8bjvDT5FwBS0tKeKw4/sbdSTWlFxc7W/MUTN5RekJ3nQkJRIWsvs28tA=="], - "metro-runtime": ["metro-runtime@0.83.7", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ=="], + "metro-runtime": ["metro-runtime@0.83.6", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-WQPua1G2VgYbwRn6vSKxOhTX7CFbSf/JdUu6Nd8bZnPXckOf7HQ2y51NXNQHoEsiuawathrkzL8pBhv+zgZFmg=="], - "metro-source-map": ["metro-source-map@0.83.7", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.7", "nullthrows": "^1.1.1", "ob1": "0.83.7", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw=="], + "metro-source-map": ["metro-source-map@0.83.6", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.6", "nullthrows": "^1.1.1", "ob1": "0.83.6", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-AqJbOMMpeyyM4iNI91pchqDIszzNuuHApEhg6OABqZ+9mjLEqzcIEQ/fboZ7x74fNU5DBd2K36FdUQYPqlGClA=="], - "metro-symbolicate": ["metro-symbolicate@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.7", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw=="], + "metro-symbolicate": ["metro-symbolicate@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-4nvkmv9T7ozhprlPwk/+xm0SVPsxly5kYyMHdNaOlFemFz4df9BanvD46Ac6OISu/4Idinzfk2KVb++6OfzPAQ=="], - "metro-transform-plugins": ["metro-transform-plugins@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA=="], + "metro-transform-plugins": ["metro-transform-plugins@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-V+zoY2Ul0v0BW6IokJkTud3raXmDdbdwkUQ/5eiSoy0jKuKMhrDjdH+H5buCS5iiJdNbykOn69Eip+Sqymkodg=="], - "metro-transform-worker": ["metro-transform-worker@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.7", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-minify-terser": "0.83.7", "metro-source-map": "0.83.7", "metro-transform-plugins": "0.83.7", "nullthrows": "^1.1.1" } }, "sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw=="], + "metro-transform-worker": ["metro-transform-worker@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.6", "metro-babel-transformer": "0.83.6", "metro-cache": "0.83.6", "metro-cache-key": "0.83.6", "metro-minify-terser": "0.83.6", "metro-source-map": "0.83.6", "metro-transform-plugins": "0.83.6", "nullthrows": "^1.1.1" } }, "sha512-G5kDJ/P0ZTIf57t3iyAd5qIXbj2Wb1j7WtIDh82uTFQHe2Mq2SO9aXG9j1wI+kxZlIe58Z22XEXIKMl89z0ibQ=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -3319,7 +3366,7 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -3355,7 +3402,9 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], + "nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="], + + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], "nativewind": ["nativewind@4.2.3", "", { "dependencies": { "comment-json": "^4.2.5", "debug": "^4.3.7", "react-native-css-interop": "0.2.3" }, "peerDependencies": { "tailwindcss": ">3.3.0" } }, "sha512-HglF1v6A8CqBFpXWs0d3yf4qQGurrreLuyE8FTRI/VDH8b0npZa2SDG5tviTkLiBg0s5j09mQALZOjxuocgMLA=="], @@ -3393,7 +3442,7 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - "ob1": ["ob1@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg=="], + "ob1": ["ob1@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-m/xZYkwcjo6UqLMrUICEB3iHk7Bjt3RSR7KXMi6Y1MO/kGkPhoRmfUDF6KAan3rLAZ7ABRqnQyKUTwaqZgUV4w=="], "object-assign": ["object-assign@4.0.1", "", {}, "sha512-c6legOHWepAbWnp3j5SRUMpxCXBKI4rD7A5Osn9IzZ8w4O/KccXdW0lqdkQKbpk0eHGjNgKihgzY6WuEq99Tfw=="], @@ -3525,7 +3574,7 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "plist": ["plist@3.1.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], @@ -3533,7 +3582,7 @@ "postal-mime": ["postal-mime@2.7.4", "", {}, "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g=="], - "postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="], @@ -3561,7 +3610,7 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.4", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw=="], + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.3", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-lckXaWWdo2ZVXoMoUO3WIBiz9hVY+YBEh1gYyMFfrWP9WZW/wpFXQKizHx7WrFQFMkcG0bGShdpp531X1n+qpg=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], @@ -3621,9 +3670,9 @@ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - "react-hook-form": ["react-hook-form@7.74.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g=="], + "react-hook-form": ["react-hook-form@7.73.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA=="], - "react-i18next": ["react-i18next@17.0.6", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw=="], + "react-i18next": ["react-i18next@17.0.4", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g=="], "react-is": ["react-is@19.2.5", "", {}, "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ=="], @@ -3761,10 +3810,12 @@ "robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="], - "rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="], + "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], @@ -3807,6 +3858,8 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -4007,7 +4060,7 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -4087,7 +4140,7 @@ "uc.micro": ["uc.micro@1.0.6", "", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="], - "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], @@ -4213,11 +4266,11 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "workerd": ["workerd@1.20260430.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260430.1", "@cloudflare/workerd-darwin-arm64": "1.20260430.1", "@cloudflare/workerd-linux-64": "1.20260430.1", "@cloudflare/workerd-linux-arm64": "1.20260430.1", "@cloudflare/workerd-windows-64": "1.20260430.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-KEgIWyiw3Jmn+DCd/L3ePo5fmiiYb/UcwKvDWPf/nLLOiwShDFzDSsegU5NY/JcwgvO/QsLHVi2FYrbkcXNY5Q=="], + "workerd": ["workerd@1.20260424.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260424.1", "@cloudflare/workerd-darwin-arm64": "1.20260424.1", "@cloudflare/workerd-linux-64": "1.20260424.1", "@cloudflare/workerd-linux-arm64": "1.20260424.1", "@cloudflare/workerd-windows-64": "1.20260424.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oKsB0Xo/mfkYMdSACoS06XZg09VUK4rXwHfF/1t3P++sMbwzf4UHQvMO57+zxpEB2nVrY/ZkW0bYFGq4GdAFSQ=="], "workers-ai-provider": ["workers-ai-provider@0.7.5", "", { "dependencies": { "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8" } }, "sha512-dhCwgc3D65oDDTpH3k8Gf0Ek7KItzvaQidn2N5L5cqLo3WG8GM/4+Nr4rU56o8O3oZRsloB1gUCHYaRv2j7Y0A=="], - "wrangler": ["wrangler@4.87.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260430.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260430.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260430.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-lfhfKwLfQlowwgV0xhlYgE9fU3n0I30d4ccGY/rTCEm/n42Mjvlr0Ng3ZPNqlsrsKBcDR531V7dsPkgELvrk/Q=="], + "wrangler": ["wrangler@4.85.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260424.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260424.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260424.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-93cwt2RPb1qdcmEgPzH7ybiLN4BIKoWpscIX6SywjHrQOeIZrQk2haoc3XMLKtQTmzapxza9OuDD+kMHpsuuhg=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -4257,7 +4310,7 @@ "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], - "youtube-transcript": ["youtube-transcript@1.3.1", "", {}, "sha512-NDCjwad113TGybbYF51y9Z4tcwzBHUZWQdF9veULNca18L+FdDbHHtTHIr69WVa3bB90l67S8kN0HtL2JO9fhg=="], + "youtube-transcript": ["youtube-transcript@1.3.0", "", {}, "sha512-laWv9RcKIWh6rZUH3hVnOngEvtKAhFMV5UepUO6AgevPYqe2zv8KW/uCkZJDSnPwf5/AdVu0Q66/1RDblKsp6Q=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4269,10 +4322,24 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/react/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4287,6 +4354,12 @@ "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@better-auth/core/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@better-auth/expo/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.35.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.3", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250906.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20250906.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250906.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-HbyXtbrh4Fi3mU8ussY85tVdQ74qpVS1vctUgaPc+bPrXBTqfDLkZ6VRtHAVF/eBhz4SFmhJtCQpN1caY2Ak8A=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], @@ -4341,7 +4414,7 @@ "@expo/xcpretty/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@gorhom/portal/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "@gorhom/portal/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -4357,7 +4430,7 @@ "@manypkg/tools/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], @@ -4395,17 +4468,25 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@react-native-ai/apple/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@react-native-ai/apple/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], + + "@react-native-ai/apple/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-60GYsRj5wIJQRcq5YwYJq4KhwLeStceXEJiZdecP1miiH+6FMmrnc7lZDOJoQ6m9lrudEb+uI4LEwddLz5+rPQ=="], + + "@react-native-ai/apple/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@react-native-ai/llama/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], + + "@react-native-ai/llama/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-60GYsRj5wIJQRcq5YwYJq4KhwLeStceXEJiZdecP1miiH+6FMmrnc7lZDOJoQ6m9lrudEb+uI4LEwddLz5+rPQ=="], - "@react-native-ai/apple/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA=="], + "@react-native-ai/llama/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@react-native-ai/apple/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "@react-native/babel-preset/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw=="], - "@react-native-ai/llama/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@react-native/babel-preset/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-replace-supers": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q=="], - "@react-native-ai/llama/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA=="], + "@react-native/babel-preset/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg=="], - "@react-native-ai/llama/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w=="], "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -4415,11 +4496,11 @@ "@react-native/dev-middleware/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "@react-navigation/core/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "@react-navigation/core/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@react-navigation/native/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "@react-navigation/native/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@react-navigation/routers/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "@react-navigation/routers/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], @@ -4451,7 +4532,7 @@ "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -4461,11 +4542,9 @@ "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "agents/partyserver": ["partyserver@0.5.5", "", { "dependencies": { "nanoid": "^5.1.9" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260424.1" } }, "sha512-7zub8oV8Od9dY2aXGrgzhX5GLceaWOg7xB5VWXtDcqt2BWVDIOCAgaF0AmBMSu3AXhJHsFdzPnA8SSZdybXMbQ=="], - "agents/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], - "agents/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "agents/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -4475,6 +4554,14 @@ "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "babel-preset-expo/@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + + "better-auth/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "better-auth-cloudflare/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], "burnt/sf-symbols-typescript": ["sf-symbols-typescript@1.0.0", "", {}, "sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw=="], @@ -4485,10 +4572,14 @@ "chrome-launcher/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + "chrome-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + "chrome-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "chromium-bidi/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "chromium-edge-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + "chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], @@ -4513,7 +4604,7 @@ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "drizzle-kit/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -4527,9 +4618,9 @@ "eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/type-utils": "8.59.1", "@typescript-eslint/utils": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="], - "eslint-config-universe/@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA=="], + "eslint-config-universe/@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="], "eslint-config-universe/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -4567,11 +4658,11 @@ "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "expo-router/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "expo-router/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - "expo-updates/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + "expo-updates/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], "expo-updates/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4659,8 +4750,12 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "meow/object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "merge-options/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], + "metro/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], "metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], @@ -4699,9 +4794,7 @@ "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - "plist/@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], - - "postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -4777,7 +4870,7 @@ "util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], - "vite/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -4785,7 +4878,7 @@ "workers-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - "wrangler/miniflare": ["miniflare@4.20260430.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260430.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-MWvMm3Siho9Yj7lbJZidLs8hbrRvIcOrif2mnsHQZdvoKfedpea+GaN8XJxbpRcq0B2WzNI1BB1ihdnqes3/ZA=="], + "wrangler/miniflare": ["miniflare@4.20260424.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260424.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw=="], "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -4907,7 +5000,7 @@ "@expo/metro-config/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "@expo/metro-config/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "@expo/metro-config/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@expo/package-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4963,6 +5056,8 @@ "@react-native/codegen/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "@react-native/dev-middleware/chrome-launcher/lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], + "@react-native/dev-middleware/serve-static/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], "@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -4983,8 +5078,12 @@ "babel-plugin-istanbul/test-exclude/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "chrome-launcher/lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "chrome-launcher/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "chromium-edge-launcher/lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "chromium-edge-launcher/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -5003,75 +5102,77 @@ "connect/finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1" } }, "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="], "eslint-config-universe/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "eslint-config-universe/@typescript-eslint/eslint-plugin/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1" } }, "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg=="], + "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], - "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], - "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g=="], + "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], - "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg=="], + "eslint-config-universe/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="], "eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], @@ -5123,6 +5224,8 @@ "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], + "metro/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], "mimetext/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -5175,7 +5278,7 @@ "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="], - "next/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -5201,57 +5304,59 @@ "tmp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "workers-ai-provider/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "workers-ai-provider/@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "wrangler/miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], @@ -5263,6 +5368,8 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -5357,6 +5464,8 @@ "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@react-native/dev-middleware/chrome-launcher/lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "@react-native/dev-middleware/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "@react-native/dev-middleware/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -5375,21 +5484,25 @@ "babel-plugin-istanbul/test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "chrome-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "chrome-launcher/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "chromium-edge-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "chromium-edge-launcher/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], - "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], "eslint-config-universe/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -5441,6 +5554,8 @@ "@react-native/codegen/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@react-native/dev-middleware/chrome-launcher/lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "@react-native/dev-middleware/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "agents/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/docs/plans/2026-04-30-feat-better-auth-migration-plan.md b/docs/plans/2026-04-30-feat-better-auth-migration-plan.md new file mode 100644 index 0000000000..41a51ab4a4 --- /dev/null +++ b/docs/plans/2026-04-30-feat-better-auth-migration-plan.md @@ -0,0 +1,581 @@ +--- +title: "feat: Migrate auth to Better Auth with OAuth 2.1 server for MCP" +type: feat +status: completed +date: 2026-04-30 +--- + +# Migrate Auth to Better Auth with OAuth 2.1 Server for MCP + +## Overview + +Replace PackRat's handwritten JWT/refresh-token auth system with **Better Auth** — a TypeScript-first, Cloudflare Workers-compatible auth library. The MCP server gets proper OAuth 2.1 via Cloudflare's `workers-oauth-provider` library, which delegates user authentication to Better Auth. The immediate business driver is enabling MCP clients (Claude Desktop, Cursor) to authorize against PackRat automatically, without manually copying a JWT from a settings page. + +**Resolved architectural decisions (from deep research phase):** +- OAuth 2.1 server lives in the **MCP Worker** (`packages/mcp`) using `cloudflare/workers-oauth-provider`. Better Auth in the API Worker serves as the identity provider for the login step. +- `nodejs_als` flag (not full `nodejs_compat`) is sufficient for Better Auth's AsyncLocalStorage dependency. +- The Expo app uses `@better-auth/expo` with the `expoClient` plugin and `bearer()` server plugin for explicit Bearer header flow. + +**LOE:** ~10–15 engineering days across 5 phases. + +--- + +## Problem Statement + +The current custom auth system has three compounding problems: + +1. **MCP requires manual JWT copy.** The MCP server accepts `Authorization: Bearer ` but issues no OAuth flow. MCP clients (Claude Desktop, Cursor) cannot authorize automatically. +2. **Security gaps.** Apple Sign In currently decodes the identity token payload without verifying the Apple signature (`base64.decode` only, lines 506–511 of `packages/api/src/routes/auth/index.ts`). This is an authentication bypass: any attacker can forge an Apple identity token and call the endpoint. +3. **Maintenance burden.** Every auth feature (2FA, passkeys, scoped permissions) must be handrolled. Better Auth ships these as stable, tested plugins. + +--- + +## Proposed Solution + +### API Worker (`packages/api`) +Migrate to **Better Auth** with: +- `emailAndPassword` plugin (bcrypt password override — no forced resets) +- `socialProviders.google` + `socialProviders.apple` (fixes Apple signature verification) +- `bearer()` plugin (Bearer token support for mobile and API clients) +- `jwt()` plugin (short-lived asymmetric JWTs with public JWKS endpoint — for downstream service verification) +- `admin` plugin (maps existing `role: 'ADMIN'` users) +- `@better-auth/drizzle-adapter` targeting existing Neon/Postgres database +- `@better-auth/expo` client in the mobile app + +### MCP Worker (`packages/mcp`) +Add `cloudflare/workers-oauth-provider` as the OAuth 2.1 authorization server. It handles PKCE, auth codes, and token issuance locally. The `/authorize` endpoint redirects users to a Better Auth login page on the API. After login, the MCP Worker stores the resulting Better Auth session and issues MCP-scoped access tokens. + +--- + +## Technical Approach + +### Resolved: OAuth Server Architecture (Option B) + +**Why workers-oauth-provider in the MCP Worker wins:** + +The MCP 2025 spec now uses RFC 9728 Protected Resource Metadata. The MCP server must serve `/.well-known/oauth-protected-resource`, and that document can point to an authorization server on any domain. Cross-domain OAuth (Option A) is technically spec-compliant — but it requires Claude Desktop and Cursor to perform a second discovery fetch against the API domain, which is more brittle and less battle-tested in the wild. + +Option B (MCP Worker as the OAuth server, Better Auth as the identity provider) cleanly isolates MCP-scoped tokens from main API tokens. The MCP Worker serves both `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` from `mcp.packrat.world`, with zero cross-domain discovery complexity. Token issuance, storage (KV), and validation are all local to the MCP Worker. + +``` +MCP client (Claude Desktop) + → GET mcp.packrat.world/.well-known/oauth-authorization-server + → POST mcp.packrat.world/register (dynamic client registration) + → GET mcp.packrat.world/authorize?code_challenge=... + → redirects to api.packrat.world login page (Better Auth) + → user authenticates with Better Auth + → Better Auth issues session token + → callback returns to mcp.packrat.world/callback?session_token=... + → MCP Worker verifies session via Better Auth's session API + → MCP Worker calls env.OAUTH_PROVIDER.completeAuthorization({ userId, scopes }) + → POST mcp.packrat.world/token (code exchange) + → MCP tool calls: Authorization: Bearer + → MCP Worker validates token from its own KV store + → forwards calls to api.packrat.world as a signed service-to-service request +``` + +### Auth Object Lifecycle in Cloudflare Workers + +`betterAuth({...})` requires `env.DB` and `env.KV` which are only available per-request, not at module init. Use a module-level singleton (valid within one isolate) that is initialized lazily on first request: + +```typescript +// packages/api/src/auth/index.ts +import { betterAuth } from "better-auth"; +import { withCloudflare, createKVStorage } from "better-auth-cloudflare"; +import { drizzleAdapter } from "@better-auth/drizzle-adapter"; + +let _auth: ReturnType | null = null; + +export function getAuth(env: Env, cf: IncomingRequestCfProperties) { + if (!_auth) { + _auth = betterAuth( + withCloudflare( + { cf, kv: env.AUTH_KV, d1: { db: drizzle(env.DB) } }, + { + secret: env.BETTER_AUTH_SECRET, + baseURL: env.BETTER_AUTH_URL, + trustedOrigins: [env.BETTER_AUTH_URL, "https://mcp.packrat.world"], + // ... plugins + } + ) + ); + } + return _auth; +} +``` + +**Note:** If `cf` context (geolocation, IP) must be accurate per-request rather than per-isolate, skip the singleton and create the auth object fresh on each request — `betterAuth({...})` is synchronous and cheap. + +### Elysia Integration (Concrete Pattern) + +Replace the existing `authPlugin` macro with a Better Auth-backed equivalent — zero call-site changes required: + +```typescript +// packages/api/src/middleware/auth.ts (replacement) +export const authPlugin = (auth: ReturnType) => + new Elysia({ name: "auth-plugin", aot: false }) // aot: false required with .mount() + .mount(auth.handler) // handles all /api/auth/* routes + .macro({ + isAuthenticated: { // existing route API unchanged + async resolve({ status, request: { headers } }) { + const session = await auth.api.getSession({ headers }); + if (!session) return status(401); + return { user: session.user, session: session.session }; + }, + }, + }); +``` + +### CORS Configuration + +```typescript +// @elysiajs/cors — both settings are required +cors({ + origin: [env.BETTER_AUTH_URL, "https://mcp.packrat.world"], + credentials: true, // REQUIRED — omitting breaks all cookie/bearer auth + allowedHeaders: ["Content-Type", "Authorization"], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], +}) + +// Better Auth config — must match CORS origin +betterAuth({ + trustedOrigins: [env.BETTER_AUTH_URL, "https://mcp.packrat.world"], +}) +``` + +`credentials: true` + `origin: *` is rejected by browsers. `allowedHeaders` must include `Authorization` for the `bearer()` plugin to work. + +### Cloudflare Workers Flags + +```toml +# packages/api/wrangler.toml +compatibility_flags = ["nodejs_als"] # narrower than nodejs_compat; sufficient for Better Auth +compatibility_date = "2024-09-23" # required for nodejs_als +``` + +`nodejs_als` enables only `AsyncLocalStorage` — Better Auth's sole Node.js dependency. Avoid the full `nodejs_compat` flag unless a plugin adds a hard Node.js API dependency (e.g., a nodemailer-based email sender). + +### Schema Migration Strategy + +Better Auth needs: `user`, `session`, `account`, `verification`, `jwks` tables. Map to the existing `users` table via schema override: + +```typescript +drizzleAdapter(db, { + provider: "pg", + schema: { + ...betterAuthSchema, + user: schema.users, // reuse existing table — preserves all foreign keys + }, + usePlural: true, +}) +``` + +Add `additionalFields` for the `role` column: +```typescript +user: { + additionalFields: { + role: { type: ["USER", "ADMIN"], required: false, defaultValue: "USER", input: false } + } +} +``` + +Add missing columns to `users` via migration: `image text`, `updated_at timestamp`. All legacy tables (`auth_providers`, `refresh_tokens`, `one_time_passwords`) are preserved until Phase 5 cleanup. + +### KV as Secondary Storage — Rate Limit TTL + +Cloudflare KV enforces a hard minimum `expirationTtl` of 60 seconds. All Better Auth rate limit windows must be `≥ 60`: + +```typescript +rateLimit: { + enabled: true, + customRules: { + "/sign-in/email": { window: 60, max: 5 }, + "/sign-up/email": { window: 60, max: 3 }, + "/forget-password": { window: 60, max: 3 }, + }, +} +``` + +For sub-60s burst protection, use Cloudflare's native Rate Limiting rules at the edge (not in Worker code). + +--- + +## Implementation Phases + +### Phase 0: Security prerequisite (1 day) — before Phase 2 + +**Backport Apple signature verification into the legacy endpoint** before opening the parallel operation window. The current `base64.decode`-only path is an authentication bypass. An attacker with access to the legacy endpoint can forge any Apple identity and create a valid session. This must be fixed regardless of migration timeline. + +- [ ] In `packages/api/src/routes/auth/index.ts`, replace the base64-decode-only Apple handling with proper JWT signature verification using Apple's public keys from `appleid.apple.com/auth/keys` +- [ ] Add `APPLE_PRIVATE_KEY`, `APPLE_KEY_ID`, `APPLE_TEAM_ID` to `packages/env/src/node.ts` and `packages/api/src/utils/env-validation.ts` (needed for both legacy fix and Better Auth Apple provider) +- [ ] Obtain `.p8` private key from Apple Developer portal, store as Cloudflare Worker secret (`wrangler secret put APPLE_PRIVATE_KEY`) — not in `.env` + +--- + +### Phase 1: Foundation (2–3 days) + +**Goal:** Better Auth running alongside existing auth, zero traffic. + +- [ ] Add `compatibility_flags = ["nodejs_als"]` and `compatibility_date = "2024-09-23"` to `packages/api/wrangler.toml` +- [ ] Install: `better-auth`, `@better-auth/drizzle-adapter`, `@better-auth/expo`, `better-auth-cloudflare` +- [ ] Create `packages/api/src/auth/config.ts` — Better Auth config with all plugins (not yet mounted) +- [ ] Add env vars to `packages/env/src/node.ts` and `packages/api/src/utils/env-validation.ts`: + - `BETTER_AUTH_SECRET` (required, min 32 chars) + - `BETTER_AUTH_URL` (required, API base URL e.g. `https://api.packrat.world`) + - `APPLE_PRIVATE_KEY`, `APPLE_KEY_ID`, `APPLE_TEAM_ID` (if not done in Phase 0) +- [ ] Write Drizzle migration `packages/api/drizzle/0038_better_auth_tables.sql`: + - Add `image text`, `updated_at timestamp` to `users` + - Create `session`, `account`, `verification` tables + - Create `jwks` table (for `jwt()` plugin key rotation) +- [ ] Wire `auth.handler` into Elysia via `authPlugin(getAuth(env, cf))` — mounted at `/api/auth/*`, zero routing change to existing routes +- [ ] Deploy to staging, verify: + - `GET /api/auth/ok` → 200 + - `GET /api/auth/jwks` → valid JWKS key set + - `POST /api/auth/sign-in/email` → issues session token +- [ ] Store `BETTER_AUTH_SECRET` as a Cloudflare Worker secret + +**Files:** +- `packages/api/wrangler.toml` +- `packages/api/src/auth/config.ts` *(new)* +- `packages/api/src/middleware/auth.ts` *(refactor to Better Auth macro)* +- `packages/api/src/index.ts` *(mount auth handler)* +- `packages/env/src/node.ts`, `packages/api/src/utils/env-validation.ts` +- `packages/api/drizzle/0038_better_auth_tables.sql` *(new)* + +--- + +### Phase 2: Parallel operation + data migration (3–4 days) + +**Goal:** New sign-ins go through Better Auth. Old JWTs still work for up to 7 days. + +- [ ] Implement dual-auth middleware: + ```typescript + async function resolveUser(headers: Headers, env: Env, cf: CF) { + const session = await getAuth(env, cf).api.getSession({ headers }); + if (session) return session.user; + return verifyLegacyJWT(headers, env); // existing verifyJWT() — keep alive + } + ``` +- [ ] Write idempotent one-time user migration script `packages/api/scripts/migrate-to-better-auth.ts`: + - Copy `users` rows into Better Auth's `user` table (using `forceAllowId: true` to preserve numeric IDs) + - Copy `auth_providers` rows into `account` table: map `provider`→`providerId`, `providerId`→`accountId`; for email accounts, copy `passwordHash` into `account.password` + - Mark all migrated users `emailVerified: true` (they already verified) + - Cache Apple users' email from `auth_providers` into the `account` record (Apple omits email after first auth — cache it now or it's gone) + - Script must be idempotent: `INSERT ... ON CONFLICT (id) DO NOTHING` + - Log before/after row counts; abort if delta > 1% +- [ ] Run migration script in production after Phase 1 stabilizes +- [ ] Switch new login/register calls to Better Auth endpoints: + - `POST /api/auth/sign-in/email` (replaces `POST /api/auth/login`) + - `POST /api/auth/sign-up/email` (replaces `POST /api/auth/register`) + - `POST /api/auth/sign-in/social` with `provider: "google"` / `"apple"` +- [ ] Keep old auth routes alive during overlap for clients still holding legacy JWTs +- [ ] Monitor: Better Auth session hit rate, legacy JWT fallback rate (structured logs); alert if legacy rate goes to zero before 7-day window closes (indicates clients updated faster than expected) +- [ ] Audit `PASSWORD_RESET_SECRET` usage — grep across all packages; it is declared in env schema but not imported in `utils/auth.ts` + +**Critical gap:** Normalize response timing in the dual-auth shim to prevent timing oracles. If Better Auth session lookup fails fast (KV miss) and legacy HMAC lookup fails slow (DB + constant-time compare), attackers can distinguish the two paths by latency. Use a fixed minimum response time or always run both paths concurrently. + +**Files:** +- `packages/api/src/middleware/auth.ts` +- `packages/api/scripts/migrate-to-better-auth.ts` *(new)* +- `packages/api/src/routes/auth/index.ts` *(mark old endpoints deprecated, keep alive)* + +--- + +### Phase 3: MCP OAuth 2.1 server (2–3 days) + +**Goal:** Claude Desktop / Cursor can authorize against PackRat via standard OAuth 2.1. + +- [x] Install `@cloudflare/workers-oauth-provider` in `packages/mcp` +- [x] Wrap MCP Worker entrypoint with `OAuthProvider`: + ```typescript + import { OAuthProvider } from "@cloudflare/workers-oauth-provider"; + export default new OAuthProvider({ + apiRoute: "/mcp", + apiHandler: mcpApiHandler, + defaultHandler: PackRatAuthHandler, + authorizeEndpoint: "/authorize", + tokenEndpoint: "/token", + clientRegistrationEndpoint: "/register", + allowPlainPKCE: false, // S256 only + accessTokenTTL: 3600, // 60 minutes + }); + ``` +- [x] Implement `PackRatAuthHandler` in `packages/mcp/src/auth.ts`: + - `/authorize` → stores OAuth state in KV, redirects to `/login` + - `GET /login` → serves sign-in HTML form + - `POST /login` → calls Better Auth sign-in API (server-to-server), stores session token in KV, redirects to `/callback` + - `/callback` → retrieves OAuth state + session from KV, calls `env.OAUTH_PROVIDER.completeAuthorization({ userId, props: { betterAuthToken } })` +- [x] Configure MCP access token lifetime at **60 minutes** via `accessTokenTTL: 3600`; refresh tokens at 30 days +- [x] Implement PKCE enforcement: `allowPlainPKCE: false` — only S256 accepted +- [x] Implement backward compatibility: `resolveExternalToken` accepts legacy Better Auth session tokens directly (no OAuth flow needed for existing clients) +- [x] Add `OAUTH_KV` KV binding to `wrangler.jsonc` (placeholder IDs to replace before deploy) +- [ ] Configure dynamic client registration security: require an initial access token — **do not allow open unauthenticated registration in production** +- [ ] Redirect URI exact-match validation (handled by `workers-oauth-provider` library) +- [ ] Pre-register Claude Desktop and Cursor as trusted clients (bypass consent screen for known clients) +- [ ] Add deprecation banner to PackRat settings UI for manually-issued MCP JWTs + +**Files:** +- `packages/mcp/package.json` *(add @cloudflare/workers-oauth-provider)* +- `packages/mcp/src/index.ts` *(wrap with OAuthProvider)* +- `packages/mcp/src/auth.ts` *(new — authorize + callback handler)* +- `packages/mcp/wrangler.toml` *(add KV binding for OAuth token storage)* +- PackRat settings UI *(deprecation notice)* + +--- + +### Phase 4: Client updates (2–3 days) + +**Goal:** Expo app and Next.js apps use Better Auth sessions natively. Fix dual-read token path. + +**Expo (`apps/expo`):** + +- [ ] Install `@better-auth/expo`, `expo-secure-store`, `expo-linking`, `expo-web-browser`, `expo-constants` +- [ ] Create `apps/expo/lib/auth-client.ts`: + ```typescript + import { createAuthClient } from "better-auth/react"; + import { expoClient } from "@better-auth/expo/client"; + import { bearer } from "better-auth/plugins"; + + export const authClient = createAuthClient({ + baseURL: process.env.EXPO_PUBLIC_API_URL, + plugins: [ + expoClient({ scheme: "packrat", storagePrefix: "packrat" }), + bearer(), + ], + }); + ``` +- [ ] Fix dual-read bug: **`packrat.ts` and `authAtoms.ts` must both read through `authClient.getSession()`** — never via direct `Storage.getItem`. Jotai atoms derive from `authClient`: + ```typescript + export const sessionAtom = atom(async () => { + const { data } = await authClient.getSession(); + return data; + }); + ``` +- [ ] Add version-keyed migration gate on app launch to force re-auth for old-format tokens: + ```typescript + const authVersion = await Storage.getItem("auth_version"); + if (authVersion !== "v2") { + await Storage.removeItem("access_token"); + await Storage.removeItem("refresh_token"); + await Storage.setItem("auth_version", "v2"); + // navigate to login screen + } + ``` +- [ ] Handle 401 → graceful re-login: the `needsReauthAtom` must surface a re-login modal, not leave the app in a broken state +- [ ] Update `useAuthActions.ts` to call `authClient.signIn.email`, `authClient.signIn.social`, `authClient.signOut`, etc. +- [ ] Google Sign In: use `@react-native-google-signin/google-signin` native ID token flow: + ```typescript + const response = await GoogleSignin.signIn(); + if (isSuccessResponse(response) && response.data.idToken) { + await authClient.signIn.social({ + provider: "google", + idToken: { token: response.data.idToken }, + }); + } + ``` +- [ ] Apple Sign In: use the native `expo-apple-authentication` flow and pass `identityToken` to `authClient.signIn.social({ provider: "apple", idToken: { token } })` +- [ ] Test OTP email verification, forgot-password, and resend-verification against Better Auth endpoints + +**Next.js apps (`apps/admin`, `apps/guides`):** +- [ ] Add `better-auth/react` client +- [ ] Replace manual cookie/JWT handling with `authClient.getSession()` +- [ ] Verify `set-cookie` from Better Auth is accepted (cookie name, SameSite, Secure, domain) +- [ ] Update auth guards/middleware in both apps + +**Files:** +- `apps/expo/lib/auth-client.ts` *(new)* +- `apps/expo/lib/api/packrat.ts` *(remove direct Storage.getItem)* +- `apps/expo/features/auth/atoms/authAtoms.ts` *(derive from authClient)* +- `apps/expo/features/auth/hooks/useAuthActions.ts` +- `apps/expo/features/auth/hooks/useAuthInit.ts` +- `apps/admin/...`, `apps/guides/...` *(auth client + guard updates)* + +--- + +### Phase 5: Cutover + cleanup (1–2 days) + +**Prerequisite:** 7 days have elapsed since Phase 2 deploy. Legacy JWT hit rate in logs is 0%. Expo app update has shipped and adoption is sufficient (confirm in analytics). + +- [ ] Remove legacy JWT validation from dual-auth middleware +- [ ] Remove deprecated old auth routes (`/api/auth/login`, `/api/auth/register`, `/api/auth/google`, `/api/auth/apple`, `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/me`) +- [ ] Remove legacy auth utilities from `packages/api/src/utils/auth.ts`: `generateJWT`, `verifyJWT`, `generateRefreshToken`. **Keep:** `isValidApiKey` (admin X-API-Key), `hashPassword`/`verifyPassword` (still used by Better Auth config) +- [ ] Remove `PASSWORD_RESET_SECRET` from env schema if confirmed unused +- [ ] Drop legacy tables in follow-up migration `0039_drop_legacy_auth_tables.sql`: + - `DROP TABLE refresh_tokens` + - `DROP TABLE auth_providers` + - `DROP TABLE one_time_passwords` +- [ ] Remove old auth test files and replace with Better Auth-aware integration tests +- [ ] Update `packages/mcp/src/__tests__/auth.test.ts` to test OAuth 2.1 flow end-to-end +- [ ] Update `packages/api/test/auth.test.ts` to test Better Auth sign-in, social auth, OTP flows + +--- + +## System-Wide Impact + +### Interaction Graph + +``` +New sign-in (email): + → POST /api/auth/sign-in/email + → Better Auth validates password (bcrypt override) + → Writes session row to `session` table in Neon + → Returns opaque session token in `set-auth-token` header (bearer() plugin) + → Mobile stores token; subsequent requests: Authorization: Bearer + → getSession() looks up session in Neon on every API request + +MCP OAuth 2.1 flow: + → Client discovers mcp.packrat.world/.well-known/oauth-authorization-server + → Client POSTs to /register (with initial access token) + → Client opens browser to /authorize?code_challenge= + → MCP Worker redirects to api.packrat.world login page (Better Auth) + → User authenticates → Better Auth session issued + → Callback: MCP Worker verifies session via api.packrat.world/api/auth/get-session + → completeAuthorization({ userId }) → auth code → /token exchange + → MCP-scoped access token stored in MCP Worker's KV + → Tool calls: Bearer → validated from KV → forwarded to API +``` + +### Error & Failure Propagation + +- **Better Auth session lookup fails (DB down):** `getSession()` throws → dual-auth middleware must catch and return 503, not 401. Clients retrying on 503 vs 401 need different backoff behavior. Document this in client error handling. +- **JWKS key rotation mid-session (MCP):** Stale cached key causes JWT verification failure. On failure: re-fetch JWKS once (background `ctx.waitUntil`), retry verification. If still fails, return 401 with `error: "token_expired"` — client must re-authorize. +- **MCP access token expiry during agentic session:** Token expires at 60 min. MCP client must implement proactive refresh at 48 min (80% of lifetime) using the refresh token grant. If client doesn't implement refresh, tool calls fail at the 60-min mark with no recovery path — this is a known hard failure mode for long-running agentic sessions. +- **Better Auth migration script partial failure:** Use `INSERT ... ON CONFLICT DO NOTHING`. Run with `--dry-run` against a production DB snapshot first. Validate row counts before and after. Do not proceed to Phase 2 until 100% of users are migrated. +- **Legacy OTP in-flight at cutover:** A password-reset OTP requested before cutover lives in the legacy `one_time_passwords` table. Better Auth has no visibility into it. Schedule Phase 5 cutover during off-peak hours, announce 1-hour maintenance window, accept ≤15 min of broken OTP links (OTP TTL is 15 min). + +### State Lifecycle Risks + +| Risk | State stranded | Mitigation | +|---|---|---| +| Apple re-auth after migration | `email` / `name` not returned by Apple on 2nd+ auth | Cache Apple email into `account` record during migration script | +| Active OTP at Phase 5 cutover | OTP in `one_time_passwords` table, invisible to Better Auth | Schedule cutover at off-peak; OTP TTL is 15 min | +| Expo users with v1 tokens | `access_token` in expo-sqlite, invalid format for Better Auth | Version-gate migration in app launch sequence (Phase 4) | +| MCP pre-issued JWTs | Orphaned at Phase 5 cutover | Deprecation banner at Phase 3; force 401 at Phase 5 | +| Dual-read race condition | Two concurrent requests both hit legacy path, create duplicate sessions | KV atomic write for session promotion (compare-and-swap) | + +### API Surface Parity + +| Current endpoint | Better Auth equivalent | During overlap | +|---|---|---| +| `POST /api/auth/login` | `POST /api/auth/sign-in/email` | Keep alive | +| `POST /api/auth/register` | `POST /api/auth/sign-up/email` | Keep alive | +| `POST /api/auth/verify-email` | `POST /api/auth/verify-email` | Better Auth path takes over | +| `POST /api/auth/forgot-password` | `POST /api/auth/forget-password` (note spelling) | Keep old alive | +| `POST /api/auth/reset-password` | `POST /api/auth/reset-password` | Same path | +| `POST /api/auth/refresh` | Implicit via session lookup (no explicit endpoint) | Keep legacy alive | +| `POST /api/auth/logout` | `POST /api/auth/sign-out` | Keep old alive | +| `GET /api/auth/me` | `GET /api/auth/get-session` | Keep old alive | +| `POST /api/auth/google` | `POST /api/auth/sign-in/social?provider=google` | Keep old alive | +| `POST /api/auth/apple` | `POST /api/auth/sign-in/social?provider=apple` | Keep old alive + fix sig verification in Phase 0 | +| `DELETE /api/auth/` | `POST /api/auth/delete-user` | Keep old alive | +| `POST /api/auth/resend-verification` | `POST /api/auth/send-verification-email` | Keep old alive | +| X-API-Key admin routes | Unchanged — `apiKeyAuthPlugin` stays | No migration needed | + +### Integration Test Scenarios + +1. **Legacy JWT accepted during overlap:** Issue JWT via old `/api/auth/login`, call a protected route → should succeed via fallback path. Issue Better Auth session → should also succeed. Both paths in a single test run. +2. **Apple re-authentication:** Existing Apple user (migrated account row). Signs in via Apple again — Better Auth must match on `accountId` (Apple `sub`), not `email` (omitted by Apple on 2nd auth). +3. **MCP full OAuth 2.1 flow:** Simulate Claude Desktop: discover → register (with initial access token) → authorize (PKCE S256) → login via Better Auth → token exchange → MCP tool call → verify tool response. +4. **MCP token expiry mid-session:** Issue MCP token, advance time past 60-min lifetime, make a tool call → expect 401 with refresh token grant → refresh → retry succeeds. +5. **Parallel session collision:** Two concurrent requests with the same legacy token both hit the dual-auth shim simultaneously. One should promote to a Better Auth session; the second should not create a duplicate. + +--- + +## Security Summary + +Issues found during the deepening research phase, ordered by severity: + +| Finding | Severity | Phase to fix | +|---|---|---| +| Apple identity token not signature-verified (current prod bug) | **Critical** | Phase 0 (before anything else) | +| PKCE `plain` method must be rejected by oauthProvider plugin | **Critical** | Phase 3 — verify during config | +| Open dynamic client registration allows attacker-controlled redirect URIs | **High** | Phase 3 — require initial access token | +| Legacy Apple endpoint exploitable during parallel window if not fixed first | **High** | Phase 0 eliminates this | +| MCP token expires during long agentic sessions (60s default is 15 min) | **High** | Phase 3 — set to 60 min + proactive refresh | +| Timing oracle in dual-auth shim (Better Auth fast miss vs legacy slow DB lookup) | **Medium** | Phase 2 — normalize response timing | +| JWKS thundering herd on key rotation expiry | **Medium** | Phase 3 — stale-while-revalidate pattern | +| Duplicate session creation via concurrent legacy token promotion | **Medium** | Phase 2 — KV atomic write | + +--- + +## Acceptance Criteria + +### Functional +- [ ] Users can sign in with email/password, Google, and Apple without a password reset +- [ ] Apple identity token is signature-verified (not just base64-decoded) +- [ ] `GET /api/auth/jwks` returns a valid Ed25519 JWKS key set +- [ ] `GET mcp.packrat.world/.well-known/oauth-authorization-server` returns RFC 8414 metadata +- [ ] Claude Desktop and Cursor complete OAuth 2.1 + PKCE flow without manual token copy +- [ ] MCP tool calls succeed with OAuth-issued tokens +- [ ] Dynamic client registration requires an initial access token (not open) +- [ ] Admin `X-API-Key` routes work unchanged throughout all phases +- [ ] Legacy JWTs work for their remaining TTL during Phase 2–4 overlap +- [ ] After Phase 5, legacy JWTs are rejected with 401 + +### Non-Functional +- [ ] No forced password resets for existing users +- [ ] Session lookup adds < 10ms p99 latency (Neon with Hyperdrive connection pooling) +- [ ] MCP JWKS cache uses stale-while-revalidate (no hard TTL thundering herd) +- [ ] `nodejs_als` flag does not regress other Worker functionality (verify in staging) + +### Quality Gates +- [ ] All existing auth tests pass or are replaced with Better Auth-equivalent tests +- [ ] New integration test covers full OAuth 2.1 PKCE flow for MCP +- [ ] Apple re-auth tested with no-email response from Apple (second sign-in simulation) +- [ ] Dual-auth shim timing normalized — no measurable latency oracle between paths +- [ ] `APPLE_PRIVATE_KEY` and `BETTER_AUTH_SECRET` stored as Worker secrets (not committed) +- [ ] `PASSWORD_RESET_SECRET` usage audited and resolved + +--- + +## Dependencies & Prerequisites + +- **Apple credentials** — `.p8` private key, Team ID, Key ID from Apple Developer portal. Must be in place before Phase 0. +- **`nodejs_als` compatibility** — verify staging Worker builds and deploys successfully before production. +- **Expo release cadence** — Phase 4 Expo changes need a shipped release before Phase 5 cutover. Gate Phase 5 on sufficient app version adoption (target: ≥ 80% of DAU on v2). +- **Neon connection pool** — Better Auth makes more frequent session table reads than the old stateless JWT system. Verify connection pool size (Hyperdrive) is adequate for the added load. +- **Initial access token provisioning** — a mechanism to issue initial access tokens for MCP client registration must exist before Phase 3 (even a one-time admin script is fine). + +--- + +## Future Considerations + +Once stabilized: +- **2FA / TOTP:** Better Auth `twoFactor()` plugin — add after migration settles. +- **Passkeys:** Better Auth `passkey()` plugin — high-value for mobile. +- **MCP scopes:** Define PackRat-specific OAuth scopes (`trails:read`, `packs:write`, `trips:read`) for granular MCP agent permissions. +- **Organization auth:** Better Auth `organization()` plugin — relevant if PackRat adds team/family sharing features. + +--- + +## Sources & References + +### Internal References +- Apple auth bypass (current): `packages/api/src/routes/auth/index.ts:506–511` +- Auth utilities: `packages/api/src/utils/auth.ts` +- Auth middleware: `packages/api/src/middleware/auth.ts` +- DB schema: `packages/api/src/db/schema.ts:338` (trailOsmId / users / auth_providers / refresh_tokens) +- MCP auth extraction: `packages/mcp/src/index.ts:80–88` +- Expo token storage: `apps/expo/features/auth/atoms/authAtoms.ts` +- Expo token dual-read bug: `apps/expo/lib/api/packrat.ts` +- Env schema: `packages/api/src/utils/env-validation.ts`, `packages/env/src/node.ts` + +### External References +- [Better Auth — Cloudflare Workers integration](https://better-auth.com/docs/integrations/cloudflare) +- [Better Auth — Drizzle adapter](https://better-auth.com/docs/adapters/drizzle) +- [Better Auth — OAuth provider plugin](https://better-auth.com/docs/plugins/oauth-provider) +- [Better Auth — jwt() plugin](https://better-auth.com/docs/plugins/jwt) +- [Better Auth — bearer() plugin](https://better-auth.com/docs/plugins/bearer) +- [Better Auth — Expo / React Native client](https://better-auth.com/docs/installation) (react-native section) +- [Better Auth — Migration guides](https://better-auth.com/docs/guides) +- [Better Auth — bcrypt issue #5016](https://github.com/better-auth/better-auth/issues/5016) +- [better-auth-cloudflare package](https://github.com/zpg6/better-auth-cloudflare) +- [cloudflare/workers-oauth-provider](https://github.com/cloudflare/workers-oauth-provider) +- [Cloudflare — Build a Remote MCP server with OAuth](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) +- [Cloudflare — MCP Authorization](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) +- [MCP Authorization spec (2025-03-26)](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) +- [MCP Authorization spec (draft — RFC 9728 Protected Resource Metadata)](https://modelcontextprotocol.io/specification/draft/basic/authorization) +- [RFC 8414 — OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) +- [RFC 7591 — Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591) +- [RFC 9728 — OAuth 2.0 Protected Resource Metadata](https://www.rfc-editor.org/rfc/rfc9728) +- [Building an MCP server with OAuth + Cloudflare — Stytch](https://stytch.com/blog/building-an-mcp-server-oauth-cloudflare-workers/) diff --git a/docs/solutions/developer-experience/better-auth-cli-cloudflare-worker-factory-2026-05-02.md b/docs/solutions/developer-experience/better-auth-cli-cloudflare-worker-factory-2026-05-02.md new file mode 100644 index 0000000000..79cf808e09 --- /dev/null +++ b/docs/solutions/developer-experience/better-auth-cli-cloudflare-worker-factory-2026-05-02.md @@ -0,0 +1,127 @@ +--- +title: Better Auth CLI Requires Static Export — Add auth.config.ts for Worker Projects +date: 2026-05-02 +category: docs/solutions/developer-experience +module: Authentication +problem_type: developer_experience +component: authentication +severity: medium +applies_when: + - Using Better Auth in a Cloudflare Workers project + - Auth instance is created via a factory function that accepts a runtime env object + - Running bunx auth generate or bunx auth migrate to manage schema +tags: + - better-auth + - cloudflare-workers + - cli + - schema-generation + - factory-pattern + - auth-config +--- + +# Better Auth CLI Requires Static Export — Add auth.config.ts for Worker Projects + +## Context + +The Better Auth CLI (`bunx auth@latest generate`, `bunx auth migrate`) needs to import your auth config at parse time to read the schema. In a standard Node.js app, `auth.ts` exports a static `betterAuth({...})` instance — the CLI finds it immediately. + +In a Cloudflare Workers project, the auth instance is typically created per-request via a factory function that receives the live `env` object (KV bindings, secrets, DB URL, etc.). This makes the instance impossible for the CLI to construct: + +``` +[#better-auth]: Couldn't read your auth config. +[#better-auth]: Make sure to default export your auth instance or to export + as a variable named auth. +``` + +The factory pattern is correct for Workers — you cannot have a static singleton with live bindings. The CLI just needs a parallel static entry point. + +## Guidance + +Create a dedicated `src/auth/auth.config.ts` file that exports a static `auth` variable with stub env values. Point the CLI at it with `--config`. + +**The stub file mirrors the real config's schema exactly** (same plugins, same `additionalFields`, same table mappings) but uses hardcoded placeholder strings in place of secrets and URLs. No real connections are made during schema generation. + +```ts +// src/auth/auth.config.ts +// Static auth instance for the Better Auth CLI (bunx auth generate --config src/auth/auth.config.ts). +// The runtime instance in index.ts requires a live Cloudflare env object; +// the CLI cannot call that factory, so this file provides stub values. + +import { drizzleAdapter } from '@better-auth/drizzle-adapter'; +import { neon } from '@neondatabase/serverless'; +import * as schema from '@packrat/api/db/schema'; +import { betterAuth } from 'better-auth'; +import { admin, bearer, jwt } from 'better-auth/plugins'; +import { drizzle } from 'drizzle-orm/neon-http'; + +const db = drizzle(neon('postgresql://stub:stub@stub/stub'), { schema }); + +export const auth = betterAuth({ + baseURL: 'http://localhost:8787', + secret: 'cli-stub-secret', + database: drizzleAdapter(db, { + provider: 'pg', + schema: { + user: schema.users, + session: schema.session, + account: schema.account, + verification: schema.verification, + }, + }), + user: { + additionalFields: { + role: { type: 'string', defaultValue: 'USER' }, + firstName: { type: 'string', fieldName: 'first_name' }, + lastName: { type: 'string', fieldName: 'last_name' }, + avatarUrl: { type: 'string', fieldName: 'avatar_url' }, + passwordHash: { type: 'string', fieldName: 'password_hash' }, + }, + }, + emailAndPassword: { enabled: true }, + socialProviders: { google: { clientId: 'stub', clientSecret: 'stub' } }, + plugins: [bearer(), jwt(), admin()], +}); +``` + +Run the CLI from `packages/api/`: + +```bash +bunx auth@latest generate --config src/auth/auth.config.ts +``` + +## Why This Matters + +Without this file the CLI exits immediately with the error above and no migration SQL is produced. The `--config` flag is the official escape hatch Better Auth provides for non-standard project layouts — using it keeps the runtime factory pattern intact while unblocking schema generation. + +The stub DB URL (`postgresql://stub:stub@stub/stub`) works because `neon()` returns a lazy query function and `drizzle()` just wraps it — neither makes a network call at import time. The CLI only inspects the JS object graph, not the database. + +## When to Apply + +- Any time you need to run `bunx auth generate`, `bunx auth migrate`, or `bunx auth schema` against a Workers-based Better Auth project +- When onboarding a new developer who needs to regenerate the schema locally +- After adding new plugins or `additionalFields` to `index.ts` — update `auth.config.ts` to match, then re-run the CLI + +## Examples + +**Before** — running the CLI against the Workers factory file: + +```bash +cd packages/api +bunx auth@latest generate +# [#better-auth]: Couldn't read your auth config. +``` + +**After** — running the CLI against the static stub file: + +```bash +cd packages/api +bunx auth@latest generate --config src/auth/auth.config.ts +# ✓ Schema generated +``` + +**Keeping the two files in sync**: `auth.config.ts` must always mirror the schema-affecting parts of `index.ts` — `database` table mappings, `user.additionalFields`, and `plugins`. The stub file does not need to replicate operational concerns (rate limiting, secondary storage, Apple client-secret generation). + +## Related + +- [packages/api/src/auth/index.ts](packages/api/src/auth/index.ts) — runtime per-request factory +- [packages/api/src/auth/auth.config.ts](packages/api/src/auth/auth.config.ts) — static CLI stub diff --git a/package.json b/package.json index 57066b82b6..5b614dfb56 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "chalk": "^5.6.2", "elysia": "^1.4.0", "hono": "^4.10.7", + "magic-regexp": "^0.11.0", "radash": "^12.1.1", "react": "19.2.0", "react-dom": "19.2.0", @@ -116,9 +117,6 @@ "typescript": "~5.9.2", "zod": "^3.24.2" }, - "patchedDependencies": { - "@packrat-ai/nativewindui@1.1.0": "patches/@packrat-ai+nativewindui@1.1.0.patch" - }, "trustedDependencies": [ "@sentry/cli" ] diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 99d4480956..7359ea654a 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -13,7 +13,7 @@ "@packrat/env": "workspace:*", "@packrat/guards": "workspace:*", "consola": "^3.4.2", - "magic-regexp": "^0.11.0", + "magic-regexp": "catalog:", "radash": "catalog:", "zod": "catalog:" }, diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index bef70a70e3..216c35e93b 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -97,7 +97,27 @@ export function createApiClient(config: ApiClientConfig) { base: RequestInfo | URL, ): [RequestInfo | URL, RequestInit | undefined] => { if (!token) return [base, init]; - const headers = new Headers(init?.headers ?? {}); + const headers = new Headers(); + const existing = init?.headers; + if (existing instanceof Headers) { + existing.forEach((v, k) => { + headers.set(k, v); + }); + } else if (Array.isArray(existing)) { + for (const entry of existing) { + if ( + Array.isArray(entry) && + typeof entry[0] === 'string' && + typeof entry[1] === 'string' + ) { + headers.set(entry[0], entry[1]); + } + } + } else if (isObject(existing)) { + for (const [k, v] of Object.entries(existing)) { + if (isString(v)) headers.set(k, v); + } + } headers.set('Authorization', `Bearer ${token}`); return [base, { ...init, headers }]; }; diff --git a/packages/api/auth-schema.ts b/packages/api/auth-schema.ts new file mode 100644 index 0000000000..196dc5f4d3 --- /dev/null +++ b/packages/api/auth-schema.ts @@ -0,0 +1,110 @@ +import { relations } from 'drizzle-orm'; +import { boolean, index, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + +export const user = pgTable('user', { + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: boolean('email_verified').default(false).notNull(), + image: text('image'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + role: text('role').default('USER').notNull(), + banned: boolean('banned').default(false), + banReason: text('ban_reason'), + banExpires: timestamp('ban_expires'), + first_name: text('first_name').notNull(), + last_name: text('last_name').notNull(), + avatar_url: text('avatar_url').notNull(), + password_hash: text('password_hash').notNull(), +}); + +export const session = pgTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + impersonatedBy: text('impersonated_by'), + }, + (table) => [index('session_userId_idx').on(table.userId)], +); + +export const account = pgTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index('account_userId_idx').on(table.userId)], +); + +export const verification = pgTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index('verification_identifier_idx').on(table.identifier)], +); + +export const jwks = pgTable('jwks', { + id: text('id').primaryKey(), + publicKey: text('public_key').notNull(), + privateKey: text('private_key').notNull(), + createdAt: timestamp('created_at').notNull(), + expiresAt: timestamp('expires_at'), +}); + +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), +})); + +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id], + }), +})); + +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +})); diff --git a/packages/api/drizzle/0040_uuid_pk_better_auth_migration.sql b/packages/api/drizzle/0040_uuid_pk_better_auth_migration.sql new file mode 100644 index 0000000000..d5e8751431 --- /dev/null +++ b/packages/api/drizzle/0040_uuid_pk_better_auth_migration.sql @@ -0,0 +1,8 @@ +-- Migration: Replace integer PK on users with UUID, install Better Auth tables, +-- and drop legacy auth tables (auth_providers, refresh_tokens, one_time_passwords). + +-- ─── 1. EXTEND USERS TABLE ─────────────────────────────────────────────────── + +ALTER TABLE "users" ADD COLUMN "new_id" text;--> statement-breakpoint +UPDATE "users" SET "new_id" = gen_random_uuid()::text;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "new_id" SET NOT NULL; \ No newline at end of file diff --git a/packages/api/drizzle/0041_continue_uuid_migration.sql b/packages/api/drizzle/0041_continue_uuid_migration.sql new file mode 100644 index 0000000000..a1be9bdc19 --- /dev/null +++ b/packages/api/drizzle/0041_continue_uuid_migration.sql @@ -0,0 +1,57 @@ +-- Continue UUID migration: Add name column and create Better Auth tables + +ALTER TABLE "users" ADD COLUMN "name" text;--> statement-breakpoint +UPDATE "users" +SET "name" = TRIM(COALESCE("first_name", '') || ' ' || COALESCE("last_name", '')) +WHERE "first_name" IS NOT NULL OR "last_name" IS NOT NULL;--> statement-breakpoint +UPDATE "users" +SET "name" = SPLIT_PART("email", '@', 1) +WHERE "name" IS NULL OR "name" = '';--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "name" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "name" SET DEFAULT '';--> statement-breakpoint + +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL +);--> statement-breakpoint + +CREATE UNIQUE INDEX "session_token_idx" ON "session" ("token");--> statement-breakpoint + +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + UNIQUE ("provider_id", "account_id") +);--> statement-breakpoint + +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +);--> statement-breakpoint + +CREATE TABLE "jwks" ( + "id" text PRIMARY KEY NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "created_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/packages/api/drizzle/0042_migrate_auth_data.sql b/packages/api/drizzle/0042_migrate_auth_data.sql new file mode 100644 index 0000000000..d947d0b636 --- /dev/null +++ b/packages/api/drizzle/0042_migrate_auth_data.sql @@ -0,0 +1,40 @@ +-- Migrate auth data and add temp UUID columns for foreign keys + +-- Migrate credential (email+password) accounts +INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "password", "created_at", "updated_at") +SELECT + gen_random_uuid()::text, + u."new_id", + 'credential', + u."new_id", + u."password_hash", + u."created_at", + u."updated_at" +FROM "users" u +WHERE u."password_hash" IS NOT NULL +ON CONFLICT ("provider_id", "account_id") DO NOTHING;--> statement-breakpoint + +-- Migrate OAuth accounts from auth_providers (if table exists) +INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "created_at", "updated_at") +SELECT + gen_random_uuid()::text, + COALESCE(ap."provider_id", u."new_id"), + ap."provider", + u."new_id", + COALESCE(ap."created_at", u."created_at"), + COALESCE(ap."created_at", u."created_at") +FROM "auth_providers" ap +JOIN "users" u ON u."id" = ap."user_id" +WHERE ap."provider" != 'email' +ON CONFLICT ("provider_id", "account_id") DO NOTHING;--> statement-breakpoint + +-- Add temporary UUID columns to FK tables (using IF NOT EXISTS) +ALTER TABLE "packs" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "pack_items" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "weight_history" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "pack_templates" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "pack_template_items" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "trail_condition_reports" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "trips" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "reported_content" ADD COLUMN IF NOT EXISTS "user_uuid" text;--> statement-breakpoint +ALTER TABLE "reported_content" ADD COLUMN IF NOT EXISTS "reviewed_by_uuid" text; \ No newline at end of file diff --git a/packages/api/drizzle/0043_finalize_uuid_conversion.sql b/packages/api/drizzle/0043_finalize_uuid_conversion.sql new file mode 100644 index 0000000000..2ca75ccd93 --- /dev/null +++ b/packages/api/drizzle/0043_finalize_uuid_conversion.sql @@ -0,0 +1,23 @@ +-- Final step: Populate UUIDs, drop integer FKs, switch users.id to UUID + +-- Populate UUID columns with user UUIDs +UPDATE "packs" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "pack_items" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "weight_history" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "pack_templates" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "pack_template_items" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "trail_condition_reports" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "trips" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "reported_content" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id";--> statement-breakpoint +UPDATE "reported_content" t SET "reviewed_by_uuid" = u."new_id" FROM "users" u WHERE t."reviewed_by" = u."id";--> statement-breakpoint + +-- Drop foreign key constraints +ALTER TABLE "packs" DROP CONSTRAINT IF EXISTS "packs_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "pack_items" DROP CONSTRAINT IF EXISTS "pack_items_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "weight_history" DROP CONSTRAINT IF EXISTS "weight_history_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "pack_templates" DROP CONSTRAINT IF EXISTS "pack_templates_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "pack_template_items" DROP CONSTRAINT IF EXISTS "pack_template_items_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "trail_condition_reports" DROP CONSTRAINT IF EXISTS "trail_condition_reports_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "trips" DROP CONSTRAINT IF EXISTS "trips_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "reported_content" DROP CONSTRAINT IF EXISTS "reported_content_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "reported_content" DROP CONSTRAINT IF EXISTS "reported_content_reviewed_by_users_id_fk"; \ No newline at end of file diff --git a/packages/api/drizzle/0044_absurd_sir_ram.sql b/packages/api/drizzle/0044_absurd_sir_ram.sql new file mode 100644 index 0000000000..b28e5ad957 --- /dev/null +++ b/packages/api/drizzle/0044_absurd_sir_ram.sql @@ -0,0 +1,17 @@ +ALTER TABLE "users" ALTER COLUMN "name" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "session" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "session" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "verification" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "verification" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "verification" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint +ALTER TABLE "verification" ALTER COLUMN "updated_at" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "image" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "banned" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "ban_reason" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "ban_expires" timestamp;--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "impersonated_by" text;--> statement-breakpoint +CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier"); \ No newline at end of file diff --git a/packages/api/drizzle/0044_complete_uuid_conversion.sql b/packages/api/drizzle/0044_complete_uuid_conversion.sql new file mode 100644 index 0000000000..4f41b97b13 --- /dev/null +++ b/packages/api/drizzle/0044_complete_uuid_conversion.sql @@ -0,0 +1,33 @@ +-- Complete UUID conversion: drop integer columns, rename UUID columns, switch users.id + +-- Drop old integer user_id columns +ALTER TABLE "packs" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "pack_items" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "weight_history" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "pack_templates" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "pack_template_items" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "trail_condition_reports" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "trips" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "reported_content" DROP COLUMN "user_id";--> statement-breakpoint +ALTER TABLE "reported_content" DROP COLUMN "reviewed_by";--> statement-breakpoint + +-- Rename UUID columns to user_id +ALTER TABLE "packs" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "pack_items" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "weight_history" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "pack_templates" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "pack_template_items" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "trail_condition_reports" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "trips" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "reported_content" RENAME COLUMN "user_uuid" TO "user_id";--> statement-breakpoint +ALTER TABLE "reported_content" RENAME COLUMN "reviewed_by_uuid" TO "reviewed_by";--> statement-breakpoint + +-- Set NOT NULL on user_id columns (where required) +ALTER TABLE "packs" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "pack_items" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "weight_history" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "pack_templates" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "pack_template_items" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "trail_condition_reports" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "trips" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "reported_content" ALTER COLUMN "user_id" SET NOT NULL; \ No newline at end of file diff --git a/packages/api/drizzle/0045_finalize_users_uuid_pk.sql b/packages/api/drizzle/0045_finalize_users_uuid_pk.sql new file mode 100644 index 0000000000..37d4eee1ad --- /dev/null +++ b/packages/api/drizzle/0045_finalize_users_uuid_pk.sql @@ -0,0 +1,82 @@ +-- Final step: Switch users.id to UUID, add FK constraints, cleanup legacy tables + +-- First drop foreign key constraints from legacy tables +ALTER TABLE "auth_providers" DROP CONSTRAINT IF EXISTS "auth_providers_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "refresh_tokens" DROP CONSTRAINT IF EXISTS "refresh_tokens_user_id_users_id_fk";--> statement-breakpoint +ALTER TABLE "one_time_passwords" DROP CONSTRAINT IF EXISTS "one_time_passwords_user_id_users_id_fk";--> statement-breakpoint + +-- Migrate social feed tables to UUID user_id if they exist (skipped if never created) +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'posts') THEN + ALTER TABLE "posts" DROP CONSTRAINT IF EXISTS "posts_user_id_users_id_fk"; + ALTER TABLE "post_likes" DROP CONSTRAINT IF EXISTS "post_likes_user_id_users_id_fk"; + ALTER TABLE "post_comments" DROP CONSTRAINT IF EXISTS "post_comments_user_id_users_id_fk"; + ALTER TABLE "comment_likes" DROP CONSTRAINT IF EXISTS "comment_likes_user_id_users_id_fk"; + ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "user_uuid" text; + ALTER TABLE "post_likes" ADD COLUMN IF NOT EXISTS "user_uuid" text; + ALTER TABLE "post_comments" ADD COLUMN IF NOT EXISTS "user_uuid" text; + ALTER TABLE "comment_likes" ADD COLUMN IF NOT EXISTS "user_uuid" text; + UPDATE "posts" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id"; + UPDATE "post_likes" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id"; + UPDATE "post_comments" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id"; + UPDATE "comment_likes" t SET "user_uuid" = u."new_id" FROM "users" u WHERE t."user_id" = u."id"; + ALTER TABLE "posts" DROP COLUMN IF EXISTS "user_id"; + ALTER TABLE "post_likes" DROP COLUMN IF EXISTS "user_id"; + ALTER TABLE "post_comments" DROP COLUMN IF EXISTS "user_id"; + ALTER TABLE "comment_likes" DROP COLUMN IF EXISTS "user_id"; + ALTER TABLE "posts" RENAME COLUMN "user_uuid" TO "user_id"; + ALTER TABLE "post_likes" RENAME COLUMN "user_uuid" TO "user_id"; + ALTER TABLE "post_comments" RENAME COLUMN "user_uuid" TO "user_id"; + ALTER TABLE "comment_likes" RENAME COLUMN "user_uuid" TO "user_id"; + ALTER TABLE "posts" ALTER COLUMN "user_id" SET NOT NULL; + ALTER TABLE "post_likes" ALTER COLUMN "user_id" SET NOT NULL; + ALTER TABLE "post_comments" ALTER COLUMN "user_id" SET NOT NULL; + ALTER TABLE "comment_likes" ALTER COLUMN "user_id" SET NOT NULL; + END IF; +END $$;--> statement-breakpoint + +-- Switch users table primary key from integer to text UUID +ALTER TABLE "users" DROP CONSTRAINT "users_pkey";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "id";--> statement-breakpoint +ALTER TABLE "users" RENAME COLUMN "new_id" TO "id";--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id");--> statement-breakpoint + +-- Re-add foreign key constraints to Better Auth tables +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint + +-- Re-add foreign key constraints to application tables +ALTER TABLE "packs" ADD CONSTRAINT "packs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pack_items" ADD CONSTRAINT "pack_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "weight_history" ADD CONSTRAINT "weight_history_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pack_templates" ADD CONSTRAINT "pack_templates_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pack_template_items" ADD CONSTRAINT "pack_template_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "trail_condition_reports" ADD CONSTRAINT "trail_condition_reports_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "trips" ADD CONSTRAINT "trips_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reported_content" ADD CONSTRAINT "reported_content_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reported_content" ADD CONSTRAINT "reported_content_reviewed_by_users_id_fk" FOREIGN KEY ("reviewed_by") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint + +-- Re-add FK constraints to social feed tables only if they exist +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'posts') THEN + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'posts_user_id_users_id_fk') THEN + ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_likes_user_id_users_id_fk') THEN + ALTER TABLE "post_likes" ADD CONSTRAINT "post_likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_comments_user_id_users_id_fk') THEN + ALTER TABLE "post_comments" ADD CONSTRAINT "post_comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'comment_likes_user_id_users_id_fk') THEN + ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + END IF; +END $$;--> statement-breakpoint + +-- Drop legacy auth tables +DROP TABLE "auth_providers" CASCADE;--> statement-breakpoint +DROP TABLE "refresh_tokens" CASCADE;--> statement-breakpoint +DROP TABLE "one_time_passwords" CASCADE; diff --git a/packages/api/drizzle/0046_social_feed_tables_uuid.sql b/packages/api/drizzle/0046_social_feed_tables_uuid.sql new file mode 100644 index 0000000000..1c068cf95e --- /dev/null +++ b/packages/api/drizzle/0046_social_feed_tables_uuid.sql @@ -0,0 +1,68 @@ +-- Create social feed tables with UUID user_id if they were never previously migrated + +CREATE TABLE IF NOT EXISTS "posts" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "caption" text, + "images" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "post_likes" ( + "id" serial PRIMARY KEY NOT NULL, + "post_id" integer NOT NULL, + "user_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "post_likes_post_id_user_id_unique" UNIQUE("post_id","user_id") +);--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "post_comments" ( + "id" serial PRIMARY KEY NOT NULL, + "post_id" integer NOT NULL, + "user_id" text NOT NULL, + "content" text NOT NULL, + "parent_comment_id" integer, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +);--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "comment_likes" ( + "id" serial PRIMARY KEY NOT NULL, + "comment_id" integer NOT NULL, + "user_id" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "comment_likes_comment_id_user_id_unique" UNIQUE("comment_id","user_id") +);--> statement-breakpoint +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'posts_user_id_users_id_fk') THEN + ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_likes_post_id_posts_id_fk') THEN + ALTER TABLE "post_likes" ADD CONSTRAINT "post_likes_post_id_posts_id_fk" + FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_likes_user_id_users_id_fk') THEN + ALTER TABLE "post_likes" ADD CONSTRAINT "post_likes_user_id_users_id_fk" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_comments_post_id_posts_id_fk') THEN + ALTER TABLE "post_comments" ADD CONSTRAINT "post_comments_post_id_posts_id_fk" + FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_comments_user_id_users_id_fk') THEN + ALTER TABLE "post_comments" ADD CONSTRAINT "post_comments_user_id_users_id_fk" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'post_comments_parent_comment_id_post_comments_id_fk') THEN + ALTER TABLE "post_comments" ADD CONSTRAINT "post_comments_parent_comment_id_post_comments_id_fk" + FOREIGN KEY ("parent_comment_id") REFERENCES "post_comments"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'comment_likes_comment_id_post_comments_id_fk') THEN + ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_comment_id_post_comments_id_fk" + FOREIGN KEY ("comment_id") REFERENCES "post_comments"("id") ON DELETE cascade ON UPDATE no action; + END IF; + IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'comment_likes_user_id_users_id_fk') THEN + ALTER TABLE "comment_likes" ADD CONSTRAINT "comment_likes_user_id_users_id_fk" + FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$; diff --git a/packages/api/drizzle/meta/0037_snapshot.json b/packages/api/drizzle/meta/0037_snapshot.json index 7dfc17736d..3a724051d4 100644 --- a/packages/api/drizzle/meta/0037_snapshot.json +++ b/packages/api/drizzle/meta/0037_snapshot.json @@ -1,5 +1,5 @@ { - "id": "a7c5105c-b819-43ec-a26c-5f5a84dc86ba", + "id": "52580680-191b-458b-86f6-96d255a92062", "prevId": "fa3d18d1-67a7-488a-aba5-5b18295e80f2", "version": "7", "dialect": "postgresql", @@ -1788,286 +1788,6 @@ "policies": {}, "checkConstraints": {}, "isRLSEnabled": false - }, - "public.posts": { - "name": "posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "caption": { - "name": "caption", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "images": { - "name": "images", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "posts_user_id_users_id_fk": { - "name": "posts_user_id_users_id_fk", - "tableFrom": "posts", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.post_likes": { - "name": "post_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "post_likes_post_id_posts_id_fk": { - "name": "post_likes_post_id_posts_id_fk", - "tableFrom": "post_likes", - "tableTo": "posts", - "schemaTo": "public", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_likes_user_id_users_id_fk": { - "name": "post_likes_user_id_users_id_fk", - "tableFrom": "post_likes", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "post_likes_post_id_user_id_unique": { - "name": "post_likes_post_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["post_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.post_comments": { - "name": "post_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_comment_id": { - "name": "parent_comment_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "post_comments_post_id_posts_id_fk": { - "name": "post_comments_post_id_posts_id_fk", - "tableFrom": "post_comments", - "tableTo": "posts", - "schemaTo": "public", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_user_id_users_id_fk": { - "name": "post_comments_user_id_users_id_fk", - "tableFrom": "post_comments", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_parent_comment_id_post_comments_id_fk": { - "name": "post_comments_parent_comment_id_post_comments_id_fk", - "tableFrom": "post_comments", - "tableTo": "post_comments", - "schemaTo": "public", - "columnsFrom": ["parent_comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.comment_likes": { - "name": "comment_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "comment_id": { - "name": "comment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "comment_likes_comment_id_post_comments_id_fk": { - "name": "comment_likes_comment_id_post_comments_id_fk", - "tableFrom": "comment_likes", - "tableTo": "post_comments", - "schemaTo": "public", - "columnsFrom": ["comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "comment_likes_user_id_users_id_fk": { - "name": "comment_likes_user_id_users_id_fk", - "tableFrom": "comment_likes", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "comment_likes_comment_id_user_id_unique": { - "name": "comment_likes_comment_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["comment_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false } }, "enums": {}, diff --git a/packages/api/drizzle/meta/0038_snapshot.json b/packages/api/drizzle/meta/0038_snapshot.json index f624f918d7..b1a48df33e 100644 --- a/packages/api/drizzle/meta/0038_snapshot.json +++ b/packages/api/drizzle/meta/0038_snapshot.json @@ -1,6 +1,6 @@ { - "id": "dce94129-f491-41b1-9d2c-e22dcf72101e", - "prevId": "a7c5105c-b819-43ec-a26c-5f5a84dc86ba", + "id": "a08154b1-28c9-4757-a8e3-26a98182d7e2", + "prevId": "52580680-191b-458b-86f6-96d255a92062", "version": "7", "dialect": "postgresql", "tables": { @@ -1788,286 +1788,6 @@ "policies": {}, "checkConstraints": {}, "isRLSEnabled": false - }, - "public.posts": { - "name": "posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "caption": { - "name": "caption", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "images": { - "name": "images", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "posts_user_id_users_id_fk": { - "name": "posts_user_id_users_id_fk", - "tableFrom": "posts", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.post_likes": { - "name": "post_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "post_likes_post_id_posts_id_fk": { - "name": "post_likes_post_id_posts_id_fk", - "tableFrom": "post_likes", - "tableTo": "posts", - "schemaTo": "public", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_likes_user_id_users_id_fk": { - "name": "post_likes_user_id_users_id_fk", - "tableFrom": "post_likes", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "post_likes_post_id_user_id_unique": { - "name": "post_likes_post_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["post_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.post_comments": { - "name": "post_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_comment_id": { - "name": "parent_comment_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "post_comments_post_id_posts_id_fk": { - "name": "post_comments_post_id_posts_id_fk", - "tableFrom": "post_comments", - "tableTo": "posts", - "schemaTo": "public", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_user_id_users_id_fk": { - "name": "post_comments_user_id_users_id_fk", - "tableFrom": "post_comments", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_parent_comment_id_post_comments_id_fk": { - "name": "post_comments_parent_comment_id_post_comments_id_fk", - "tableFrom": "post_comments", - "tableTo": "post_comments", - "schemaTo": "public", - "columnsFrom": ["parent_comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.comment_likes": { - "name": "comment_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "comment_id": { - "name": "comment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "comment_likes_comment_id_post_comments_id_fk": { - "name": "comment_likes_comment_id_post_comments_id_fk", - "tableFrom": "comment_likes", - "tableTo": "post_comments", - "schemaTo": "public", - "columnsFrom": ["comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "comment_likes_user_id_users_id_fk": { - "name": "comment_likes_user_id_users_id_fk", - "tableFrom": "comment_likes", - "tableTo": "users", - "schemaTo": "public", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "comment_likes_comment_id_user_id_unique": { - "name": "comment_likes_comment_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["comment_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false } }, "enums": {}, diff --git a/packages/api/drizzle/meta/0039_snapshot.json b/packages/api/drizzle/meta/0039_snapshot.json index 4982183617..4a3de2b62b 100644 --- a/packages/api/drizzle/meta/0039_snapshot.json +++ b/packages/api/drizzle/meta/0039_snapshot.json @@ -1,6 +1,6 @@ { - "id": "acca2b7a-650e-4e23-83f3-47b309d37e6b", - "prevId": "dce94129-f491-41b1-9d2c-e22dcf72101e", + "id": "8ca2e099-15ca-4088-b4f6-5eeb66305236", + "prevId": "a08154b1-28c9-4757-a8e3-26a98182d7e2", "version": "7", "dialect": "postgresql", "tables": { @@ -340,69 +340,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.comment_likes": { - "name": "comment_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "comment_id": { - "name": "comment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "comment_likes_comment_id_post_comments_id_fk": { - "name": "comment_likes_comment_id_post_comments_id_fk", - "tableFrom": "comment_likes", - "tableTo": "post_comments", - "columnsFrom": ["comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "comment_likes_user_id_users_id_fk": { - "name": "comment_likes_user_id_users_id_fk", - "tableFrom": "comment_likes", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "comment_likes_comment_id_user_id_unique": { - "name": "comment_likes_comment_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["comment_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.etl_jobs": { "name": "etl_jobs", "schema": "", @@ -705,12 +642,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "is_ai_generated": { "name": "is_ai_generated", "type": "boolean", @@ -905,12 +836,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "created_at": { "name": "created_at", "type": "timestamp", @@ -1022,12 +947,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "content_source": { "name": "content_source", "type": "text", @@ -1220,12 +1139,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "is_ai_generated": { "name": "is_ai_generated", "type": "boolean", @@ -1287,227 +1200,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.post_comments": { - "name": "post_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_comment_id": { - "name": "parent_comment_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "post_comments_post_id_posts_id_fk": { - "name": "post_comments_post_id_posts_id_fk", - "tableFrom": "post_comments", - "tableTo": "posts", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_user_id_users_id_fk": { - "name": "post_comments_user_id_users_id_fk", - "tableFrom": "post_comments", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_comments_parent_comment_id_post_comments_id_fk": { - "name": "post_comments_parent_comment_id_post_comments_id_fk", - "tableFrom": "post_comments", - "tableTo": "post_comments", - "columnsFrom": ["parent_comment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.post_likes": { - "name": "post_likes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "post_id": { - "name": "post_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "post_likes_post_id_posts_id_fk": { - "name": "post_likes_post_id_posts_id_fk", - "tableFrom": "post_likes", - "tableTo": "posts", - "columnsFrom": ["post_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "post_likes_user_id_users_id_fk": { - "name": "post_likes_user_id_users_id_fk", - "tableFrom": "post_likes", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "post_likes_post_id_user_id_unique": { - "name": "post_likes_post_id_user_id_unique", - "nullsNotDistinct": false, - "columns": ["post_id", "user_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.posts": { - "name": "posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "caption": { - "name": "caption", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "images": { - "name": "images", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_user_id_users_id_fk": { - "name": "posts_user_id_users_id_fk", - "tableFrom": "posts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.refresh_tokens": { "name": "refresh_tokens", "schema": "", @@ -1767,12 +1459,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "local_created_at": { "name": "local_created_at", "type": "timestamp", @@ -1953,12 +1639,6 @@ "primaryKey": false, "notNull": false }, - "trail_osm_id": { - "name": "trail_osm_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, "local_created_at": { "name": "local_created_at", "type": "timestamp", @@ -1978,12 +1658,6 @@ "notNull": true, "default": false }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "created_at": { "name": "created_at", "type": "timestamp", @@ -1997,6 +1671,12 @@ "primaryKey": false, "notNull": true, "default": "now()" + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false } }, "indexes": {}, @@ -2093,18 +1773,6 @@ "primaryKey": false, "notNull": false, "default": "now()" - }, - "last_active_at": { - "name": "last_active_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false } }, "indexes": {}, diff --git a/packages/api/drizzle/meta/0040_snapshot.json b/packages/api/drizzle/meta/0040_snapshot.json new file mode 100644 index 0000000000..50ebfe8c3f --- /dev/null +++ b/packages/api/drizzle/meta/0040_snapshot.json @@ -0,0 +1,1804 @@ +{ + "id": "e6e0d6b1-c96d-4ce4-90e6-dd2303a17f38", + "prevId": "8ca2e099-15ca-4088-b4f6-5eeb66305236", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_providers": { + "name": "auth_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_providers_user_id_users_id_fk": { + "name": "auth_providers_user_id_users_id_fk", + "tableFrom": "auth_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.one_time_passwords": { + "name": "one_time_passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_passwords_user_id_users_id_fk": { + "name": "one_time_passwords_user_id_users_id_fk", + "tableFrom": "one_time_passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token": { + "name": "replaced_by_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/0041_snapshot.json b/packages/api/drizzle/meta/0041_snapshot.json new file mode 100644 index 0000000000..7811313d0e --- /dev/null +++ b/packages/api/drizzle/meta/0041_snapshot.json @@ -0,0 +1,1804 @@ +{ + "id": "c7407e9f-d059-4c8c-9a5d-3bb23aef916a", + "prevId": "e6e0d6b1-c96d-4ce4-90e6-dd2303a17f38", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_providers": { + "name": "auth_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_providers_user_id_users_id_fk": { + "name": "auth_providers_user_id_users_id_fk", + "tableFrom": "auth_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.one_time_passwords": { + "name": "one_time_passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_passwords_user_id_users_id_fk": { + "name": "one_time_passwords_user_id_users_id_fk", + "tableFrom": "one_time_passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token": { + "name": "replaced_by_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/0042_snapshot.json b/packages/api/drizzle/meta/0042_snapshot.json new file mode 100644 index 0000000000..5e0e762b07 --- /dev/null +++ b/packages/api/drizzle/meta/0042_snapshot.json @@ -0,0 +1,1804 @@ +{ + "id": "9d1f9671-0ceb-4946-af2a-78b79e45b4af", + "prevId": "c7407e9f-d059-4c8c-9a5d-3bb23aef916a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_providers": { + "name": "auth_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_providers_user_id_users_id_fk": { + "name": "auth_providers_user_id_users_id_fk", + "tableFrom": "auth_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.one_time_passwords": { + "name": "one_time_passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_passwords_user_id_users_id_fk": { + "name": "one_time_passwords_user_id_users_id_fk", + "tableFrom": "one_time_passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token": { + "name": "replaced_by_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/0043_snapshot.json b/packages/api/drizzle/meta/0043_snapshot.json new file mode 100644 index 0000000000..88fa979c59 --- /dev/null +++ b/packages/api/drizzle/meta/0043_snapshot.json @@ -0,0 +1,2173 @@ +{ + "id": "2ea3c5a1-4e51-433b-b855-c021bdc23d96", + "prevId": "9d1f9671-0ceb-4946-af2a-78b79e45b4af", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_users_id_fk": { + "name": "session_user_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_users_id_fk": { + "name": "account_user_id_users_id_fk", + "tableFrom": "account", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_provider_account_idx": { + "name": "account_provider_account_idx", + "nullsNotDistinct": false, + "columns": ["provider_id", "account_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_likes": { + "name": "post_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_likes_post_id_posts_id_fk": { + "name": "post_likes_post_id_posts_id_fk", + "tableFrom": "post_likes", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_likes_user_id_users_id_fk": { + "name": "post_likes_user_id_users_id_fk", + "tableFrom": "post_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_likes_post_id_user_id_unique": { + "name": "post_likes_post_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["post_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_comments": { + "name": "post_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_comments_post_id_posts_id_fk": { + "name": "post_comments_post_id_posts_id_fk", + "tableFrom": "post_comments", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_user_id_users_id_fk": { + "name": "post_comments_user_id_users_id_fk", + "tableFrom": "post_comments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_parent_comment_id_post_comments_id_fk": { + "name": "post_comments_parent_comment_id_post_comments_id_fk", + "tableFrom": "post_comments", + "tableTo": "post_comments", + "columnsFrom": ["parent_comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_likes": { + "name": "comment_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_likes_comment_id_post_comments_id_fk": { + "name": "comment_likes_comment_id_post_comments_id_fk", + "tableFrom": "comment_likes", + "tableTo": "post_comments", + "columnsFrom": ["comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_likes_user_id_users_id_fk": { + "name": "comment_likes_user_id_users_id_fk", + "tableFrom": "comment_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comment_likes_comment_id_user_id_unique": { + "name": "comment_likes_comment_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["comment_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/0044_snapshot.json b/packages/api/drizzle/meta/0044_snapshot.json new file mode 100644 index 0000000000..77e53213dd --- /dev/null +++ b/packages/api/drizzle/meta/0044_snapshot.json @@ -0,0 +1,2257 @@ +{ + "id": "548299d6-dc62-4a37-893b-932e6b7451a1", + "prevId": "2ea3c5a1-4e51-433b-b855-c021bdc23d96", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_users_id_fk": { + "name": "account_user_id_users_id_fk", + "tableFrom": "account", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_provider_account_idx": { + "name": "account_provider_account_idx", + "nullsNotDistinct": false, + "columns": ["provider_id", "account_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_likes": { + "name": "comment_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_likes_comment_id_post_comments_id_fk": { + "name": "comment_likes_comment_id_post_comments_id_fk", + "tableFrom": "comment_likes", + "tableTo": "post_comments", + "columnsFrom": ["comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_likes_user_id_users_id_fk": { + "name": "comment_likes_user_id_users_id_fk", + "tableFrom": "comment_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comment_likes_comment_id_user_id_unique": { + "name": "comment_likes_comment_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["comment_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_comments": { + "name": "post_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_comments_post_id_posts_id_fk": { + "name": "post_comments_post_id_posts_id_fk", + "tableFrom": "post_comments", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_user_id_users_id_fk": { + "name": "post_comments_user_id_users_id_fk", + "tableFrom": "post_comments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_parent_comment_id_post_comments_id_fk": { + "name": "post_comments_parent_comment_id_post_comments_id_fk", + "tableFrom": "post_comments", + "tableTo": "post_comments", + "columnsFrom": ["parent_comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_likes": { + "name": "post_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_likes_post_id_posts_id_fk": { + "name": "post_likes_post_id_posts_id_fk", + "tableFrom": "post_likes", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_likes_user_id_users_id_fk": { + "name": "post_likes_user_id_users_id_fk", + "tableFrom": "post_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_likes_post_id_user_id_unique": { + "name": "post_likes_post_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["post_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_users_id_fk": { + "name": "session_user_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USER'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index 0107445106..af35b27774 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -278,15 +278,57 @@ { "idx": 38, "version": "7", - "when": 1777637285430, - "tag": "0038_broad_winter_soldier", + "when": 1777256400000, + "tag": "0040_uuid_pk_better_auth_migration", "breakpoints": true }, { "idx": 39, "version": "7", - "when": 1777637471970, - "tag": "0039_worried_wrecking_crew", + "when": 1777256460000, + "tag": "0041_continue_uuid_migration", + "breakpoints": true + }, + { + "idx": 40, + "version": "7", + "when": 1777256520000, + "tag": "0042_migrate_auth_data", + "breakpoints": true + }, + { + "idx": 41, + "version": "7", + "when": 1777256580000, + "tag": "0043_finalize_uuid_conversion", + "breakpoints": true + }, + { + "idx": 42, + "version": "7", + "when": 1777256640000, + "tag": "0044_complete_uuid_conversion", + "breakpoints": true + }, + { + "idx": 43, + "version": "7", + "when": 1777256700000, + "tag": "0045_finalize_users_uuid_pk", + "breakpoints": true + }, + { + "idx": 44, + "version": "7", + "when": 1777717813481, + "tag": "0044_absurd_sir_ram", + "breakpoints": true + }, + { + "idx": 45, + "version": "7", + "when": 1777803600000, + "tag": "0046_social_feed_tables_uuid", "breakpoints": true } ] diff --git a/packages/api/package.json b/packages/api/package.json index ee38c7f99b..e3c2b44cac 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -69,12 +69,15 @@ "zod-openapi": "^5.4.6" }, "devDependencies": { + "@better-auth/drizzle-adapter": "^1.6.9", "@cloudflare/vitest-pool-workers": "0.8.71", "@cloudflare/workers-types": "^4.20250405.0", "@types/bun": "latest", "@types/pg": "^8.11.15", "@types/ws": "^8.5.14", "@vitest/coverage-v8": "~3.1.4", + "better-auth": "^1.6.9", + "better-auth-cloudflare": "^0.3.0", "concurrently": "^8.2.2", "drizzle-orm": "^0.45.2", "typed-htmx": "^0.3.1", diff --git a/packages/api/src/auth/auth.config.ts b/packages/api/src/auth/auth.config.ts new file mode 100644 index 0000000000..e8f21594b3 --- /dev/null +++ b/packages/api/src/auth/auth.config.ts @@ -0,0 +1,75 @@ +/** + * Static auth config for the Better Auth CLI (`bunx auth generate`). + * + * The real auth instance is created per-request in index.ts because it needs + * a live Cloudflare Worker env object. The CLI cannot call that factory, so + * this file exports a static instance with stub values — enough for the CLI to + * read the schema and generate migrations without a real DB connection. + * + * Usage: + * bunx auth generate --config src/auth/auth.config.ts + */ + +import { drizzleAdapter } from '@better-auth/drizzle-adapter'; +import { neon } from '@neondatabase/serverless'; +import * as schema from '@packrat/api/db/schema'; +import { betterAuth } from 'better-auth'; +import { admin, bearer, jwt } from 'better-auth/plugins'; +import { drizzle } from 'drizzle-orm/neon-http'; + +const db = drizzle(neon('postgresql://stub:stub@stub/stub'), { schema }); + +export const auth = betterAuth({ + baseURL: 'http://localhost:8787', + secret: 'cli-stub-secret', + + advanced: { + generateId: () => crypto.randomUUID(), + ipAddress: { + ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'], + }, + crossSubDomainCookies: { enabled: false }, + }, + + database: drizzleAdapter(db, { + provider: 'pg', + schema: { + user: schema.users, + session: schema.session, + account: schema.account, + verification: schema.verification, + }, + }), + + user: { + additionalFields: { + role: { type: 'string', defaultValue: 'USER' }, + firstName: { type: 'string', fieldName: 'first_name' }, + lastName: { type: 'string', fieldName: 'last_name' }, + avatarUrl: { type: 'string', fieldName: 'avatar_url' }, + passwordHash: { type: 'string', fieldName: 'password_hash' }, + }, + }, + + emailAndPassword: { + enabled: true, + autoSignIn: true, + minPasswordLength: 8, + requireEmailVerification: false, + }, + + emailVerification: { + sendVerificationEmail: async () => {}, + }, + + socialProviders: { + google: { + clientId: 'stub', + clientSecret: 'stub', + }, + }, + + plugins: [bearer(), jwt(), admin()], + + trustedOrigins: ['http://localhost:8787', 'packrat://'], +}); diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts new file mode 100644 index 0000000000..7c488119c4 --- /dev/null +++ b/packages/api/src/auth/index.ts @@ -0,0 +1,210 @@ +/** + * Better Auth configuration for the PackRat API Worker. + * + * getAuth(env) is called per-request so each isolate invocation picks up the + * correct KV binding, credentials, and DB connection. The result is cached + * in a WeakMap keyed by the raw env object so the instance is reused across + * requests within the same isolate lifetime. + */ + +import { drizzleAdapter } from '@better-auth/drizzle-adapter'; +import { expo } from '@better-auth/expo'; +import { verifyPassword } from '@better-auth/utils/password'; +import { neon } from '@neondatabase/serverless'; +import * as schema from '@packrat/api/db/schema'; +import type { ValidatedEnv } from '@packrat/api/utils/env-validation'; +import * as bcrypt from 'bcryptjs'; +import { betterAuth } from 'better-auth'; +import { admin, bearer, jwt } from 'better-auth/plugins'; +import { drizzle } from 'drizzle-orm/neon-http'; +import { importPKCS8, SignJWT } from 'jose'; + +// Matches bcrypt hashes ($2a$, $2b$, $2y$) left over from pre-migration auth. +const BCRYPT_HASH_RE = /^\$2[aby]\$/; + +async function verifyPasswordCompat({ + hash, + password, +}: { + hash: string; + password: string; +}): Promise { + if (BCRYPT_HASH_RE.test(hash)) { + return bcrypt.compare(password, hash); + } + return verifyPassword(hash, password); +} + +// ─── Apple client-secret generation ────────────────────────────────────────── +// Apple requires a JWT as the OAuth2 client secret. It is valid for up to +// 6 months, so we regenerate it once per isolate (WeakMap cache below +// handles the per-request dedup). +// Returns null when Apple credentials are not configured (e.g., in tests). +async function generateAppleClientSecret(env: ValidatedEnv): Promise { + if (!env.APPLE_PRIVATE_KEY) return null; + try { + const privateKey = await importPKCS8(env.APPLE_PRIVATE_KEY, 'ES256'); + const now = Math.floor(Date.now() / 1000); + return await new SignJWT({}) + .setProtectedHeader({ alg: 'ES256', kid: env.APPLE_KEY_ID }) + .setIssuer(env.APPLE_TEAM_ID) + .setSubject(env.APPLE_CLIENT_ID) + .setAudience('https://appleid.apple.com') + .setIssuedAt(now) + .setExpirationTime(now + 60 * 60 * 24 * 180) // 180 days + .sign(privateKey); + } catch (err) { + // Malformed or placeholder key — log so the issue is visible, then fall + // through so the provider is still registered for the native id-token flow + // (which verifies against Apple's public JWKS and does not use this secret). + console.warn( + '[auth] Apple client-secret generation failed; web OAuth flow will be unavailable:', + err, + ); + return null; + } +} + +// ─── Per-isolate auth instance cache ───────────────────────────────────────── +// biome-ignore lint/suspicious/noExplicitAny: Better Auth's generic type parameter is too specific to the exact plugin set — can't use ReturnType here +const authCache = new WeakMap(); + +// biome-ignore lint/suspicious/noExplicitAny: Better Auth instance type is plugin-specific and can't be expressed at declaration time without duplicating the full config signature +export async function getAuth(env: ValidatedEnv): Promise { + const cached = authCache.get(env as object); + if (cached) return cached; + + const appleClientSecret = await generateAppleClientSecret(env); + + // Use the HTTP Neon driver — no long-lived connections inside a Worker. + const db = drizzle(neon(env.NEON_DATABASE_URL), { schema }); + + const auth = betterAuth({ + baseURL: env.BETTER_AUTH_URL, + secret: env.BETTER_AUTH_SECRET, + + advanced: { + // All IDs are UUID-formatted text (matching the DB migration). + generateId: () => crypto.randomUUID(), + // Trust the X-Forwarded-For header added by Cloudflare. + ipAddress: { + ipAddressHeaders: ['cf-connecting-ip', 'x-forwarded-for'], + }, + // Disable cross-site cookies so the Bearer plugin is the primary + // session mechanism for mobile/API clients. + crossSubDomainCookies: { enabled: false }, + }, + + // Use KV as a fast secondary store for session lookups. + secondaryStorage: env.AUTH_KV + ? { + get: async (key: string) => env.AUTH_KV.get(key), + // biome-ignore lint/complexity/useMaxParams: Better Auth secondaryStorage.set interface requires 3 params + set: async (key: string, value: string, ttl?: number) => { + await env.AUTH_KV.put(key, value, ttl ? { expirationTtl: ttl } : undefined); + }, + delete: async (key: string) => env.AUTH_KV.delete(key), + } + : undefined, + + database: drizzleAdapter(db, { + provider: 'pg', + schema: { + user: schema.users, + session: schema.session, + account: schema.account, + verification: schema.verification, + jwks: schema.jwks, + }, + }), + + // Map Better Auth's model field names to our column names. + user: { + additionalFields: { + role: { type: 'string', defaultValue: 'USER' }, + firstName: { type: 'string', fieldName: 'first_name' }, + lastName: { type: 'string', fieldName: 'last_name' }, + avatarUrl: { type: 'string', fieldName: 'avatar_url' }, + passwordHash: { type: 'string', fieldName: 'password_hash' }, + }, + }, + + emailAndPassword: { + enabled: true, + autoSignIn: true, + minPasswordLength: 8, + requireEmailVerification: false, + password: { + verify: verifyPasswordCompat, + }, + }, + + emailVerification: { + sendVerificationEmail: async ({ user, url }) => { + // Email sending is handled separately via the email service. + // Log for now; wire up in the email integration task. + console.log(`[auth] email verification for ${user.email}: ${url}`); + }, + }, + + socialProviders: { + google: { + clientId: env.GOOGLE_CLIENT_ID ?? '', + clientSecret: env.GOOGLE_CLIENT_SECRET ?? '', + }, + // Always register Apple when clientId is present so the native id-token + // flow works even without a valid client-secret JWT (the id-token path + // verifies against Apple's public JWKS; the secret is only used for the + // web OAuth redirect flow). + // audience covers all EAS build variants — Apple puts the bundle ID in + // the `aud` claim, which differs per variant (.dev, .preview, base). + ...(env.APPLE_CLIENT_ID + ? { + apple: { + clientId: env.APPLE_CLIENT_ID, + clientSecret: appleClientSecret ?? 'native-id-token-only', + appBundleIdentifier: env.APPLE_CLIENT_ID, + audience: [ + env.APPLE_CLIENT_ID, + `${env.APPLE_CLIENT_ID}.dev`, + `${env.APPLE_CLIENT_ID}.preview`, + ], + }, + } + : {}), + }, + + plugins: [ + // Bearer: converts Authorization: Bearer into a session cookie + // transparently so existing mobile/API clients keep working. + bearer(), + + // JWT: issues asymmetric JWTs and exposes a JWKS endpoint at + // /api/auth/jwks for downstream service verification. + jwt(), + + // Admin: role-based user management endpoints. + admin(), + + // Expo: promotes the expo-origin header → Origin so the CSRF check + // passes for requests from the native app (which can't send a browser + // Origin header). + expo(), + ], + + rateLimit: { + enabled: true, + window: 60, + max: 100, + storage: 'secondary-storage', + }, + + trustedOrigins: [env.BETTER_AUTH_URL, 'packrat://'], + }); + + authCache.set(env as object, auth); + return auth; +} + +export type Auth = Awaited>; +export type Session = Auth['$Infer']['Session']; diff --git a/packages/api/src/db/schema.ts b/packages/api/src/db/schema.ts index ab2578a1f3..d59a4ff314 100644 --- a/packages/api/src/db/schema.ts +++ b/packages/api/src/db/schema.ts @@ -14,7 +14,6 @@ import { text, timestamp, unique, - varchar, vector, } from 'drizzle-orm/pg-core'; import type { ValidationError } from '../types/validation'; @@ -23,53 +22,88 @@ const availabilityEnum = pgEnum('availability', ['in_stock', 'out_of_stock', 'pr // User table export const users = pgTable('users', { - id: serial('id').primaryKey(), + id: text('id').primaryKey(), + name: text('name').notNull(), email: text('email').unique().notNull(), - emailVerified: boolean('email_verified').default(false), - passwordHash: text('password_hash'), + emailVerified: boolean('email_verified').default(false).notNull(), + image: text('image'), + role: text('role').default('USER').notNull(), + banned: boolean('banned').default(false), + banReason: text('ban_reason'), + banExpires: timestamp('ban_expires'), firstName: text('first_name'), lastName: text('last_name'), avatarUrl: text('avatar_url'), - role: text('role').default('USER'), // 'USER', 'ADMIN' - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at').defaultNow(), - lastActiveAt: timestamp('last_active_at'), - deletedAt: timestamp('deleted_at'), + passwordHash: text('password_hash'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), }); -// Authentication providers table -export const authProviders = pgTable('auth_providers', { - id: serial('id').primaryKey(), - userId: integer('user_id') - .references(() => users.id) - .notNull(), - provider: text('provider').notNull(), // 'email', 'google', 'apple' - providerId: text('provider_id'), // ID from the provider - createdAt: timestamp('created_at').defaultNow(), -}); +// Better Auth — session table +export const session = pgTable( + 'session', + { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + impersonatedBy: text('impersonated_by'), + }, + (table) => [index('session_userId_idx').on(table.userId)], +); -// Refresh tokens table -export const refreshTokens = pgTable('refresh_tokens', { - id: serial('id').primaryKey(), - userId: integer('user_id') - .references(() => users.id) - .notNull(), - token: text('token').notNull().unique(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - revokedAt: timestamp('revoked_at'), - replacedByToken: text('replaced_by_token'), -}); +// Better Auth — account table (OAuth + credential provider) +export const account = pgTable( + 'account', + { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + }, + (t) => [ + unique('account_provider_account_idx').on(t.providerId, t.accountId), + index('account_userId_idx').on(t.userId), + ], +); -// One-time password table -export const oneTimePasswords = pgTable('one_time_passwords', { - id: serial('id').primaryKey(), - userId: integer('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - code: varchar('code', { length: 6 }).notNull(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), +// Better Auth — verification table (email/OTP verification tokens) +export const verification = pgTable( + 'verification', + { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), + }, + (table) => [index('verification_identifier_idx').on(table.identifier)], +); + +// Better Auth — jwks table (asymmetric JWT key pairs for jwt() plugin) +export const jwks = pgTable('jwks', { + id: text('id').primaryKey(), + publicKey: text('public_key').notNull(), + privateKey: text('private_key').notNull(), + createdAt: timestamp('created_at').notNull(), }); // Packs table @@ -78,7 +112,7 @@ export const packs = pgTable('packs', { name: text('name').notNull(), description: text('description'), category: text('category').notNull().$type(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), templateId: text('template_id').references(() => packTemplates.id), @@ -87,7 +121,6 @@ export const packs = pgTable('packs', { image: text('image'), tags: jsonb('tags').$type(), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), isAIGenerated: boolean('is_ai_generated').notNull().default(false), localCreatedAt: timestamp('local_created_at').notNull(), localUpdatedAt: timestamp('local_updated_at').notNull(), @@ -207,11 +240,10 @@ export const packItems = pgTable( .references(() => packs.id, { onDelete: 'cascade' }) .notNull(), catalogItemId: integer('catalog_item_id').references(() => catalogItems.id), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id) .notNull(), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), isAIGenerated: boolean('is_ai_generated').notNull().default(false), templateItemId: text('template_item_id').references(() => packTemplateItems.id), embedding: vector('embedding', { dimensions: 1536 }), @@ -228,7 +260,7 @@ export const packItems = pgTable( export const packWeightHistory = pgTable('weight_history', { id: text('id').primaryKey(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), packId: text('pack_id') @@ -245,14 +277,13 @@ export const packTemplates = pgTable('pack_templates', { name: text('name').notNull(), description: text('description'), category: text('category').notNull(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), image: text('image'), tags: jsonb('tags').$type(), isAppTemplate: boolean('is_app_template').notNull().default(false), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), contentSource: text('content_source'), contentId: text('content_id'), @@ -280,11 +311,10 @@ export const packTemplateItems = pgTable('pack_template_items', { .references(() => packTemplates.id, { onDelete: 'cascade' }) .notNull(), catalogItemId: integer('catalog_item_id').references(() => catalogItems.id), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id) .notNull(), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), @@ -304,12 +334,11 @@ export const trailConditionReports = pgTable( waterCrossingDifficulty: text('water_crossing_difficulty'), // easy | moderate | difficult notes: text('notes'), photos: jsonb('photos').$type().notNull().default([]), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), tripId: text('trip_id').references(() => trips.id, { onDelete: 'set null' }), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), localCreatedAt: timestamp('local_created_at').notNull(), localUpdatedAt: timestamp('local_updated_at').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), @@ -338,7 +367,7 @@ export const trips = pgTable('trips', { endDate: timestamp('end_date'), location: jsonb('location').$type<{ latitude: number; longitude: number; name?: string }>(), notes: text('notes'), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id) .notNull(), packId: text('pack_id').references(() => packs.id, { onDelete: 'set null' }), @@ -346,7 +375,6 @@ export const trips = pgTable('trips', { localCreatedAt: timestamp('local_created_at').notNull(), localUpdatedAt: timestamp('local_updated_at').notNull(), deleted: boolean('deleted').notNull().default(false), - deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); @@ -402,7 +430,7 @@ export const tripsRelations = relations(trips, ({ one }) => ({ // Reported content table export const reportedContent = pgTable('reported_content', { id: serial('id').primaryKey(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id) .notNull(), userQuery: text('user_query').notNull(), @@ -411,7 +439,7 @@ export const reportedContent = pgTable('reported_content', { userComment: text('user_comment'), status: text('status').default('pending').notNull(), // pending, reviewed, dismissed reviewed: boolean('reviewed').default(false), - reviewedBy: integer('reviewed_by').references(() => users.id), + reviewedBy: text('reviewed_by').references(() => users.id), reviewedAt: timestamp('reviewed_at'), createdAt: timestamp('created_at').defaultNow().notNull(), }); @@ -526,14 +554,17 @@ export const catalogItemEtlJobsRelations = relations(catalogItemEtlJobs, ({ one export type User = InferSelectModel; export type NewUser = InferInsertModel; -export type AuthProvider = InferSelectModel; -export type NewAuthProvider = InferInsertModel; +export type Session = InferSelectModel; +export type NewSession = InferInsertModel; + +export type Account = InferSelectModel; +export type NewAccount = InferInsertModel; -export type RefreshToken = InferSelectModel; -export type NewRefreshToken = InferInsertModel; +export type Verification = InferSelectModel; +export type NewVerification = InferInsertModel; -export type OneTimePassword = InferSelectModel; -export type NewOneTimePassword = InferInsertModel; +export type Jwks = InferSelectModel; +export type NewJwks = InferInsertModel; export type Pack = InferSelectModel; export type PackWithItems = Pack & { @@ -582,14 +613,13 @@ export type PackTemplateWithItems = PackTemplate & { // Posts table export const posts = pgTable('posts', { id: serial('id').primaryKey(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), caption: text('caption'), images: jsonb('images').$type().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), - deletedAt: timestamp('deleted_at'), }); // Post likes table @@ -600,7 +630,7 @@ export const postLikes = pgTable( postId: integer('post_id') .references(() => posts.id, { onDelete: 'cascade' }) .notNull(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), @@ -616,7 +646,7 @@ export const postComments = pgTable('post_comments', { postId: integer('post_id') .references(() => posts.id, { onDelete: 'cascade' }) .notNull(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), content: text('content').notNull(), @@ -625,7 +655,6 @@ export const postComments = pgTable('post_comments', { }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), - deletedAt: timestamp('deleted_at'), }); // Comment likes table @@ -636,7 +665,7 @@ export const commentLikes = pgTable( commentId: integer('comment_id') .references(() => postComments.id, { onDelete: 'cascade' }) .notNull(), - userId: integer('user_id') + userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), diff --git a/packages/api/src/db/seed-e2e-user.ts b/packages/api/src/db/seed-e2e-user.ts index 31721389e0..0a7a23dcbe 100644 --- a/packages/api/src/db/seed-e2e-user.ts +++ b/packages/api/src/db/seed-e2e-user.ts @@ -68,13 +68,15 @@ async function seedE2EUser() { if (existingUser) { await db .update(schema.users) - .set({ passwordHash, emailVerified: true, deletedAt: null, updatedAt: new Date() }) + .set({ passwordHash, emailVerified: true, updatedAt: new Date() }) .where(eq(schema.users.id, existingUser.id)); console.log(`E2E user refreshed: ${normalizedEmail} (id=${existingUser.id})`); } else { const [inserted] = await db .insert(schema.users) .values({ + id: crypto.randomUUID(), + name: 'E2E Automation', email: normalizedEmail, passwordHash, emailVerified: true, diff --git a/packages/api/src/db/seed.ts b/packages/api/src/db/seed.ts index f42034c5f3..2efae017c0 100644 --- a/packages/api/src/db/seed.ts +++ b/packages/api/src/db/seed.ts @@ -1831,14 +1831,8 @@ async function seed() { const dbUrl = nodeEnv.NEON_DATABASE_URL; if (!dbUrl) throw new Error('NEON_DATABASE_URL is required'); - // Get optional admin user ID from CLI args - const argUserIdRaw = process.argv[2] ? Number.parseInt(process.argv[2], 10) : undefined; - if (argUserIdRaw !== undefined && Number.isNaN(argUserIdRaw)) { - throw new Error( - 'Invalid user ID provided. Please provide a valid numeric user ID, e.g.: bun run seed.ts 1', - ); - } - const argUserId = argUserIdRaw; + // Get optional admin user ID from CLI args (now a UUID string) + const argUserId = process.argv[2] ?? undefined; type SeedDatabase = NodePgDatabase | NeonHttpDatabase; @@ -1860,7 +1854,7 @@ async function seed() { const seedDb = db; // Resolve admin user ID - let adminUserId: number; + let adminUserId: string; if (argUserId) { adminUserId = argUserId; console.log(`Using provided user ID: ${adminUserId}`); diff --git a/packages/api/src/db/zod-schemas.ts b/packages/api/src/db/zod-schemas.ts index d577e873b0..fcf00731bf 100644 --- a/packages/api/src/db/zod-schemas.ts +++ b/packages/api/src/db/zod-schemas.ts @@ -1,17 +1,14 @@ import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { - authProviders, catalogItemEtlJobs, catalogItems, etlJobs, invalidItemLogs, - oneTimePasswords, packItems, packs, packTemplateItems, packTemplates, packWeightHistory, - refreshTokens, reportedContent, users, } from './schema'; @@ -20,14 +17,6 @@ import { export const selectUserSchema = createSelectSchema(users); export const insertUserSchema = createInsertSchema(users); -// Auth schemas -export const selectAuthProviderSchema = createSelectSchema(authProviders); -export const insertAuthProviderSchema = createInsertSchema(authProviders); -export const selectRefreshTokenSchema = createSelectSchema(refreshTokens); -export const insertRefreshTokenSchema = createInsertSchema(refreshTokens); -export const selectOneTimePasswordSchema = createSelectSchema(oneTimePasswords); -export const insertOneTimePasswordSchema = createInsertSchema(oneTimePasswords); - // Pack schemas export const selectPackSchema = createSelectSchema(packs); export const insertPackSchema = createInsertSchema(packs); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8b6e6c32cb..0ff3fa9edd 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,41 +2,44 @@ * Cloudflare Worker entry point. * * Elysia-based Worker using the official `CloudflareAdapter` (Elysia 1.4.x). - * Every route is Elysia-native so Eden Treaty gets full end-to-end type - * safety and @elysiajs/openapi generates a complete OpenAPI/Scalar UI. + * Better Auth handles all /api/auth/** requests; all other routes are + * Elysia-native so Eden Treaty gets full end-to-end type safety. */ import type { MessageBatch } from '@cloudflare/workers-types'; import { cors } from '@elysiajs/cors'; +import { getAuth } from '@packrat/api/auth'; import { AppContainer } from '@packrat/api/containers'; import { routes } from '@packrat/api/routes'; import { CatalogService } from '@packrat/api/services'; import { processQueueBatch } from '@packrat/api/services/etl/queue'; import type { Env } from '@packrat/api/types/env'; -import { setWorkerEnv } from '@packrat/api/utils/env-validation'; +import { getEnv, setWorkerEnv } from '@packrat/api/utils/env-validation'; import { packratOpenApi } from '@packrat/api/utils/openapi'; import { Elysia } from 'elysia'; import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'; import type { CatalogETLMessage } from './services/etl/types'; -/** - * Root Elysia application – exported so Eden Treaty can infer the full route - * surface. - */ export const app = new Elysia({ adapter: CloudflareAdapter }) .use( cors({ - // Reflect-origin is intentional: PackRat has many consumer origins - // (Expo dev, web, landing, admin panel, preview deploys) and maintaining - // an explicit allowlist would be operational overhead. Bearer-token auth - // (not cookies) limits the CSRF blast radius; revisit if cookie-auth - // is ever added. - // - // Reflect the requested headers rather than hard-coding an allowlist so - // clients can send browser/Sentry/feature-flag headers without requiring - // a server deploy. Matches dev's bare `cors()` behavior. - credentials: false, - allowedHeaders: '*', + // Better Auth uses cookies — credentials must be true and origins must + // be explicit (not wildcard) so the browser sends cookies cross-origin. + credentials: true, + origin: (request) => { + const origin = request.headers.get('Origin'); + if (!origin) return false; + // Allow the API base URL and any subdomain of packrat.world + const allowed = [ + /^https:\/\/(www\.)?packrat\.world$/, + /^https:\/\/[\w-]+\.packrat\.world$/, + /^http:\/\/localhost:\d+$/, + /^exp:\/\//, + ]; + return allowed.some((re) => re.test(origin)); + }, + allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], }), ) .use(packratOpenApi) @@ -68,10 +71,6 @@ export const app = new Elysia({ adapter: CloudflareAdapter }) .use(routes) .compile(); -/** - * End-to-end type exported for the Eden Treaty client (see - * `apps/expo/lib/api/client.ts`). - */ export type App = typeof app; export { AppContainer }; @@ -83,8 +82,6 @@ type CfFetchFn = ( ) => Response | Promise; function enrichEnv(env: Env): Env { - // If the OSM Hyperdrive binding is present, use its connection string so - // createOsmDb() routes through Hyperdrive instead of a plain env var URL. if (env.OSM_HYPERDRIVE) { return { ...env, OSM_DATABASE_URL: env.OSM_HYPERDRIVE.connectionString }; } @@ -92,28 +89,32 @@ function enrichEnv(env: Env): Env { } export default { - fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const e = enrichEnv(env); - setWorkerEnv(e as unknown as Record); // safe-cast: Cloudflare Worker entry point — env is a plain bindings object at runtime - return (app.fetch as unknown as CfFetchFn)(request, e, ctx); // safe-cast: Elysia's fetch matches the CfFetchFn signature at runtime; unknown intermediate required for variance + setWorkerEnv(e as unknown as Record); // safe-cast: setWorkerEnv accepts Record; ValidatedEnv has no index signature by design + + // Route /api/auth/** to Better Auth before Elysia sees it. + const url = new URL(request.url); + if (url.pathname.startsWith('/api/auth')) { + const validatedEnv = getEnv(); + const auth = await getAuth(validatedEnv); + return auth.handler(request); + } + + return (app.fetch as unknown as CfFetchFn)(request, e, ctx); // safe-cast: Elysia's fetch has Cloudflare-specific env/ctx params not in the standard type }, + async queue(batch: MessageBatch, env: Env): Promise { - setWorkerEnv(enrichEnv(env) as unknown as Record); // safe-cast: Cloudflare Worker entry point — env is a plain bindings object at runtime + setWorkerEnv(enrichEnv(env) as unknown as Record); // safe-cast: same as fetch handler above if (batch.queue === 'packrat-etl-queue' || batch.queue === 'packrat-etl-queue-dev') { - if (!env.ETL_QUEUE) { - throw new Error('ETL_QUEUE is not configured'); - } - // The queue name check above is the runtime guard; the Worker runtime delivers - // correctly-typed messages for this queue binding. - await processQueueBatch({ batch: batch as MessageBatch, env }); // safe-cast: queue name guard above confirms this batch carries CatalogETLMessage payloads + if (!env.ETL_QUEUE) throw new Error('ETL_QUEUE is not configured'); + await processQueueBatch({ batch: batch as MessageBatch, env }); // safe-cast: batch queue name checked above; MessageBatch is compatible at runtime } else if ( batch.queue === 'packrat-embeddings-queue' || batch.queue === 'packrat-embeddings-queue-dev' ) { - if (!env.EMBEDDINGS_QUEUE) { - throw new Error('EMBEDDINGS_QUEUE is not configured'); - } + if (!env.EMBEDDINGS_QUEUE) throw new Error('EMBEDDINGS_QUEUE is not configured'); await new CatalogService(env, true).handleEmbeddingsBatch(batch); } else { throw new Error(`Unknown queue: ${batch.queue}`); diff --git a/packages/api/src/middleware/auth.ts b/packages/api/src/middleware/auth.ts index 0211f0c96b..507378091b 100644 --- a/packages/api/src/middleware/auth.ts +++ b/packages/api/src/middleware/auth.ts @@ -1,65 +1,37 @@ -import { createDb } from '@packrat/api/db'; -import { users } from '@packrat/api/db/schema'; -import { isValidApiKey, verifyJWT } from '@packrat/api/utils/auth'; -import { and, eq, isNull, lt, or } from 'drizzle-orm'; +import { getAuth } from '@packrat/api/auth'; +import { isValidApiKey } from '@packrat/api/utils/auth'; +import type { ValidatedEnv } from '@packrat/api/utils/env-validation'; +import { getEnv } from '@packrat/api/utils/env-validation'; import { Elysia, status } from 'elysia'; export type AuthUser = { - userId: number; - role: 'USER' | 'ADMIN'; - [key: string]: unknown; + userId: string; + role: string; + email: string; + name: string; }; /** - * Elysia macro that enforces Bearer-JWT authentication on a route. Routes - * that need server-to-server API-key access must use `apiKeyAuthPlugin` - * explicitly; this macro does not accept `X-API-Key` to avoid synthesizing - * a user identity for a route that expects a real user. + * Elysia macro that enforces Better Auth session authentication. + * + * Accepts both cookie-based sessions and Bearer token sessions (via the + * bearer() plugin). Sets `user` in the request context for downstream routes. */ export const authPlugin = new Elysia({ name: 'packrat-auth' }).macro({ isAuthenticated: { resolve: async ({ request }: { request: Request }) => { - const authHeader = request.headers.get('authorization'); - if (!authHeader) return status(401, { error: 'Unauthorized' }); + const env = getEnv() as ValidatedEnv; // safe-cast: Worker env validated at startup; TS can't narrow the return type + const auth = await getAuth(env); + const session = await auth.api.getSession({ headers: request.headers }); + if (!session) return status(401, { error: 'Unauthorized' }); - const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; - if (!token) return status(401, { error: 'No token provided' }); - - const payload = await verifyJWT({ token }); - if (!payload) return status(401, { error: 'Invalid token' }); - - const uid = Number(payload.userId); - if (!Number.isFinite(uid) || uid <= 0) return status(401, { error: 'Unauthorized' }); - - const db = createDb(); - - // Reject soft-deleted accounts even when their JWT is still valid. - const dbUser = await db.query.users.findFirst({ - columns: { id: true, deletedAt: true }, - where: eq(users.id, uid), - }); - if (!dbUser || dbUser.deletedAt) return status(401, { error: 'Unauthorized' }); - - // Fire-and-forget: update last_active_at at most once per 5 min per user - const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); - db.update(users) - .set({ lastActiveAt: new Date() }) - .where( - and( - eq(users.id, uid), - isNull(users.deletedAt), - or(isNull(users.lastActiveAt), lt(users.lastActiveAt, fiveMinutesAgo)), - ), - ) - .catch(() => {}); - - const { userId: _uid, role: _role, ...rest } = payload; return { user: { - userId: uid, - role: (payload.role as 'USER' | 'ADMIN') ?? 'USER', - ...rest, - } as AuthUser, // safe-cast: JWT payload validated by auth middleware — userId and role fields are confirmed present + userId: session.user.id, + role: (session.user as unknown as { role?: string }).role ?? 'USER', + email: session.user.email, + name: session.user.name, + }, }; }, }, @@ -71,22 +43,21 @@ export const authPlugin = new Elysia({ name: 'packrat-auth' }).macro({ export const adminAuthPlugin = new Elysia({ name: 'packrat-admin-auth' }).use(authPlugin).macro({ isAdmin: { resolve: async ({ request }: { request: Request }) => { - const authHeader = request.headers.get('authorization'); - if (!authHeader) return status(401, { error: 'Unauthorized' }); - const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; - if (!token) return status(401, { error: 'Unauthorized' }); - const payload = await verifyJWT({ token }); - if (!payload) return status(401, { error: 'Unauthorized' }); - if (payload.role !== 'ADMIN') { - return status(403, { error: 'Forbidden' }); - } - const { userId: _uid, role: _role, ...rest } = payload; + const env = getEnv() as ValidatedEnv; // safe-cast: Worker env validated at startup; TS can't narrow the return type + const auth = await getAuth(env); + const session = await auth.api.getSession({ headers: request.headers }); + if (!session) return status(401, { error: 'Unauthorized' }); + + const role = (session.user as unknown as { role?: string }).role; + if (role !== 'ADMIN') return status(403, { error: 'Forbidden' }); + return { user: { - userId: Number(payload.userId), + userId: session.user.id, role: 'ADMIN' as const, - ...rest, - } as AuthUser, // safe-cast: JWT payload validated by auth middleware — userId and ADMIN role confirmed present + email: session.user.email, + name: session.user.name, + }, }; }, }, diff --git a/packages/api/src/routes/admin/analytics/platform.ts b/packages/api/src/routes/admin/analytics/platform.ts index b877917f9c..4b3f35bae1 100644 --- a/packages/api/src/routes/admin/analytics/platform.ts +++ b/packages/api/src/routes/admin/analytics/platform.ts @@ -14,7 +14,7 @@ import { BreakdownItemSchema, GrowthPointSchema, } from '@packrat/api/schemas/admin'; -import { and, count, desc, eq, gte, isNull, sql } from 'drizzle-orm'; +import { and, count, desc, eq, gte, sql } from 'drizzle-orm'; import { Elysia, status, t } from 'elysia'; import { z } from 'zod'; @@ -56,7 +56,7 @@ export const platformAnalyticsRoutes = new Elysia({ prefix: '/platform' }) count: count(), }) .from(users) - .where(and(isNull(users.deletedAt), gte(users.createdAt, startDate))) + .where(gte(users.createdAt, startDate)) .groupBy(sql`date_trunc(${sql.raw(`'${period}'`)}, ${users.createdAt})`) .orderBy(sql`date_trunc(${sql.raw(`'${period}'`)}, ${users.createdAt})`), db @@ -191,33 +191,12 @@ export const platformAnalyticsRoutes = new Elysia({ prefix: '/platform' }) .get( '/active-users', async () => { - const db = createDb(); - try { - const now = new Date(); - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); - const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - - const [dau, wau, mau] = await Promise.all([ - db - .select({ count: count() }) - .from(users) - .where(and(isNull(users.deletedAt), gte(users.lastActiveAt, oneDayAgo))), - db - .select({ count: count() }) - .from(users) - .where(and(isNull(users.deletedAt), gte(users.lastActiveAt, sevenDaysAgo))), - db - .select({ count: count() }) - .from(users) - .where(and(isNull(users.deletedAt), gte(users.lastActiveAt, thirtyDaysAgo))), - ]); - + // Note: Better Auth users don't have lastActiveAt field - tracking requires separate implementation return { - dau: dau[0]?.count ?? 0, - wau: wau[0]?.count ?? 0, - mau: mau[0]?.count ?? 0, + dau: 0, // Requires lastActiveAt tracking + wau: 0, // Requires lastActiveAt tracking + mau: 0, // Requires lastActiveAt tracking }; } catch (error) { console.error('Analytics active-users error:', error); diff --git a/packages/api/src/routes/admin/index.ts b/packages/api/src/routes/admin/index.ts index 8950eacf23..2afa59854d 100644 --- a/packages/api/src/routes/admin/index.ts +++ b/packages/api/src/routes/admin/index.ts @@ -15,7 +15,7 @@ import { import { timingSafeEqual } from '@packrat/api/utils/auth'; import { getEnv } from '@packrat/api/utils/env-validation'; import { assertAllDefined } from '@packrat/guards'; -import { and, count, desc, eq, ilike, isNull, or, sql } from 'drizzle-orm'; +import { and, count, desc, eq, ilike, or, sql } from 'drizzle-orm'; import { Elysia, status } from 'elysia'; import { jwtVerify, SignJWT } from 'jose'; import { z } from 'zod'; @@ -48,7 +48,7 @@ function basicAuthGuard(request: Request): { authorized: true } | { authorized: async function issueAdminJwt(username: string): Promise { const env = getEnv(); - const secret = new TextEncoder().encode(env.JWT_SECRET); + const secret = new TextEncoder().encode(env.BETTER_AUTH_SECRET); return new SignJWT({ role: 'admin' }) .setProtectedHeader({ alg: 'HS256' }) .setSubject(username) @@ -62,7 +62,7 @@ async function issueAdminJwt(username: string): Promise { async function verifyAdminJwt(token: string): Promise { try { const env = getEnv(); - const secret = new TextEncoder().encode(env.JWT_SECRET); + const secret = new TextEncoder().encode(env.BETTER_AUTH_SECRET); const { payload } = await jwtVerify(token, secret, { issuer: ADMIN_JWT_ISSUER, audience: ADMIN_JWT_AUDIENCE, @@ -73,9 +73,10 @@ async function verifyAdminJwt(token: string): Promise { } } -// Protected routes: Bearer JWT only. -// The JWT is issued by /token, which already enforced both factors (CF JWT + Basic -// in prod, Basic-only in local dev). No need to re-check CF or Basic here. +// Protected routes: Bearer JWT is always accepted. +// When CF Access is configured, CF JWT is also accepted directly (the CF edge +// injects Cf-Access-Jwt-Assertion on every request, so the user has already +// passed the CF Access gate). Basic auth is accepted only in local dev. async function adminAuthGuard(request: Request): Promise { const env = getEnv(); const { CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD } = env; @@ -83,6 +84,15 @@ async function adminAuthGuard(request: Request): Promise { if (header.startsWith('Bearer ')) return verifyAdminJwt(header.slice(7)); + // When CF Access is configured, verify the CF JWT injected by the CF edge. + if (CF_ACCESS_TEAM_DOMAIN && CF_ACCESS_AUD) { + const cfIdentity = await verifyCFAccessRequest(request, { + teamDomain: CF_ACCESS_TEAM_DOMAIN, + aud: CF_ACCESS_AUD, + }); + if (cfIdentity) return true; + } + // Local dev only: allow Basic auth directly on protected routes as a convenience. // Both CF vars absent AND non-production environment must hold — missing CF vars // alone is not enough so a misconfigured prod cannot fall back to Basic auth. @@ -172,10 +182,7 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) async () => { const db = createDb(); try { - const [userCount] = await db - .select({ count: count() }) - .from(users) - .where(isNull(users.deletedAt)); + const [userCount] = await db.select({ count: count() }).from(users); const [packCount] = await db .select({ count: count() }) .from(packs) @@ -209,7 +216,6 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) const limit = Number(query.limit ?? 100); const offset = Number(query.offset ?? 0); const search = query.q; - const includeDeleted = query.includeDeleted === 'true'; const searchFilter = search ? or( @@ -219,11 +225,7 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) ) : undefined; - const deletedFilter = includeDeleted ? undefined : isNull(users.deletedAt); - const whereClause = - searchFilter && deletedFilter - ? and(deletedFilter, searchFilter) - : (deletedFilter ?? searchFilter); + const whereClause = searchFilter; const [usersList, [totalRow]] = await Promise.all([ db @@ -235,8 +237,6 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) role: users.role, emailVerified: users.emailVerified, createdAt: users.createdAt, - lastActiveAt: users.lastActiveAt, - deletedAt: users.deletedAt, }) .from(users) .where(whereClause) @@ -250,8 +250,6 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) data: usersList.map((u) => ({ ...u, createdAt: u.createdAt?.toISOString() ?? null, - lastActiveAt: u.lastActiveAt?.toISOString() ?? null, - deletedAt: u.deletedAt?.toISOString() ?? null, })), total: totalRow?.count ?? 0, limit, @@ -308,7 +306,6 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) category: packs.category, isPublic: packs.isPublic, deleted: packs.deleted, - deletedAt: packs.deletedAt, createdAt: packs.createdAt, userEmail: users.email, }) @@ -329,7 +326,6 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) data: packsList.map((p) => ({ ...p, createdAt: p.createdAt?.toISOString() ?? null, - deletedAt: p.deletedAt?.toISOString() ?? null, })), total: totalRow?.count ?? 0, limit, @@ -419,21 +415,12 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) .delete( '/users/:id', async ({ params }) => { - const id = Number(params.id); - if (!Number.isFinite(id) || id <= 0) return status(400, { error: 'Invalid user id' }); - const db = createDb(); - try { - const updated = await db - .update(users) - .set({ deletedAt: new Date() }) - .where(and(eq(users.id, id), isNull(users.deletedAt))) - .returning(); - if (!updated.length) return status(404, { error: 'User not found or already deleted' }); - return { success: true as const }; - } catch (error) { - console.error('Error soft-deleting user:', error); - return status(500, { error: 'Failed to delete user' }); - } + const id = params.id; + if (!id) return status(400, { error: 'Invalid user id' }); + // Soft delete not supported for users in Better Auth - use hard delete or ban instead + return status(400, { + error: 'Soft delete not supported for users. Use hard delete endpoint or ban user.', + }); }, { params: z.object({ id: z.string() }), @@ -446,8 +433,8 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) .delete( '/users/:id/hard', async ({ params, body }) => { - const id = Number(params.id); - if (!Number.isFinite(id) || id <= 0) return status(400, { error: 'Invalid user id' }); + const id = params.id; + if (!id) return status(400, { error: 'Invalid user id' }); const db = createDb(); try { // Cascading FKs handle deletion of all related user data. @@ -481,21 +468,10 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) .post( '/users/:id/restore', async ({ params }) => { - const id = Number(params.id); - if (!Number.isFinite(id) || id <= 0) return status(400, { error: 'Invalid user id' }); - const db = createDb(); - try { - const restored = await db - .update(users) - .set({ deletedAt: null }) - .where(and(eq(users.id, id), sql`${users.deletedAt} IS NOT NULL`)) - .returning(); - if (!restored.length) return status(404, { error: 'User not found or not deleted' }); - return { success: true as const }; - } catch (error) { - console.error('Error restoring user:', error); - return status(500, { error: 'Failed to restore user' }); - } + const id = params.id; + if (!id) return status(400, { error: 'Invalid user id' }); + // Soft delete not supported for users in Better Auth + return status(400, { error: 'Soft delete not supported for users in Better Auth' }); }, { params: z.object({ id: z.string() }), @@ -510,10 +486,9 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) async ({ params }) => { const db = createDb(); try { - const now = new Date(); const updated = await db .update(packs) - .set({ deleted: true, deletedAt: now }) + .set({ deleted: true }) .where(and(eq(packs.id, params.id), eq(packs.deleted, false))) .returning(); if (!updated.length) return status(404, { error: 'Pack not found' }); diff --git a/packages/api/src/routes/admin/trails.ts b/packages/api/src/routes/admin/trails.ts index 232ad6bdf8..c04c388f75 100644 --- a/packages/api/src/routes/admin/trails.ts +++ b/packages/api/src/routes/admin/trails.ts @@ -280,7 +280,6 @@ export const adminTrailsRoutes = new Elysia({ prefix: '/trails' }) waterCrossings: trailConditionReports.waterCrossings, notes: trailConditionReports.notes, deleted: trailConditionReports.deleted, - deletedAt: trailConditionReports.deletedAt, createdAt: trailConditionReports.createdAt, userId: trailConditionReports.userId, userEmail: users.email, @@ -298,7 +297,6 @@ export const adminTrailsRoutes = new Elysia({ prefix: '/trails' }) data: reports.map((r) => ({ ...r, createdAt: r.createdAt.toISOString(), - deletedAt: r.deletedAt?.toISOString() ?? null, })), total: totalRow?.count ?? 0, limit, @@ -330,10 +328,9 @@ export const adminTrailsRoutes = new Elysia({ prefix: '/trails' }) async ({ params }) => { const db = createDb(); try { - const now = new Date(); const updated = await db .update(trailConditionReports) - .set({ deleted: true, deletedAt: now }) + .set({ deleted: true }) .where( and( eq(trailConditionReports.id, params.reportId), diff --git a/packages/api/src/routes/alltrails.ts b/packages/api/src/routes/alltrails.ts index 0cc5e6ac93..98a9e97a4a 100644 --- a/packages/api/src/routes/alltrails.ts +++ b/packages/api/src/routes/alltrails.ts @@ -78,6 +78,18 @@ export const alltrailsRoutes = new Elysia({ prefix: '/alltrails' }).post( return status(502, { error: `AllTrails returned status ${response.status}` }); } + // Verify the final URL is still within alltrails.com (guards against redirects followed by fetch) + if (response.url) { + try { + const finalUrl = new URL(response.url); + if (finalUrl.protocol !== 'https:' || !ALLTRAILS_HOSTNAME_RE.test(finalUrl.hostname)) { + return status(400, { error: 'URL redirected outside alltrails.com' }); + } + } catch { + // Non-parseable response.url — proceed + } + } + const html = await response.text(); const title = extractOgTag(html, 'og:title'); diff --git a/packages/api/src/routes/auth/index.ts b/packages/api/src/routes/auth/index.ts deleted file mode 100644 index 8154129aa1..0000000000 --- a/packages/api/src/routes/auth/index.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { createDb } from '@packrat/api/db'; -import { - authProviders, - oneTimePasswords, - packs, - packTemplateItems, - packTemplates, - refreshTokens, - users, -} from '@packrat/api/db/schema'; -import { authPlugin } from '@packrat/api/middleware/auth'; -import { - AppleAuthRequestSchema, - ForgotPasswordRequestSchema, - GoogleAuthRequestSchema, - LoginRequestSchema, - LogoutRequestSchema, - RefreshTokenRequestSchema, - RegisterRequestSchema, - ResetPasswordRequestSchema, - VerifyEmailRequestSchema, -} from '@packrat/api/schemas/auth'; -import { - findRefreshToken, - issueRefreshToken, - revokeRefreshToken, - revokeTokenFamily, - rotateRefreshToken, -} from '@packrat/api/services/refreshTokenService'; -import { - generateJWT, - generateVerificationCode, - hashPassword, - validateEmail, - validatePassword, - verifyPassword, -} from '@packrat/api/utils/auth'; -import { sendPasswordResetEmail, sendVerificationCodeEmail } from '@packrat/api/utils/email'; -import { getEnv } from '@packrat/api/utils/env-validation'; -import { assertDefined } from '@packrat/guards'; -import { and, eq, getTableColumns, gt } from 'drizzle-orm'; -import { Elysia, status } from 'elysia'; -import { OAuth2Client } from 'google-auth-library'; -import { z } from 'zod'; - -const { passwordHash: _pw, ...userWithoutPassword } = getTableColumns(users); - -export const authRoutes = new Elysia({ prefix: '/auth' }) - .use(authPlugin) - - // public-route: credentials are the auth mechanism - .post( - '/login', - async ({ body }) => { - const { email, password } = body; - const db = createDb(); - - if (!email || !password) { - return status(400, { error: 'Email and password are required' }); - } - - const user = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (user.length === 0) return status(401, { error: 'Invalid email or password' }); - - const userRecord = user[0]; - if (!userRecord) return status(401, { error: 'Invalid email or password' }); - - // biome-ignore lint/style/noNonNullAssertion: password hash exists for password auth - const isPasswordValid = await verifyPassword(password, userRecord.passwordHash!); - if (!isPasswordValid) return status(401, { error: 'Invalid email or password' }); - - if (!userRecord.emailVerified) { - return status(403, { error: 'Please verify your email before logging in' }); - } - - const refreshToken = await issueRefreshToken(db, { userId: userRecord.id }); - - const accessToken = await generateJWT({ - payload: { userId: userRecord.id, role: userRecord.role }, - }); - - const { passwordHash: _ph, ...userPayload } = userRecord; - - return { success: true, accessToken, refreshToken, user: userPayload }; - }, - { - body: LoginRequestSchema, - detail: { tags: ['Authentication'], summary: 'User login' }, - }, - ) - - // public-route: pre-authentication account creation - .post( - '/register', - async ({ body }) => { - const { email, password, firstName, lastName } = body; - const db = createDb(); - - if (!email || !password) { - return status(400, { error: 'Email and password are required' }); - } - if (!validateEmail(email)) return status(400, { error: 'Invalid email format' }); - - const passwordValidation = validatePassword(password); - if (!passwordValidation.valid) { - return status(400, { error: passwordValidation.message || 'Invalid password' }); - } - - const existingUser = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (existingUser.length > 0) return status(409, { error: 'Email already in use' }); - - const passwordHash = await hashPassword(password); - const [newUser] = await db - .insert(users) - .values({ - email: email.toLowerCase(), - passwordHash, - firstName, - lastName, - emailVerified: false, - }) - .returning(); - - if (!newUser) return status(500, { error: 'Failed to create user' }); - - const code = generateVerificationCode(5); - await db.insert(oneTimePasswords).values({ - userId: newUser.id, - code, - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), - }); - - await sendVerificationCodeEmail({ to: email, code }); - - return { - success: true, - message: - 'User registered successfully. Please check your email for your verification code.', - userId: newUser.id, - }; - }, - { - body: RegisterRequestSchema, - detail: { tags: ['Authentication'], summary: 'Register new user' }, - }, - ) - - // public-route: OTP code is the credential; no prior session needed - .post( - '/verify-email', - async ({ body }) => { - const { email, code } = body; - const db = createDb(); - - if (!email || !code) { - return status(400, { error: 'Email and verification code are required' }); - } - - const user = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (user.length === 0) return status(404, { error: 'User not found' }); - - const userRecord = user[0]; - if (!userRecord) return status(404, { error: 'User not found' }); - assertDefined(userRecord); - - const userId = userRecord.id; - - const verificationCode = await db - .select() - .from(oneTimePasswords) - .where( - and( - eq(oneTimePasswords.userId, userId), - eq(oneTimePasswords.code, code), - gt(oneTimePasswords.expiresAt, new Date()), - ), - ) - .limit(1); - - if (verificationCode.length === 0) { - return status(400, { error: 'Invalid or expired verification code' }); - } - - const [finalUser] = await db - .update(users) - .set({ emailVerified: true }) - .where(eq(users.id, userId)) - .returning(); - - await db.delete(oneTimePasswords).where(eq(oneTimePasswords.userId, userId)); - - const refreshToken = await issueRefreshToken(db, { userId: userRecord.id }); - - const accessToken = await generateJWT({ - payload: { userId, role: userRecord.role }, - }); - - assertDefined(finalUser); - const { passwordHash: _passwordHash, ...userPayload } = finalUser; - - return { - success: true, - message: 'Email verified successfully', - accessToken, - refreshToken, - user: userPayload, - }; - }, - { - body: VerifyEmailRequestSchema, - detail: { tags: ['Authentication'], summary: 'Verify email address' }, - }, - ) - - // public-route: pre-authentication email flow - .post( - '/resend-verification', - async ({ body }) => { - const { email } = body; - if (!email) return status(400, { error: 'Email is required' }); - - const db = createDb(); - const user = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (user.length === 0) return status(404, { error: 'User not found' }); - - const userRecord = user[0]; - if (!userRecord) return status(404, { error: 'User not found' }); - - const userId = userRecord.id; - if (userRecord.emailVerified) { - return status(400, { error: 'Email is already verified' }); - } - - await db.delete(oneTimePasswords).where(eq(oneTimePasswords.userId, userId)); - - const code = generateVerificationCode(5); - await db.insert(oneTimePasswords).values({ - userId, - code, - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), - }); - - await sendVerificationCodeEmail({ to: email, code }); - - return { success: true, message: 'Verification code sent successfully' }; - }, - { - body: z.object({ email: z.string().email() }), - detail: { tags: ['Authentication'], summary: 'Resend verification code' }, - }, - ) - - // public-route: pre-authentication email flow - .post( - '/forgot-password', - async ({ body }) => { - const { email } = body; - const db = createDb(); - - if (!email) return status(400, { error: 'Email is required' }); - - const user = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - const genericResponse = { - success: true as const, - message: 'If your email is registered, you will receive a verification code', - }; - - if (user.length === 0) return genericResponse; - const userRecord = user[0]; - if (!userRecord) return genericResponse; - - const code = generateVerificationCode(5); - await db.delete(oneTimePasswords).where(eq(oneTimePasswords.userId, userRecord.id)); - await db.insert(oneTimePasswords).values({ - userId: userRecord.id, - code, - expiresAt: new Date(Date.now() + 1 * 60 * 60 * 1000), - }); - - await sendPasswordResetEmail({ to: email, code }); - - return genericResponse; - }, - { - body: ForgotPasswordRequestSchema, - detail: { tags: ['Authentication'], summary: 'Request password reset' }, - }, - ) - - // public-route: OTP code is the credential; no prior session needed - .post( - '/reset-password', - async ({ body }) => { - const { email, code, newPassword } = body; - const db = createDb(); - - if (!email || !code || !newPassword) { - return status(400, { error: 'Email, code, and new password are required' }); - } - - const passwordValidation = validatePassword(newPassword); - if (!passwordValidation.valid) { - return status(400, { error: passwordValidation.message || 'Invalid password' }); - } - - const userResult = await db - .select() - .from(users) - .where(eq(users.email, email.toLowerCase())) - .limit(1); - - if (userResult.length === 0) return status(404, { error: 'User not found' }); - const user = userResult[0]; - if (!user) return status(404, { error: 'User not found' }); - - const codeRecord = await db - .select() - .from(oneTimePasswords) - .where(and(eq(oneTimePasswords.userId, user.id), eq(oneTimePasswords.code, code))) - .limit(1); - - if (codeRecord.length === 0) return status(400, { error: 'Invalid verification code' }); - const codeRecordItem = codeRecord[0]; - if (!codeRecordItem) return status(400, { error: 'Invalid verification code' }); - - if (new Date() > codeRecordItem.expiresAt) { - return status(400, { error: 'Verification code has expired' }); - } - - const passwordHash = await hashPassword(newPassword); - await db.update(users).set({ passwordHash }).where(eq(users.id, user.id)); - await db.delete(oneTimePasswords).where(eq(oneTimePasswords.id, codeRecordItem.id)); - - return { success: true, message: 'Password reset successfully' }; - }, - { - body: ResetPasswordRequestSchema, - detail: { tags: ['Authentication'], summary: 'Reset password' }, - }, - ) - - // public-route: refresh token is the credential - .post( - '/refresh', - async ({ body }) => { - try { - const { refreshToken } = body; - if (!refreshToken) return status(400, { error: 'Refresh token is required' }); - - const db = createDb(); - const record = await findRefreshToken(db, refreshToken); - if (!record) return status(401, { error: 'Invalid refresh token' }); - - // Replay detection: any submission of a revoked token invalidates the - // entire lineage. Forces the real user back through full auth and - // renders the leaked token useless going forward. - if (record.revokedAt) { - await revokeTokenFamily(db, refreshToken); - return status(401, { error: 'Refresh token reuse detected' }); - } - - if (new Date() > record.expiresAt) { - return status(401, { error: 'Refresh token expired' }); - } - - // Rotate inside a transaction so revoke + new-token insert happen - // atomically. Concurrent refreshes from multiple devices see a - // serializable view and the loser retries against an already-revoked - // token, triggering replay detection above. - const newRefreshToken = await rotateRefreshToken(db, { - oldId: record.id, - userId: record.userId, - }); - - const [user] = await db - .select(userWithoutPassword) - .from(users) - .where(eq(users.id, record.userId)) - .limit(1); - - if (!user) return status(401, { error: 'User not found' }); - - const accessToken = await generateJWT({ - payload: { userId: record.userId, role: user.role }, - }); - - return { success: true, accessToken, refreshToken: newRefreshToken, user }; - } catch (error) { - console.error('Token refresh error:', error); - return status(401, { error: 'An error occurred during token refresh' }); - } - }, - { - body: RefreshTokenRequestSchema, - detail: { tags: ['Authentication'], summary: 'Refresh access token' }, - }, - ) - - // public-route: refresh token is the credential; no JWT required - .post( - '/logout', - async ({ body }) => { - const db = createDb(); - const { refreshToken } = body; - - if (!refreshToken) return status(400, { error: 'Refresh token is required' }); - - await revokeRefreshToken(db, refreshToken); - - return { success: true, message: 'Logged out successfully' }; - }, - { - body: LogoutRequestSchema, - detail: { tags: ['Authentication'], summary: 'Logout user' }, - }, - ) - - // Me - .get( - '/me', - async ({ user }) => { - const db = createDb(); - const userId = Number(user.userId); - const userRows = await db - .select(userWithoutPassword) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - const userRecord = userRows[0]; - if (!userRecord) return status(401, { error: 'Unauthorized' }); - - return { success: true, user: userRecord }; - }, - { - isAuthenticated: true, - detail: { - tags: ['Authentication'], - summary: 'Get current user', - security: [{ bearerAuth: [] }], - }, - }, - ) - - // Delete account - .delete( - '/', - async ({ user }) => { - const db = createDb(); - const userId = user.userId; - - await db.delete(refreshTokens).where(eq(refreshTokens.userId, userId)); - await db.delete(oneTimePasswords).where(eq(oneTimePasswords.userId, userId)); - await db.delete(authProviders).where(eq(authProviders.userId, userId)); - await db.delete(packTemplateItems).where(eq(packTemplateItems.userId, userId)); - await db.delete(packTemplates).where(eq(packTemplates.userId, userId)); - await db.delete(packs).where(eq(packs.userId, userId)); - await db.delete(users).where(eq(users.id, userId)); - - return { success: true }; - }, - { - isAuthenticated: true, - detail: { - tags: ['Authentication'], - summary: 'Delete user account', - security: [{ bearerAuth: [] }], - }, - }, - ) - - // public-route: Apple identity token is the credential - .post( - '/apple', - async ({ body }) => { - const { identityToken } = body; - const db = createDb(); - - let payload: { sub: string; email: string; email_verified: boolean }; - try { - const part = identityToken.split('.')[1]; - if (!part) throw new Error('invalid'); - payload = JSON.parse(Buffer.from(part, 'base64').toString()); - } catch { - return status(400, { error: 'Invalid Apple token' }); - } - - const { sub, email, email_verified } = payload; - if (!sub || !email) return status(400, { error: 'Invalid Apple token' }); - - const [existingProvider] = await db - .select() - .from(authProviders) - .where(and(eq(authProviders.provider, 'apple'), eq(authProviders.providerId, sub))) - .limit(1); - - let userId: number; - if (existingProvider) { - userId = existingProvider.userId; - } else { - const [existingUser] = await db.select().from(users).where(eq(users.email, email)).limit(1); - - if (existingUser) { - userId = existingUser.id; - } else { - const [newUser] = await db - .insert(users) - .values({ email, emailVerified: email_verified || false }) - .returning(); - userId = newUser?.id || 0; - } - - await db.insert(authProviders).values({ - userId, - provider: 'apple', - providerId: sub, - }); - } - - const [user] = await db - .select(userWithoutPassword) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - assertDefined(user); - - const refreshToken = await issueRefreshToken(db, { userId }); - - const accessToken = await generateJWT({ - payload: { userId, role: user?.role || 'USER' }, - }); - - return { success: true, accessToken, refreshToken, user }; - }, - { - body: AppleAuthRequestSchema, - detail: { tags: ['Authentication'], summary: 'Sign in with Apple' }, - }, - ) - - // public-route: Google ID token is the credential - .post( - '/google', - async ({ body }) => { - const { GOOGLE_CLIENT_ID } = getEnv(); - const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID); - const { idToken } = body; - - if (!idToken) return status(400, { error: 'ID token is required' }); - - const db = createDb(); - - const ticket = await googleClient.verifyIdToken({ - idToken, - audience: GOOGLE_CLIENT_ID, - }); - - const payload = ticket.getPayload(); - if (!payload?.email || !payload?.sub) { - return status(400, { error: 'Invalid Google token' }); - } - - const [existingProvider] = await db - .select() - .from(authProviders) - .where(and(eq(authProviders.provider, 'google'), eq(authProviders.providerId, payload.sub))) - .limit(1); - - let userId: number; - let isNewUser = false; - - if (existingProvider) { - userId = existingProvider.userId; - } else { - const [existingUser] = await db - .select() - .from(users) - .where(eq(users.email, payload.email)) - .limit(1); - - if (existingUser) { - userId = existingUser.id; - await db.insert(authProviders).values({ - userId, - provider: 'google', - providerId: payload.sub, - }); - } else { - const [newUser] = await db - .insert(users) - .values({ - email: payload.email, - firstName: payload.given_name, - lastName: payload.family_name, - emailVerified: payload.email_verified || false, - }) - .returning(); - assertDefined(newUser); - - userId = newUser.id; - isNewUser = true; - - await db.insert(authProviders).values({ - userId, - provider: 'google', - providerId: payload.sub, - }); - } - } - - const [user] = await db - .select(userWithoutPassword) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - assertDefined(user); - - const refreshToken = await issueRefreshToken(db, { userId }); - - const accessToken = await generateJWT({ - payload: { userId, role: user.role }, - }); - - return { success: true, accessToken, refreshToken, user, isNewUser }; - }, - { - body: GoogleAuthRequestSchema, - detail: { tags: ['Authentication'], summary: 'Sign in with Google' }, - }, - ); diff --git a/packages/api/src/routes/catalog/index.ts b/packages/api/src/routes/catalog/index.ts index be393c8969..adb9f7f32f 100644 --- a/packages/api/src/routes/catalog/index.ts +++ b/packages/api/src/routes/catalog/index.ts @@ -218,6 +218,12 @@ export const catalogRoutes = new Elysia({ prefix: '/catalog' }) async ({ body }) => { const db = createDb(); const data = body; + if (!data.name || data.weight === undefined || data.weight === null || !data.weightUnit) { + return status(400, { error: 'name, weight, and weightUnit are required' }); + } + if (data.weight <= 0) { + return status(400, { error: 'weight must be a positive number' }); + } const { OPENAI_API_KEY, AI_PROVIDER, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_GATEWAY_ID, AI } = getEnv(); @@ -284,6 +290,14 @@ export const catalogRoutes = new Elysia({ prefix: '/catalog' }) async ({ params }) => { const db = createDb(); const itemId = Number(params.id); + if ( + !Number.isFinite(itemId) || + !Number.isInteger(itemId) || + itemId <= 0 || + itemId > 2147483647 + ) { + return status(404, { error: 'Catalog item not found' }); + } const item = await db.query.catalogItems.findFirst({ where: eq(catalogItems.id, itemId), @@ -320,6 +334,14 @@ export const catalogRoutes = new Elysia({ prefix: '/catalog' }) async ({ params, query }) => { const db = createDb(); const itemId = Number(params.id); + if ( + !Number.isFinite(itemId) || + !Number.isInteger(itemId) || + itemId <= 0 || + itemId > 2147483647 + ) { + return status(404, { error: 'Catalog item not found or has no embedding' }); + } const limit = query.limit ? Number(query.limit) : 5; const threshold = query.threshold ? Number(query.threshold) : 0.1; @@ -378,7 +400,18 @@ export const catalogRoutes = new Elysia({ prefix: '/catalog' }) async ({ params, body }) => { const db = createDb(); const itemId = Number(params.id); + if ( + !Number.isFinite(itemId) || + !Number.isInteger(itemId) || + itemId <= 0 || + itemId > 2147483647 + ) { + return status(404, { error: 'Catalog item not found' }); + } const data = body; + if (data.weight !== undefined && data.weight !== null && data.weight <= 0) { + return status(400, { error: 'weight must be a positive number' }); + } const { OPENAI_API_KEY, AI_PROVIDER, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_GATEWAY_ID, AI } = getEnv(); @@ -439,6 +472,14 @@ export const catalogRoutes = new Elysia({ prefix: '/catalog' }) async ({ params }) => { const db = createDb(); const itemId = Number(params.id); + if ( + !Number.isFinite(itemId) || + !Number.isInteger(itemId) || + itemId <= 0 || + itemId > 2147483647 + ) { + return status(404, { error: 'Catalog item not found' }); + } const existingItem = await db.query.catalogItems.findFirst({ where: eq(catalogItems.id, itemId), diff --git a/packages/api/src/routes/guides/index.ts b/packages/api/src/routes/guides/index.ts index df8a418fd8..c327787e3d 100644 --- a/packages/api/src/routes/guides/index.ts +++ b/packages/api/src/routes/guides/index.ts @@ -191,6 +191,9 @@ export const guidesRoutes = new Elysia({ prefix: '/guides' }) '/search', async ({ query }) => { const { q, page, limit, category } = query; + if (!q || q.trim() === '') { + return status(400, { error: 'Search query parameter q is required' }); + } const searchQuery = q.toLowerCase(); try { diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts index 0f5e6aaac2..db72d1d13f 100644 --- a/packages/api/src/routes/index.ts +++ b/packages/api/src/routes/index.ts @@ -2,7 +2,6 @@ import { Elysia } from 'elysia'; import { adminRoutes } from './admin'; import { aiRoutes } from './ai'; import { alltrailsRoutes } from './alltrails'; -import { authRoutes } from './auth'; import { catalogRoutes } from './catalog'; import { chatRoutes } from './chat'; import { feedRoutes } from './feed'; @@ -25,7 +24,6 @@ import { wildlifeRoutes } from './wildlife'; * Eden Treaty client for end-to-end type safety. */ export const routes = new Elysia({ prefix: '/api' }) - .use(authRoutes) .use(adminRoutes) .use(catalogRoutes) .use(guidesRoutes) diff --git a/packages/api/src/routes/packTemplates/index.ts b/packages/api/src/routes/packTemplates/index.ts index 5a65841180..56fbab9342 100644 --- a/packages/api/src/routes/packTemplates/index.ts +++ b/packages/api/src/routes/packTemplates/index.ts @@ -220,6 +220,15 @@ export const packTemplatesRoutes = new Elysia({ prefix: '/pack-templates' }) const { isAppTemplate } = body; contentUrl = body.contentUrl; + if (!contentUrl) { + return status(400, { error: 'contentUrl is required' }); + } + try { + new URL(contentUrl); + } catch { + return status(400, { error: 'contentUrl must be a valid URL' }); + } + const { GOOGLE_GENERATIVE_AI_API_KEY } = getEnv(); const google = createGoogleGenerativeAI({ apiKey: GOOGLE_GENERATIVE_AI_API_KEY }); @@ -378,7 +387,7 @@ export const packTemplatesRoutes = new Elysia({ prefix: '/pack-templates' }) return { newTemplate: createdTemplate, insertedItems: insertedItemsResult }; }); - return { ...newTemplate, items: insertedItems }; + return status(201, { ...newTemplate, items: insertedItems }); } catch (error) { console.error('Error generating pack template:', error); let errorCode = 'UNKNOWN_ERROR'; diff --git a/packages/api/src/routes/packs/index.ts b/packages/api/src/routes/packs/index.ts index 6f096aaa28..1bdf287ca5 100644 --- a/packages/api/src/routes/packs/index.ts +++ b/packages/api/src/routes/packs/index.ts @@ -172,6 +172,10 @@ export const packsRoutes = new Elysia({ prefix: '/packs' }) try { const { image, matchLimit } = body; + if (!image) { + return status(400, { error: 'image is required' }); + } + if (!image.startsWith(`${user.userId}-`)) { return status(403, { error: 'Unauthorized' }); } diff --git a/packages/api/src/schemas/auth.ts b/packages/api/src/schemas/auth.ts index 3c3c3f25fc..b6784b07fb 100644 --- a/packages/api/src/schemas/auth.ts +++ b/packages/api/src/schemas/auth.ts @@ -10,7 +10,7 @@ export const LoginResponseSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), user: z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), @@ -28,7 +28,7 @@ export const RegisterRequestSchema = z.object({ export const RegisterResponseSchema = z.object({ success: z.boolean(), message: z.string(), - userId: z.number(), + userId: z.string(), }); export const VerifyEmailRequestSchema = z.object({ @@ -42,7 +42,7 @@ export const VerifyEmailResponseSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), user: z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), @@ -59,7 +59,7 @@ export const RefreshTokenResponseSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), user: z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), @@ -102,7 +102,7 @@ export const SocialAuthResponseSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), user: z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), @@ -124,7 +124,7 @@ export const LogoutResponseSchema = z.object({ export const MeResponseSchema = z.object({ success: z.boolean(), user: z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), diff --git a/packages/api/src/schemas/catalog.ts b/packages/api/src/schemas/catalog.ts index de4a965b65..8efda018cb 100644 --- a/packages/api/src/schemas/catalog.ts +++ b/packages/api/src/schemas/catalog.ts @@ -242,7 +242,7 @@ export const UpdateCatalogItemRequestSchema = z.object({ name: z.string().min(1).max(255).optional(), productUrl: z.string().url().optional(), sku: z.string().optional(), - weight: z.number().positive().optional(), + weight: z.number().optional(), weightUnit: z.enum(WEIGHT_UNITS).optional(), description: z.string().optional(), categories: z.array(z.string()).optional(), diff --git a/packages/api/src/schemas/chat.ts b/packages/api/src/schemas/chat.ts index fd57c13466..9c408b969f 100644 --- a/packages/api/src/schemas/chat.ts +++ b/packages/api/src/schemas/chat.ts @@ -25,7 +25,7 @@ export const ChatRequestSchema = z.any(); // ; export const ReportedContentUserSchema = z.object({ - id: z.number(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), @@ -33,14 +33,14 @@ export const ReportedContentUserSchema = z.object({ export const ReportedContentSchema = z.object({ id: z.number(), - userId: z.number(), + userId: z.string(), userQuery: z.string(), aiResponse: z.string(), reason: z.string(), userComment: z.string().nullable(), status: z.string(), reviewed: z.boolean().nullable(), - reviewedBy: z.number().nullable(), + reviewedBy: z.string().nullable(), reviewedAt: z.string().datetime().nullable(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), diff --git a/packages/api/src/schemas/feed.ts b/packages/api/src/schemas/feed.ts index 08b3286c0a..4b4b5f31a7 100644 --- a/packages/api/src/schemas/feed.ts +++ b/packages/api/src/schemas/feed.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; export const PostAuthorSchema = z.object({ - id: z.number().int(), + id: z.string(), firstName: z.string().nullable(), lastName: z.string().nullable(), }); export const PostSchema = z.object({ id: z.number().int(), - userId: z.number().int(), + userId: z.string(), caption: z.string().nullable(), images: z.array(z.string()), createdAt: z.string().datetime(), @@ -35,7 +35,7 @@ export const FeedResponseSchema = z.object({ export const CommentSchema = z.object({ id: z.number().int(), postId: z.number().int(), - userId: z.number().int(), + userId: z.string(), content: z.string(), parentCommentId: z.number().int().nullable(), createdAt: z.string().datetime(), diff --git a/packages/api/src/schemas/packTemplates.ts b/packages/api/src/schemas/packTemplates.ts index 2b9fe4f254..6ffe2de886 100644 --- a/packages/api/src/schemas/packTemplates.ts +++ b/packages/api/src/schemas/packTemplates.ts @@ -1,5 +1,10 @@ import { z } from 'zod'; +const datetimeString = z.preprocess( + (v) => (v instanceof Date ? v.toISOString() : v), + z.string().datetime(), +); + export const ErrorResponseSchema = z.object({ error: z.string(), code: z.string().optional(), @@ -11,15 +16,15 @@ export const PackTemplateSchema = z.object({ name: z.string(), description: z.string().nullable(), category: z.string(), - userId: z.number(), + userId: z.string(), image: z.string().nullable(), tags: z.array(z.string()).nullable(), isAppTemplate: z.boolean(), deleted: z.boolean(), - localCreatedAt: z.string().datetime(), - localUpdatedAt: z.string().datetime(), - createdAt: z.string().datetime(), - updatedAt: z.string().datetime(), + localCreatedAt: datetimeString, + localUpdatedAt: datetimeString, + createdAt: datetimeString, + updatedAt: datetimeString, contentSource: z.string().nullable(), contentId: z.string().nullable(), }); @@ -38,10 +43,10 @@ export const PackTemplateItemSchema = z.object({ notes: z.string().nullable(), packTemplateId: z.string(), catalogItemId: z.number().nullable(), - userId: z.number(), + userId: z.string(), deleted: z.boolean(), - createdAt: z.string().datetime(), - updatedAt: z.string().datetime(), + createdAt: datetimeString, + updatedAt: datetimeString, }); export const PackTemplateWithItemsSchema = PackTemplateSchema.extend({ diff --git a/packages/api/src/schemas/packs.ts b/packages/api/src/schemas/packs.ts index 46c624c9ff..3414d227da 100644 --- a/packages/api/src/schemas/packs.ts +++ b/packages/api/src/schemas/packs.ts @@ -1,6 +1,11 @@ import { PACK_CATEGORIES, WEIGHT_UNITS } from '@packrat/api/types'; import { z } from 'zod'; +const datetimeString = z.preprocess( + (v) => (v instanceof Date ? v.toISOString() : v), + z.string().datetime(), +); + export const PackItemSchema = z.object({ id: z.string(), name: z.string(), @@ -15,17 +20,17 @@ export const PackItemSchema = z.object({ notes: z.string().nullable(), packId: z.string(), catalogItemId: z.number().int().nullable(), - userId: z.number().int(), + userId: z.string(), deleted: z.boolean(), isAIGenerated: z.boolean(), templateItemId: z.string().nullable(), - createdAt: z.string().datetime(), - updatedAt: z.string().datetime(), + createdAt: datetimeString, + updatedAt: datetimeString, }); export const PackSchema = z.object({ id: z.string(), - userId: z.number(), + userId: z.string(), name: z.string(), description: z.string().nullable(), category: z.enum(PACK_CATEGORIES).nullable(), @@ -35,10 +40,10 @@ export const PackSchema = z.object({ templateId: z.string().nullable().optional(), deleted: z.boolean(), isAIGenerated: z.boolean(), - localCreatedAt: z.string().datetime().optional(), - localUpdatedAt: z.string().datetime().optional(), - createdAt: z.string().datetime(), - updatedAt: z.string().datetime(), + localCreatedAt: datetimeString.optional(), + localUpdatedAt: datetimeString.optional(), + createdAt: datetimeString, + updatedAt: datetimeString, items: z.array(PackItemSchema).optional(), }); @@ -152,11 +157,11 @@ export const UpdatePackBodySchema = UpdatePackRequestSchema.extend({ export const PackWeightHistoryResponseSchema = z.object({ id: z.string(), packId: z.string(), - userId: z.number(), + userId: z.string(), weight: z.number(), - localCreatedAt: z.string().datetime().optional(), - createdAt: z.string().datetime(), - updatedAt: z.string().datetime(), + localCreatedAt: datetimeString.optional(), + createdAt: datetimeString, + updatedAt: datetimeString, }); export const CreatePackWeightHistoryBodySchema = z.object({ diff --git a/packages/api/src/schemas/trailConditions.ts b/packages/api/src/schemas/trailConditions.ts index fb928c4a2d..1dc4254775 100644 --- a/packages/api/src/schemas/trailConditions.ts +++ b/packages/api/src/schemas/trailConditions.ts @@ -1,5 +1,10 @@ import { z } from 'zod'; +const datetimeString = z.preprocess( + (v) => (v instanceof Date ? v.toISOString() : v), + z.string().datetime(), +); + export const TrailSurfaceSchema = z.enum(['paved', 'gravel', 'dirt', 'rocky', 'snow', 'mud']); export const OverallConditionSchema = z.enum(['excellent', 'good', 'fair', 'poor']); export const WaterCrossingDifficultySchema = z.enum(['easy', 'moderate', 'difficult']); @@ -15,13 +20,13 @@ export const TrailConditionReportSchema = z.object({ waterCrossingDifficulty: WaterCrossingDifficultySchema.nullable().optional(), notes: z.string().nullable().optional(), photos: z.array(z.string()), - userId: z.number().optional(), + userId: z.string().optional(), tripId: z.string().nullable().optional(), deleted: z.boolean(), - localCreatedAt: z.string().datetime().optional(), - localUpdatedAt: z.string().datetime().optional(), - createdAt: z.string().datetime().optional(), - updatedAt: z.string().datetime().optional(), + localCreatedAt: datetimeString.optional(), + localUpdatedAt: datetimeString.optional(), + createdAt: datetimeString.optional(), + updatedAt: datetimeString.optional(), }); export type TrailConditionReport = z.infer; diff --git a/packages/api/src/schemas/trips.ts b/packages/api/src/schemas/trips.ts index 7317c3d6e4..9e4e301dc1 100644 --- a/packages/api/src/schemas/trips.ts +++ b/packages/api/src/schemas/trips.ts @@ -1,5 +1,15 @@ import { z } from 'zod'; +const datetimeString = z.preprocess( + (v) => (v instanceof Date ? v.toISOString() : v), + z.string().datetime(), +); + +const nullableDateString = z.preprocess( + (v) => (v instanceof Date ? v.toISOString() : v), + z.string().nullable(), +); + export const TripLocationSchema = z.object({ latitude: z.number(), longitude: z.number(), @@ -12,15 +22,15 @@ export const TripSchema = z.object({ description: z.string().nullable().optional(), notes: z.string().nullable().optional(), location: TripLocationSchema.nullable().optional(), - startDate: z.string().nullable().optional(), - endDate: z.string().nullable().optional(), - userId: z.number().optional(), + startDate: nullableDateString.optional(), + endDate: nullableDateString.optional(), + userId: z.string().optional(), packId: z.string().nullable().optional(), deleted: z.boolean(), - localCreatedAt: z.string().datetime().optional(), - localUpdatedAt: z.string().datetime().optional(), - createdAt: z.string().datetime().optional(), - updatedAt: z.string().datetime().optional(), + localCreatedAt: datetimeString.optional(), + localUpdatedAt: datetimeString.optional(), + createdAt: datetimeString.optional(), + updatedAt: datetimeString.optional(), }); export const CreateTripBodySchema = z.object({ diff --git a/packages/api/src/services/__tests__/packService.test.ts b/packages/api/src/services/__tests__/packService.test.ts index b69d592fff..b1557eaf8e 100644 --- a/packages/api/src/services/__tests__/packService.test.ts +++ b/packages/api/src/services/__tests__/packService.test.ts @@ -80,7 +80,7 @@ describe('PackService', () => { beforeEach(() => { vi.clearAllMocks(); - service = new PackService(1); + service = new PackService('user-test-id-1'); }); // ------------------------------------------------------------------------- diff --git a/packages/api/src/services/embeddingService.ts b/packages/api/src/services/embeddingService.ts index d3223f0e0c..4af4c63a53 100644 --- a/packages/api/src/services/embeddingService.ts +++ b/packages/api/src/services/embeddingService.ts @@ -24,7 +24,7 @@ export const generateEmbedding = async ( const { value, ...providerConfig } = params; // Guard: skip if no text or only whitespace - if (!value || !value.trim()) { + if (!value?.trim()) { return null; } diff --git a/packages/api/src/services/executeSqlAiTool.ts b/packages/api/src/services/executeSqlAiTool.ts index c171685b84..87ba81b85b 100644 --- a/packages/api/src/services/executeSqlAiTool.ts +++ b/packages/api/src/services/executeSqlAiTool.ts @@ -7,7 +7,7 @@ const SQL_JOIN_KEYWORD = /\bjoin\b/g; interface Params { query: string; limit: number; - userId: number; + userId: string; } export async function executeSqlAiTool(params: Params) { diff --git a/packages/api/src/services/packItemService.ts b/packages/api/src/services/packItemService.ts index 9eb317ee1e..bb1c0ffdde 100644 --- a/packages/api/src/services/packItemService.ts +++ b/packages/api/src/services/packItemService.ts @@ -4,9 +4,9 @@ import { and, eq } from 'drizzle-orm'; export class PackItemService { private db; - private userId: number; + private userId: string; - constructor(userId: number) { + constructor(userId: string) { this.userId = userId; this.db = createDb(); } diff --git a/packages/api/src/services/packService.ts b/packages/api/src/services/packService.ts index 44ff2ed716..72f21addfe 100644 --- a/packages/api/src/services/packService.ts +++ b/packages/api/src/services/packService.ts @@ -40,9 +40,9 @@ type PackItemConceptSchema = z.infer; export class PackService { private db; - private userId: number; + private userId: string; - constructor(userId: number) { + constructor(userId: string) { this.userId = userId; this.db = createDb(); } diff --git a/packages/api/src/services/refreshTokenService.ts b/packages/api/src/services/refreshTokenService.ts index d238b84752..08e2bd7e01 100644 --- a/packages/api/src/services/refreshTokenService.ts +++ b/packages/api/src/services/refreshTokenService.ts @@ -1,176 +1,3 @@ -import type { createDb } from '@packrat/api/db'; -import { refreshTokens } from '@packrat/api/db/schema'; -import { generateRefreshToken } from '@packrat/api/utils/auth'; -import { getEnv } from '@packrat/api/utils/env-validation'; -import { and, eq, isNull, or, type SQL } from 'drizzle-orm'; - -/** - * Refresh-token persistence layer. - * - * Tokens are stored as HMAC-SHA256(raw, REFRESH_TOKEN_PEPPER) in the - * `refresh_tokens.token` column when the pepper is configured. During the - * rollout window, reads accept either the hashed form or the legacy - * plaintext form so existing sessions keep working until they expire. - * - * When `REFRESH_TOKEN_PEPPER` is not set, behavior falls back to plaintext - * storage so dev/test environments without a pepper still function. - */ - -type Db = ReturnType; - -export const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; -export const refreshTokenExpiry = () => new Date(Date.now() + REFRESH_TOKEN_TTL_MS); - -async function hmacSha256Hex(key: string, data: string): Promise { - const enc = new TextEncoder(); - const cryptoKey = await crypto.subtle.importKey( - 'raw', - enc.encode(key), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'], - ); - const sig = await crypto.subtle.sign('HMAC', cryptoKey, enc.encode(data)); - const bytes = new Uint8Array(sig); - let hex = ''; - for (let i = 0; i < bytes.byteLength; i++) { - hex += (bytes[i] ?? 0).toString(16).padStart(2, '0'); - } - return hex; -} - -/** Hash a raw refresh token for storage. Returns raw when pepper is unset. */ -export async function hashRefreshToken(raw: string): Promise { - const { REFRESH_TOKEN_PEPPER } = getEnv(); - if (!REFRESH_TOKEN_PEPPER) return raw; - return hmacSha256Hex(REFRESH_TOKEN_PEPPER, raw); -} - -/** - * WHERE-clause builder that matches either the hashed form or the legacy - * plaintext value. Use this on every query keyed by a raw refresh token so - * that rotation from plaintext → hashed storage is a hot upgrade. - */ -async function tokenMatchClause(raw: string): Promise { - const hashed = await hashRefreshToken(raw); - if (hashed === raw) return eq(refreshTokens.token, raw); - return or(eq(refreshTokens.token, hashed), eq(refreshTokens.token, raw)) as SQL; // safe-cast: Drizzle sql`` tag returns SQL type — or() returns SQL | undefined but is non-null when given two non-null args -} - -/** - * Insert a freshly-generated refresh token and return the **raw** value for - * the client response. The database row holds only the hashed form when a - * pepper is configured. - */ -export async function issueRefreshToken( - db: Db, - params: { userId: number; expiresAt?: Date }, -): Promise { - const raw = generateRefreshToken(); - const stored = await hashRefreshToken(raw); - await db.insert(refreshTokens).values({ - userId: params.userId, - token: stored, - expiresAt: params.expiresAt ?? refreshTokenExpiry(), - }); - return raw; -} - -/** - * Fetch a refresh-token row by raw value, INCLUDING revoked rows. Callers - * need the record even when revoked to detect replay. - */ -export async function findRefreshToken(db: Db, raw: string) { - const clause = await tokenMatchClause(raw); - const [row] = await db - .select({ - id: refreshTokens.id, - userId: refreshTokens.userId, - expiresAt: refreshTokens.expiresAt, - revokedAt: refreshTokens.revokedAt, - replacedByToken: refreshTokens.replacedByToken, - }) - .from(refreshTokens) - .where(clause) - .limit(1); - return row; -} - -/** - * Revoke the entire descendant chain of a refresh token. Used when a - * previously-revoked token is presented again (replay) — the safest response - * is to kill every token in the lineage so the attacker (or the confused - * client) is forced back through full auth. - */ -export async function revokeTokenFamily(db: Db, startToken: string): Promise { - const now = new Date(); - const visited = new Set(); - let current: string | null = startToken; - while (current && !visited.has(current)) { - visited.add(current); - const clause = await tokenMatchClause(current); - const [row] = await db - .select({ replacedByToken: refreshTokens.replacedByToken }) - .from(refreshTokens) - .where(clause) - .limit(1); - await db.update(refreshTokens).set({ revokedAt: now }).where(clause); - current = row?.replacedByToken ?? null; - } -} - -/** - * Revoke a single refresh token (logout path). Idempotent. - */ -export async function revokeRefreshToken(db: Db, raw: string): Promise { - const clause = await tokenMatchClause(raw); - await db.update(refreshTokens).set({ revokedAt: new Date() }).where(clause); -} - -/** - * Revoke a specific token row by id, set its `replacedByToken` to the HASHED - * successor. Used inside the rotation transaction so the lineage is - * reconstructable when checking for replay. - */ -export async function markRotated( - db: Db, - params: { id: number; replacedByRawToken: string }, -): Promise { - const replacedByStored = await hashRefreshToken(params.replacedByRawToken); - await db - .update(refreshTokens) - .set({ revokedAt: new Date(), replacedByToken: replacedByStored }) - .where(eq(refreshTokens.id, params.id)); -} - -/** - * Atomically rotate a refresh token: revoke the presented row, link its - * `replacedByToken` to the hashed successor, and insert the new row — all - * in a single transaction. Returns the raw successor for the client. - */ -export async function rotateRefreshToken( - db: Db, - params: { oldId: number; userId: number; expiresAt?: Date }, -): Promise { - return db.transaction(async (tx) => { - const fresh = generateRefreshToken(); - const stored = await hashRefreshToken(fresh); - const now = new Date(); - await tx - .update(refreshTokens) - .set({ revokedAt: now, replacedByToken: stored }) - .where(eq(refreshTokens.id, params.oldId)); - await tx.insert(refreshTokens).values({ - userId: params.userId, - token: stored, - expiresAt: params.expiresAt ?? refreshTokenExpiry(), - }); - return fresh; - }); -} - -/** Active-and-unrevoked clause for existing plaintext-style `.where` uses. */ -export async function activeTokenClause(raw: string): Promise { - const match = await tokenMatchClause(raw); - return and(match, isNull(refreshTokens.revokedAt)) as SQL; // safe-cast: Drizzle sql`` tag returns SQL type — and() returns SQL | undefined but is non-null when given two non-null args -} +// Refresh-token management is now handled by Better Auth (session table). +// This file is intentionally empty. +export {}; diff --git a/packages/api/src/services/userService.ts b/packages/api/src/services/userService.ts index 38295a9cdc..755a708153 100644 --- a/packages/api/src/services/userService.ts +++ b/packages/api/src/services/userService.ts @@ -34,6 +34,7 @@ export class UserService { const [user] = await this.db .insert(users) .values({ + id: crypto.randomUUID(), email: input.email.toLowerCase(), passwordHash, firstName: input.firstName ?? null, diff --git a/packages/api/src/utils/__tests__/auth.test.ts b/packages/api/src/utils/__tests__/auth.test.ts index 71603170f6..3043193d01 100644 --- a/packages/api/src/utils/__tests__/auth.test.ts +++ b/packages/api/src/utils/__tests__/auth.test.ts @@ -1,26 +1,5 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - generateJWT, - generateRefreshToken, - generateToken, - generateVerificationCode, - hashPassword, - isValidApiKey, - validateEmail, - validatePassword, - verifyJWT, - verifyPassword, -} from '../auth'; - -// --------------------------------------------------------------------------- -// Mocks – bcrypt and the crypto random function are mocked so tests are -// deterministic. `jose` is used at runtime for real signing/verification. -// --------------------------------------------------------------------------- -vi.mock('node:crypto', () => ({ - randomBytes: vi.fn((length: number) => ({ - toString: (_encoding: string) => 'a'.repeat(length * 2), - })), -})); +import { describe, expect, it, vi } from 'vitest'; +import { hashPassword, isValidApiKey, verifyPassword } from '../auth'; vi.mock('bcryptjs', () => ({ hash: vi.fn((password: string) => Promise.resolve(`hashed_${password}`)), @@ -31,126 +10,26 @@ vi.mock('bcryptjs', () => ({ vi.mock('../env-validation', () => ({ getEnv: vi.fn(() => ({ - JWT_SECRET: 'test-secret-that-is-long-enough', PACKRAT_API_KEY: 'test-api-key', })), })); describe('auth utilities', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('generateToken', () => { - it('generates a hex token with default length', () => { - expect(generateToken()).toBe('a'.repeat(64)); - }); - - it('generates a hex token with custom length', () => { - expect(generateToken(16)).toBe('a'.repeat(32)); - }); - }); - - describe('generateRefreshToken', () => { - it('generates an 80-character hex token', () => { - expect(generateRefreshToken()).toBe('a'.repeat(80)); - }); - }); - - describe('hashPassword', () => { + describe('hashPassword / verifyPassword', () => { it('hashes a password via bcrypt', async () => { const hash = await hashPassword('password123'); expect(hash).toBe('hashed_password123'); }); - }); - describe('verifyPassword', () => { - it('returns true for matching password and hash', async () => { + it('verifies a matching password', async () => { expect(await verifyPassword('password123', 'hashed_password123')).toBe(true); }); - it('returns false for non-matching password and hash', async () => { + it('rejects a non-matching password', async () => { expect(await verifyPassword('password123', 'hashed_wrong')).toBe(false); }); }); - describe('generateJWT / verifyJWT', () => { - it('round-trips a payload through jose', async () => { - const token = await generateJWT({ payload: { userId: 1, role: 'USER' } }); - expect(typeof token).toBe('string'); - expect(token.split('.').length).toBe(3); - - const payload = await verifyJWT({ token }); - expect(payload).not.toBeNull(); - expect(payload?.userId).toBe(1); - expect(payload?.role).toBe('USER'); - }); - - it('returns null for an invalid token', async () => { - expect(await verifyJWT({ token: 'not.a.valid-token' })).toBeNull(); - }); - }); - - describe('generateVerificationCode', () => { - it('generates a 6-digit code by default', () => { - vi.spyOn(Math, 'random').mockReturnValue(0.5); - const code = generateVerificationCode(); - expect(code).toHaveLength(6); - expect(code).toMatch(/^\d+$/); - vi.restoreAllMocks(); - }); - - it('generates a code of custom length', () => { - vi.spyOn(Math, 'random').mockReturnValue(0.5); - const code = generateVerificationCode(4); - expect(code).toHaveLength(4); - vi.restoreAllMocks(); - }); - }); - - describe('validatePassword', () => { - it('accepts a valid password', () => { - expect(validatePassword('StrongPass123')).toEqual({ valid: true }); - }); - - it('rejects password shorter than 8 characters', () => { - expect(validatePassword('Short1').valid).toBe(false); - }); - - it('rejects password without uppercase letter', () => { - expect(validatePassword('lowercase123').valid).toBe(false); - }); - - it('rejects password without lowercase letter', () => { - expect(validatePassword('UPPERCASE123').valid).toBe(false); - }); - - it('rejects password without number', () => { - expect(validatePassword('NoNumbersHere').valid).toBe(false); - }); - - it('accepts password with special characters', () => { - expect(validatePassword('Valid@Pass123')).toEqual({ valid: true }); - }); - }); - - describe('validateEmail', () => { - it('accepts valid email addresses', () => { - expect(validateEmail('test@example.com')).toBe(true); - expect(validateEmail('user.name@domain.co.uk')).toBe(true); - expect(validateEmail('test+tag@example.com')).toBe(true); - }); - - it('rejects invalid email addresses', () => { - expect(validateEmail('invalid')).toBe(false); - expect(validateEmail('invalid@')).toBe(false); - expect(validateEmail('@domain.com')).toBe(false); - expect(validateEmail('test@')).toBe(false); - expect(validateEmail('test @example.com')).toBe(false); - expect(validateEmail('')).toBe(false); - }); - }); - describe('isValidApiKey', () => { it('accepts a Headers instance with a matching key', () => { const headers = new Headers({ 'x-api-key': 'test-api-key' }); diff --git a/packages/api/src/utils/__tests__/compute-pack.test.ts b/packages/api/src/utils/__tests__/compute-pack.test.ts index 5df1bba21f..1eec9d5099 100644 --- a/packages/api/src/utils/__tests__/compute-pack.test.ts +++ b/packages/api/src/utils/__tests__/compute-pack.test.ts @@ -11,13 +11,12 @@ function makePack(overrides: Partial = {}): PackWithItems { name: 'Test Pack', description: null, category: 'hiking', - userId: 1, + userId: 'user-uuid-1', templateId: null, isPublic: false, image: null, tags: [], deleted: false, - deletedAt: null, isAIGenerated: false, localCreatedAt: new Date(), localUpdatedAt: new Date(), @@ -38,9 +37,8 @@ function makePackItem( consumable: overrides.consumable ?? false, worn: overrides.worn ?? false, packId: 'pack-1', - userId: 1, + userId: 'user-uuid-1', deleted: false, - deletedAt: null, isAIGenerated: false, category: null, description: null, diff --git a/packages/api/src/utils/__tests__/env-validation.test.ts b/packages/api/src/utils/__tests__/env-validation.test.ts index f4143f5dde..49a75039ad 100644 --- a/packages/api/src/utils/__tests__/env-validation.test.ts +++ b/packages/api/src/utils/__tests__/env-validation.test.ts @@ -9,9 +9,15 @@ function makeRawEnv(overrides: Record = {}): Record { (process.env as Record).NODE_ENV = 'production'; const rawEnv = makeRawEnv(); const result = getEnv(rawEnv); - expect(result.JWT_SECRET).toBe('secret'); + expect(result.BETTER_AUTH_SECRET).toBe('a-secret-that-is-at-least-32-characters-long!!'); expect(result.ENVIRONMENT).toBe('production'); }); it('uses relaxed validation in test environment', () => { (process.env as Record).NODE_ENV = 'test'; - const result = getEnv({ JWT_SECRET: 'test-secret' }); - expect(result.JWT_SECRET).toBe('test-secret'); + const result = getEnv({ BETTER_AUTH_SECRET: 'test-better-auth-secret-32-chars-long!!' }); + expect(result.BETTER_AUTH_SECRET).toBe('test-better-auth-secret-32-chars-long!!'); expect(result.ENVIRONMENT).toBe('development'); expect(result.SENTRY_DSN).toBe('https://test@test.ingest.sentry.io/test'); }); @@ -165,7 +171,7 @@ describe('env-validation', () => { }); it('throws on missing required variable', () => { - const invalid = makeRawEnv({ JWT_SECRET: undefined }); + const invalid = makeRawEnv({ BETTER_AUTH_SECRET: undefined }); expect(() => validateCloudflareApiEnv(invalid)).toThrow(); }); }); diff --git a/packages/api/src/utils/ai/tools.ts b/packages/api/src/utils/ai/tools.ts index 971d553116..9b0e30c4c6 100644 --- a/packages/api/src/utils/ai/tools.ts +++ b/packages/api/src/utils/ai/tools.ts @@ -9,7 +9,7 @@ import { executeSqlAiTool } from '@packrat/api/services/executeSqlAiTool'; import { tool } from 'ai'; import { z } from 'zod'; -export function createTools(userId: number) { +export function createTools(userId: string) { const packService = new PackService(userId); const packItemService = new PackItemService(userId); const weatherService = new WeatherService(); diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts index a475661d6b..3a9aca74da 100644 --- a/packages/api/src/utils/auth.ts +++ b/packages/api/src/utils/auth.ts @@ -1,110 +1,14 @@ -import { randomBytes } from 'node:crypto'; import { getEnv } from '@packrat/api/utils/env-validation'; -import { isNumber } from '@packrat/guards'; import * as bcrypt from 'bcryptjs'; -import { jwtVerify, SignJWT } from 'jose'; -export type JWTPayload = { - userId: number; - role?: 'USER' | 'ADMIN'; - exp?: number; - iat?: number; - [key: string]: unknown; -}; - -// Generate a random token -export function generateToken(length = 32): string { - return randomBytes(length).toString('hex'); -} - -// Hash a password using bcrypt export async function hashPassword(password: string): Promise { - const saltRounds = 10; - return bcrypt.hash(password, saltRounds); + return bcrypt.hash(password, 10); } -// Verify a password against a hash export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } -// Generate a refresh token -export function generateRefreshToken(): string { - return randomBytes(40).toString('hex'); -} - -function secretKey(secret: string): Uint8Array { - return new TextEncoder().encode(secret); -} - -// Generate a JWT token -export async function generateJWT({ - payload, - expiresIn = '7d', -}: { - payload: Omit & { exp?: number }; - expiresIn?: string; -}): Promise { - const { JWT_SECRET } = getEnv(); - const jwt = new SignJWT({ ...payload }).setProtectedHeader({ alg: 'HS256' }).setIssuedAt(); - - if (isNumber(payload.exp)) { - jwt.setExpirationTime(payload.exp); - } else { - jwt.setExpirationTime(expiresIn); - } - - return jwt.sign(secretKey(JWT_SECRET)); -} - -// Verify a JWT token -export async function verifyJWT({ token }: { token: string }): Promise { - try { - const { JWT_SECRET } = getEnv(); - const { payload } = await jwtVerify(token, secretKey(JWT_SECRET), { - algorithms: ['HS256'], - }); - return payload as JWTPayload; // safe-cast: jose jwtVerify returns JWTPayload — our JWTPayload type extends the jose type with userId/role fields - } catch { - return null; - } -} - -// Generate a random numeric verification code -export function generateVerificationCode(length = 6): string { - return Array.from({ length }, () => Math.floor(Math.random() * 10)).join(''); -} - -// Validate password strength -const HAS_UPPERCASE = /[A-Z]/; -const HAS_LOWERCASE = /[a-z]/; -const HAS_DIGIT = /[0-9]/; -const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -export function validatePassword(password: string): { - valid: boolean; - message?: string; -} { - if (password.length < 8) { - return { valid: false, message: 'Password must be at least 8 characters long' }; - } - if (!HAS_UPPERCASE.test(password)) { - return { valid: false, message: 'Password must contain at least one uppercase letter' }; - } - if (!HAS_LOWERCASE.test(password)) { - return { valid: false, message: 'Password must contain at least one lowercase letter' }; - } - if (!HAS_DIGIT.test(password)) { - return { valid: false, message: 'Password must contain at least one number' }; - } - return { valid: true }; -} - -// Validate email format -export function validateEmail(email: string): boolean { - return EMAIL_RE.test(email); -} - /** * Constant-time string comparison. Compares byte-by-byte after * length-equalizing the two inputs so neither the match result nor the diff --git a/packages/api/src/utils/env-validation.ts b/packages/api/src/utils/env-validation.ts index c8bdc96b64..93b8cba0d0 100644 --- a/packages/api/src/utils/env-validation.ts +++ b/packages/api/src/utils/env-validation.ts @@ -16,14 +16,21 @@ export const apiEnvSchema = z.object({ // set to env.OSM_HYPERDRIVE.connectionString (Hyperdrive binding). OSM_DATABASE_URL: z.string().url().optional(), - // Authentication & Security - JWT_SECRET: z.string(), - PASSWORD_RESET_SECRET: z.string(), + // Better Auth + BETTER_AUTH_SECRET: z.string().min(32), + BETTER_AUTH_URL: z.string().url(), // API base URL e.g. https://api.packrat.world + // Google OAuth (Better Auth social provider) GOOGLE_CLIENT_ID: z.string(), + GOOGLE_CLIENT_SECRET: z.string(), + // Apple Sign In (Better Auth social provider) + APPLE_CLIENT_ID: z.string(), // bundle ID e.g. world.packrat.app + APPLE_PRIVATE_KEY: z.string(), // .p8 key contents — store via wrangler secret + APPLE_KEY_ID: z.string(), + APPLE_TEAM_ID: z.string(), + // Admin & API key auth (unchanged) ADMIN_USERNAME: z.string(), ADMIN_PASSWORD: z.string(), PACKRAT_API_KEY: z.string(), - REFRESH_TOKEN_PEPPER: z.string().min(32).optional(), // Cloudflare Zero Trust / Access (optional — enables CF Access JWT verification for admin routes) CF_ACCESS_TEAM_DOMAIN: z.string().optional(), // e.g. "packrat.cloudflareaccess.com" @@ -77,6 +84,8 @@ export const apiEnvSchema = z.object({ // Hyperdrive binding for the dedicated OSM/trail Postgres instance. // When present, its connectionString overrides OSM_DATABASE_URL at runtime. OSM_HYPERDRIVE: z.unknown().optional(), + // Better Auth KV namespace for session storage and rate limiting + AUTH_KV: z.unknown(), }); // Relaxed schema for test environments @@ -86,7 +95,8 @@ const testEnvSchema = apiEnvSchema.partial().extend({ NEON_DATABASE_URL: z.string().optional().default('postgres://user:pass@localhost/db'), NEON_DATABASE_URL_READONLY: z.string().optional().default('postgres://user:pass@localhost/db'), OSM_DATABASE_URL: z.string().url().optional().default('postgres://user:pass@localhost/db'), - JWT_SECRET: z.string().optional().default('secret'), + BETTER_AUTH_SECRET: z.string().optional().default('test-better-auth-secret-32-chars-long!!'), + BETTER_AUTH_URL: z.string().url().optional().default('http://localhost:8787'), CF_VERSION_METADATA: z.unknown().optional().default({ id: 'test-version' }), AI: z.unknown().optional(), PACKRAT_SCRAPY_BUCKET: z.unknown().optional(), @@ -96,6 +106,7 @@ const testEnvSchema = apiEnvSchema.partial().extend({ LOGS_QUEUE: z.unknown().optional(), EMBEDDINGS_QUEUE: z.unknown().optional(), APP_CONTAINER: z.unknown().optional(), + AUTH_KV: z.unknown().optional(), }); type ValidatedAppEnv = z.infer; @@ -113,6 +124,7 @@ export type ValidatedEnv = Omit< | 'EMBEDDINGS_QUEUE' | 'APP_CONTAINER' | 'TOKEN_RATE_LIMITER' + | 'AUTH_KV' > & { CF_VERSION_METADATA: WorkerVersionMetadata; AI: Ai; @@ -125,6 +137,7 @@ export type ValidatedEnv = Omit< APP_CONTAINER: DurableObjectNamespace>; TOKEN_RATE_LIMITER?: { limit(opts: { key: string }): Promise<{ success: boolean }> }; OSM_HYPERDRIVE?: Hyperdrive; + AUTH_KV: KVNamespace; }; // Cache for validated envs keyed by the raw env reference. @@ -165,6 +178,7 @@ function validate(rawEnv: Record): ValidatedEnv { >, TOKEN_RATE_LIMITER: rawEnv.TOKEN_RATE_LIMITER as ValidatedEnv['TOKEN_RATE_LIMITER'] | undefined, // safe-cast: Cloudflare Worker binding injected by runtime OSM_HYPERDRIVE: rawEnv.OSM_HYPERDRIVE as Hyperdrive | undefined, // safe-cast: Cloudflare Worker binding injected by runtime + AUTH_KV: rawEnv.AUTH_KV as KVNamespace, // safe-cast: Cloudflare Worker binding injected by runtime } as ValidatedEnv; // safe-cast: all fields have been individually assigned above with correct runtime binding types } diff --git a/packages/api/test/admin-auth-guard.test.ts b/packages/api/test/admin-auth-guard.test.ts index f0b0ea0edd..8f899ad6e7 100644 --- a/packages/api/test/admin-auth-guard.test.ts +++ b/packages/api/test/admin-auth-guard.test.ts @@ -38,7 +38,7 @@ function withEnv(overrides: Record = {}) { */ async function issueTestAdminJwt(): Promise { const env = vi.mocked(getEnv)(); - const secret = new TextEncoder().encode(String(env.JWT_SECRET ?? 'secret')); + const secret = new TextEncoder().encode(String(env.BETTER_AUTH_SECRET ?? 'secret')); return new SignJWT({ role: 'admin' }) .setProtectedHeader({ alg: 'HS256' }) .setSubject('admin') @@ -272,7 +272,7 @@ describe('bypass attempts', () => { it('rejects a regular user JWT (correct secret, wrong role)', async () => { const env = vi.mocked(getEnv)(); - const secret = new TextEncoder().encode(String(env.JWT_SECRET ?? 'secret')); + const secret = new TextEncoder().encode(String(env.BETTER_AUTH_SECRET ?? 'secret')); const token = await new SignJWT({ role: 'USER', userId: 42 }) .setProtectedHeader({ alg: 'HS256' }) .setSubject('42') diff --git a/packages/api/test/admin-jwt.test.ts b/packages/api/test/admin-jwt.test.ts index 99e1a7e461..39ef75e650 100644 --- a/packages/api/test/admin-jwt.test.ts +++ b/packages/api/test/admin-jwt.test.ts @@ -21,9 +21,9 @@ const ADMIN_JWT_ISSUER = 'packrat-api'; const ADMIN_JWT_AUDIENCE = 'packrat-admin'; function secretKey(): Uint8Array { - // Reads the JWT_SECRET from the already-mocked getEnv in setup.ts. + // Reads the BETTER_AUTH_SECRET from the already-mocked getEnv in setup.ts. const env = vi.mocked(getEnv)(); - return new TextEncoder().encode(env.JWT_SECRET ?? 'secret'); + return new TextEncoder().encode(env.BETTER_AUTH_SECRET ?? 'secret'); } /** Issue a JWT via the /token endpoint using Basic auth. */ diff --git a/packages/api/test/admin.test.ts b/packages/api/test/admin.test.ts index 9f291ef548..2804dcb910 100644 --- a/packages/api/test/admin.test.ts +++ b/packages/api/test/admin.test.ts @@ -1,6 +1,3 @@ -import { createDb } from '@packrat/api/db'; -import { refreshTokens } from '@packrat/api/db/schema'; - import { describe, expect, it } from 'vitest'; import { seedCatalogItem, seedPack, seedTestUser } from './utils/db-helpers'; import { @@ -109,22 +106,6 @@ describe('Admin Routes', () => { const res = await apiWithBasicAuth('/users/999999', { method: 'DELETE' }); expect(res.status).toBe(404); }); - - it('returns 409 when the user has dependent data (e.g. refresh token)', async () => { - // refresh_tokens.user_id uses ON DELETE RESTRICT (schema.ts:48), so a - // row here triggers Postgres 23503 → 409 in the admin delete handler. - // packs/pack_items cascade, so they can't be used to verify this path. - const user = await seedTestUser({ email: 'admin-del-conflict@example.com' }); - const db = createDb(); - await db.insert(refreshTokens).values({ - userId: user.id, - token: `test-${Date.now()}-${Math.random()}`, - expiresAt: new Date(Date.now() + 86_400_000), - }); - - const res = await apiWithBasicAuth(`/users/${user.id}`, { method: 'DELETE' }); - expect(res.status).toBe(409); - }); }); describe('DELETE /admin/packs/:id', () => { diff --git a/packages/api/test/alltrails.test.ts b/packages/api/test/alltrails.test.ts index d05383bda3..931c22eed9 100644 --- a/packages/api/test/alltrails.test.ts +++ b/packages/api/test/alltrails.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { api } from './utils/test-helpers'; +import { apiWithAuth } from './utils/test-helpers'; const PREVIEW_PATH = '/alltrails/preview'; function post(body: unknown) { - return api(PREVIEW_PATH, { + return apiWithAuth(PREVIEW_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), diff --git a/packages/api/test/auth.test.ts b/packages/api/test/auth.test.ts index 22ff82122c..ae24d04299 100644 --- a/packages/api/test/auth.test.ts +++ b/packages/api/test/auth.test.ts @@ -1,520 +1,608 @@ +/** + * Better Auth integration tests. + * + * Creates a real Better Auth instance wired to the Docker Compose test database + * and calls auth.handler(request) directly. This verifies the full credential + * lifecycle, session token integrity, and the security properties that prevent + * users from being locked out. + * + * Scope: + * - Sign-up / sign-in / sign-out HTTP flows + * - Session token round-trip (sign-in → token → get-session → validate) + * - JWKS endpoint availability + * - Forget-password non-enumeration (200 for unknown email) + * - Lock-out prevention (bad passwords don't block valid logins) + * - Session isolation (sign-out of one session doesn't affect others) + * - End-to-end: real session token from sign-in authenticates Elysia routes + * + * Middleware unit tests (isAuthenticated macro, isAdmin macro, apiKeyAuth) are + * in test/middleware/ and are not duplicated here. + */ + +import { drizzleAdapter } from '@better-auth/drizzle-adapter'; +import { getAuth } from '@packrat/api/auth'; import { createDb } from '@packrat/api/db'; -import { oneTimePasswords } from '@packrat/api/db/schema'; - -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { app } from '../src/index'; -import { - apiWithAuth, - apiWithAuthAs, - expectBadRequest, - expectUnauthorized, - httpMethods, -} from './utils/test-helpers'; -import { createTestUser } from './utils/user-helpers'; - -// Helper for auth-specific API calls -const authApi = (path: string, init?: RequestInit) => - app.fetch(new Request(`http://localhost/api/auth${path}`, init)); - -describe('Auth Routes', () => { - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); - - describe('POST /auth/login', () => { - it('requires email and password', async () => { - const res = await authApi('/login', httpMethods.post({})); - expectBadRequest(res); - - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); +import * as schema from '@packrat/api/db/schema'; +import { authPlugin } from '@packrat/api/middleware/auth'; +import { betterAuth } from 'better-auth'; +import { bearer, jwt } from 'better-auth/plugins'; +import { eq } from 'drizzle-orm'; +import { Elysia } from 'elysia'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +// ─── Real Better Auth instance ──────────────────────────────────────────────── + +let realAuth: any; + +const TEST_BASE_URL = 'http://localhost:8787'; +const TEST_SECRET = 'test-better-auth-secret-32-chars-long!!'; + +beforeAll(async () => { + const db = createDb(); // returns testDb (mocked WS drizzle) from setup.ts + + realAuth = betterAuth({ + baseURL: TEST_BASE_URL, + secret: TEST_SECRET, + + database: drizzleAdapter(db, { + provider: 'pg', + schema: { + user: schema.users, + session: schema.session, + account: schema.account, + verification: schema.verification, + jwks: schema.jwks, + }, + }), + + user: { + additionalFields: { + role: { type: 'string', defaultValue: 'USER' }, + firstName: { type: 'string', fieldName: 'first_name' }, + lastName: { type: 'string', fieldName: 'last_name' }, + avatarUrl: { type: 'string', fieldName: 'avatar_url' }, + passwordHash: { type: 'string', fieldName: 'password_hash' }, + }, + }, + + emailAndPassword: { + enabled: true, + autoSignIn: true, + minPasswordLength: 8, + requireEmailVerification: false, + // No-op: request-password-reset endpoint requires this to be configured; + // we test only the HTTP status, not actual email delivery. + sendResetPassword: async () => {}, + }, + + trustedOrigins: [TEST_BASE_URL], + + // bearer() converts Authorization: Bearer into a session lookup, + // matching the production config that mobile clients depend on. + // jwt() exposes the JWKS endpoint. + plugins: [bearer(), jwt()], + }); +}); - it('requires email field', async () => { - const res = await authApi('/login', httpMethods.post({ password: 'test123' })); - expectBadRequest(res); - }); +afterAll(() => { + // Restore getAuth mock to the default setup.ts behaviour (HS256 JWT validator). + // Without this, tests that run after this file (in the same singleWorker + // process) would continue to use realAuth for session validation. + vi.mocked(getAuth).mockReset(); +}); - it('requires password field', async () => { - const res = await authApi('/login', httpMethods.post({ email: 'test@example.com' })); - expectBadRequest(res); - }); +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function authReq(path: string, init?: RequestInit): Promise { + return realAuth.handler( + new Request(`${TEST_BASE_URL}/api/auth/${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...init, + }), + ); +} + +function signUp(email: string, password: string) { + return authReq('sign-up/email', { + method: 'POST', + body: JSON.stringify({ email, password, name: 'Test User' }), + }); +} - it('returns error for non-existent user', async () => { - const res = await authApi( - '/login', - httpMethods.post({ - email: 'nonexistent@example.com', - password: 'password123', - }), - ); - expect(res.status).toBe(401); - - const data = await res.json(); - expect(data.error).toBe('Invalid email or password'); - }); +async function signIn(email: string, password: string): Promise { + return authReq('sign-in/email', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); +} + +async function getToken(email: string, password: string): Promise { + const res = await signIn(email, password); + const body = await res.json<{ token?: string }>(); + if (!body.token) throw new Error(`sign-in failed: ${JSON.stringify(body)}`); + return body.token; +} + +function getSession(token: string): Promise { + return authReq('get-session', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); +} + +function signOut(token: string): Promise { + return authReq('sign-out', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); +} + +let emailSeq = 0; +function uniq(prefix = 'user') { + return `${prefix}-${Date.now()}-${emailSeq++}@test.example.com`; +} + +// ─── Sign-up ────────────────────────────────────────────────────────────────── + +describe('POST /api/auth/sign-up/email', () => { + it('creates a user and returns a session token', async () => { + const email = uniq(); + const res = await signUp(email, 'Password123!'); + expect(res.status).toBe(200); + const body = await res.json<{ token?: string; user?: { email: string } }>(); + expect(body.token).toBeTruthy(); + expect(body.user?.email).toBe(email); + }); - it('returns error for incorrect password', async () => { - const user = await createTestUser({ email: 'login-test@example.com' }); - const res = await authApi( - '/login', - httpMethods.post({ - email: user.email, - password: 'wrong-password', - }), - ); - expect(res.status).toBe(401); - const data = await res.json(); - expect(data.error).toBe('Invalid email or password'); - }); + it('returns a non-null, non-empty token string', async () => { + const res = await signUp(uniq(), 'Password123!'); + const { token } = await res.json<{ token: string }>(); + expect(typeof token).toBe('string'); + expect(token.length).toBeGreaterThan(10); + }); + + it('rejects a duplicate email with 4xx', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const res = await signUp(email, 'DifferentPass1!'); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it('rejects a password shorter than 8 characters', async () => { + const res = await signUp(uniq(), 'abc'); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); - it('returns tokens and user on successful login', async () => { - const user = await createTestUser({ email: 'login-success@example.com' }); - const res = await authApi( - '/login', - httpMethods.post({ - email: user.email, - password: user.password, - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); - expect(data.accessToken).toBeDefined(); - expect(data.refreshToken).toBeDefined(); - expect(data.user.id).toBe(user.id); - expect(data.user.email).toBe(user.email); + it('rejects when the email field is missing', async () => { + const res = await authReq('sign-up/email', { + method: 'POST', + body: JSON.stringify({ password: 'Password123!' }), }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); - it('prevents login if email is not verified', async () => { - const user = await createTestUser({ - email: 'unverified@example.com', - emailVerified: false, - }); - const res = await authApi( - '/login', - httpMethods.post({ - email: user.email, - password: user.password, - }), - ); - expect(res.status).toBe(403); - const data = await res.json(); - expect(data.error).toBe('Please verify your email before logging in'); + it('rejects when the password field is missing', async () => { + const res = await authReq('sign-up/email', { + method: 'POST', + body: JSON.stringify({ email: uniq() }), }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); }); - describe('POST /auth/register', () => { - it('requires email and password', async () => { - const res = await authApi('/register', httpMethods.post({})); - expectBadRequest(res); + it('rejects an invalid email format', async () => { + const res = await signUp('not-an-email', 'Password123!'); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); + it('stores the password as a hash — not plaintext', async () => { + const password = 'SecrEtPass1!'; + const email = uniq(); + await signUp(email, password); + + // Better Auth stores credentials in the `account` table (providerId = 'credential'), + // not directly in users.password_hash. Verify the hash is not the plaintext password. + const db = createDb(); + const [cred] = await db + .select({ password: schema.account.password }) + .from(schema.account) + .where(eq(schema.account.providerId, 'credential')); + expect(cred?.password).not.toBe(password); + expect(cred?.password).toBeTruthy(); + }); +}); - it('validates email format', async () => { - const res = await authApi( - '/register', - httpMethods.post({ - email: 'invalid-email', - password: 'Password123!', - }), - ); - expectBadRequest(res); - - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); +// ─── Sign-in ────────────────────────────────────────────────────────────────── - it('validates password strength', async () => { - const res = await authApi( - '/register', - httpMethods.post({ - email: 'test@example.com', - password: '123', // Too weak - }), - ); - expectBadRequest(res); - - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); +describe('POST /api/auth/sign-in/email', () => { + it('returns a session token on valid credentials', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const res = await signIn(email, 'Password123!'); + expect(res.status).toBe(200); + const body = await res.json<{ token?: string }>(); + expect(body.token).toBeTruthy(); + }); - it('accepts valid registration data', async () => { - const res = await authApi( - '/register', - httpMethods.post({ - email: 'newuser@example.com', - password: 'Password123!', - firstName: 'Test', - lastName: 'User', - }), - ); - - expect(res.status).toBe(200); - - const data = await res.json(); - - expect(data.success).toBe(true); - expect(data.message).toContain('registered successfully'); - expect(data.userId).toBeDefined(); - }, 20000); - - it('prevents registration with an existing email', async () => { - await createTestUser({ email: 'existing@example.com' }); - const res = await authApi( - '/register', - httpMethods.post({ - email: 'existing@example.com', - password: 'Password123!', - }), - ); - expect(res.status).toBe(409); - const data = await res.json(); - expect(data.error).toBe('Email already in use'); - }); + it('returns 401 on wrong password', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const res = await signIn(email, 'WrongPass999!'); + expect(res.status).toBe(401); }); - describe('POST /auth/verify-email', () => { - it('requires email and code', async () => { - const res = await authApi('/verify-email', httpMethods.post({})); - expectBadRequest(res); + it('returns 4xx for a non-existent user without leaking existence', async () => { + const res = await signIn('nobody@nowhere.test', 'SomePass123!'); + // Must fail (not 200), but the response must not reveal whether the account + // exists vs the password was wrong — both should produce the same status code. + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); + it('wrong-password response body does not reveal whether user exists', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); - it('requires email field', async () => { - const res = await authApi('/verify-email', httpMethods.post({ code: '12345' })); - expectBadRequest(res); + const badPassRes = await signIn(email, 'WrongPass999!'); + const noUserRes = await signIn('ghost@nowhere.test', 'WrongPass999!'); + + // Status codes must be identical so callers cannot enumerate accounts. + expect(badPassRes.status).toBe(noUserRes.status); + }); + + it('rejects when email is missing', async () => { + const res = await authReq('sign-in/email', { + method: 'POST', + body: JSON.stringify({ password: 'Password123!' }), }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); - it('requires code field', async () => { - const res = await authApi('/verify-email', httpMethods.post({ email: 'test@example.com' })); - expectBadRequest(res); + it('rejects when password is missing', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const res = await authReq('sign-in/email', { + method: 'POST', + body: JSON.stringify({ email }), }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); +}); - it('verifies email with valid code', async () => { - const user = await createTestUser({ - email: 'verify-happy@example.com', - emailVerified: false, - }); - const db = createDb(); - await db.insert(oneTimePasswords).values({ - userId: user.id, - code: '12345', - expiresAt: new Date(Date.now() + 60_000), - }); +// ─── Session validation ─────────────────────────────────────────────────────── + +describe('GET /api/auth/get-session', () => { + it('returns user data for a valid session token', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); - const res = await authApi( - '/verify-email', - httpMethods.post({ email: user.email, code: '12345' }), - ); + const res = await getSession(token); + expect(res.status).toBe(200); + const body = await res.json<{ user?: { email: string }; session?: object }>(); + expect(body.user?.email).toBe(email); + expect(body.session).toBeTruthy(); + }); + + it('returns null/empty for an invalid token', async () => { + const res = await getSession('completely-invalid-token-xyz'); + // Better Auth returns 200 with null session, not 401 + expect(res.status).toBe(200); + const body = await res.json<{ session: null; user: null } | null>(); + // Either the body is null or the session field is null + const session = body && typeof body === 'object' && 'session' in body ? body.session : body; + expect(session).toBeNull(); + }); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); + it('returns null/empty when Authorization header is absent', async () => { + const res = await authReq('get-session', { + headers: { 'Content-Type': 'application/json' }, }); + expect(res.status).toBe(200); + const body = await res.json<{ session: null } | null>(); + const session = body && typeof body === 'object' && 'session' in body ? body.session : body; + expect(session).toBeNull(); + }); - it('rejects an expired code', async () => { - const user = await createTestUser({ - email: 'verify-expired@example.com', - emailVerified: false, - }); - const db = createDb(); - await db.insert(oneTimePasswords).values({ - userId: user.id, - code: '67890', - expiresAt: new Date(Date.now() - 60_000), - }); + it('session from sign-up is immediately usable', async () => { + const email = uniq(); + const signUpRes = await signUp(email, 'Password123!'); + const { token } = await signUpRes.json<{ token: string }>(); - const res = await authApi( - '/verify-email', - httpMethods.post({ email: user.email, code: '67890' }), - ); + const sessionRes = await getSession(token); + expect(sessionRes.status).toBe(200); + const { user } = await sessionRes.json<{ user: { email: string } }>(); + expect(user.email).toBe(email); + }); - expect(res.status).toBe(400); - }); + it('sign-in issues a different token from sign-up', async () => { + const email = uniq(); + const { token: tokenA } = await signUp(email, 'Password123!').then((r) => + r.json<{ token: string }>(), + ); + const { token: tokenB } = await signIn(email, 'Password123!').then((r) => + r.json<{ token: string }>(), + ); + // Two independent sessions — tokens must differ. + expect(tokenA).not.toBe(tokenB); + // Both tokens must be valid. + const s1 = await getSession(tokenA).then((r) => r.json<{ session: object }>()); + const s2 = await getSession(tokenB).then((r) => r.json<{ session: object }>()); + expect(s1.session).toBeTruthy(); + expect(s2.session).toBeTruthy(); + }); +}); - it('rejects a wrong code', async () => { - const user = await createTestUser({ - email: 'verify-wrong@example.com', - emailVerified: false, - }); - const db = createDb(); - await db.insert(oneTimePasswords).values({ - userId: user.id, - code: '11111', - expiresAt: new Date(Date.now() + 60_000), - }); +// ─── Sign-out ───────────────────────────────────────────────────────────────── - const res = await authApi( - '/verify-email', - httpMethods.post({ email: user.email, code: '99999' }), - ); +describe('POST /api/auth/sign-out', () => { + it('invalidates the session — getSession returns null afterwards', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); - expect(res.status).toBe(400); - }); + const signOutRes = await signOut(token); + expect(signOutRes.status).toBe(200); + + const sessionRes = await getSession(token); + const body = await sessionRes.json<{ session: null } | null>(); + const session = body && typeof body === 'object' && 'session' in body ? body.session : body; + expect(session).toBeNull(); }); - describe('POST /auth/resend-verification', () => { - it('requires email', async () => { - const res = await authApi('/resend-verification', httpMethods.post({})); - expectBadRequest(res); - }); + it('does not invalidate a different session for the same user', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token1 = await getToken(email, 'Password123!'); + const token2 = await getToken(email, 'Password123!'); - it('validates email format', async () => { - const res = await authApi( - '/resend-verification', - httpMethods.post({ - email: 'invalid-email', - }), - ); - expectBadRequest(res); - }); + await signOut(token1); + + // token1 is gone but token2 must still work. + const res = await getSession(token2); + const { session } = await res.json<{ session: object }>(); + expect(session).toBeTruthy(); }); - describe('POST /auth/forgot-password', () => { - it('requires email', async () => { - const res = await authApi('/forgot-password', httpMethods.post({})); - expectBadRequest(res); - }); + it('user can sign back in after sign-out', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); + await signOut(token); + + const res = await signIn(email, 'Password123!'); + expect(res.status).toBe(200); + const { token: newToken } = await res.json<{ token: string }>(); + expect(newToken).toBeTruthy(); + expect(newToken).not.toBe(token); + }); +}); - it('validates email format', async () => { - const res = await authApi( - '/forgot-password', - httpMethods.post({ - email: 'invalid-email', - }), - ); - expectBadRequest(res); - }); +// ─── JWKS endpoint ──────────────────────────────────────────────────────────── + +describe('GET /api/auth/jwks', () => { + it('returns a JWKS object with at least one key', async () => { + const res = await authReq('jwks'); + expect(res.status).toBe(200); + const body = await res.json<{ keys?: unknown[] }>(); + expect(Array.isArray(body.keys)).toBe(true); + expect((body.keys ?? []).length).toBeGreaterThanOrEqual(1); }); - describe('POST /auth/reset-password', () => { - it('requires email, code, and new password', async () => { - const res = await authApi('/reset-password', httpMethods.post({})); - expect(res.status).toBe(400); + it('returned keys contain required JWK fields', async () => { + const res = await authReq('jwks'); + const { keys } = await res.json<{ keys: Array> }>(); + const key = keys[0]; + // RSA public key fields + expect(key).toHaveProperty('kty'); + expect(key).toHaveProperty('kid'); + // The public key must not leak the private key exponent + expect(key).not.toHaveProperty('d'); + }); +}); - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); +// ─── Forget-password (no user enumeration) ─────────────────────────────────── - it('validates new password strength', async () => { - const res = await authApi( - '/reset-password', - httpMethods.post({ - email: 'test@example.com', - code: '12345', - newPassword: '123', // Too weak - }), - ); - expect(res.status).toBe(400); +describe('POST /api/auth/request-password-reset', () => { + it('returns 200 for a known email', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const res = await authReq('request-password-reset', { + method: 'POST', + body: JSON.stringify({ email, redirectTo: `${TEST_BASE_URL}/reset` }), }); + expect(res.status).toBe(200); + }); - it('resets password with valid code and the new password then works for login', async () => { - const user = await createTestUser({ email: 'reset-happy@example.com' }); - const db = createDb(); - await db.insert(oneTimePasswords).values({ - userId: user.id, - code: '54321', - expiresAt: new Date(Date.now() + 60_000), - }); + it('returns 200 for an unknown email — no user enumeration', async () => { + const res = await authReq('request-password-reset', { + method: 'POST', + body: JSON.stringify({ + email: 'nobody-exists@nowhere.test', + redirectTo: `${TEST_BASE_URL}/reset`, + }), + }); + // MUST be the same status as for a known email so callers cannot tell whether + // the account exists. + expect(res.status).toBe(200); + }); +}); - const res = await authApi( - '/reset-password', - httpMethods.post({ - email: user.email, - code: '54321', - newPassword: 'NewPassword123!', - }), - ); - - expect(res.status).toBe(200); - - // Old password must no longer work - const oldLogin = await authApi( - '/login', - httpMethods.post({ email: user.email, password: user.password }), - ); - expect(oldLogin.status).toBe(401); - - // New password works - const newLogin = await authApi( - '/login', - httpMethods.post({ email: user.email, password: 'NewPassword123!' }), - ); - expect(newLogin.status).toBe(200); - }); +// ─── Lock-out prevention (critical) ────────────────────────────────────────── - it('rejects an expired code', async () => { - const user = await createTestUser({ email: 'reset-expired@example.com' }); - const db = createDb(); - await db.insert(oneTimePasswords).values({ - userId: user.id, - code: '54322', - expiresAt: new Date(Date.now() - 60_000), - }); +describe('lock-out prevention', () => { + it('repeated wrong passwords do not block a subsequent valid login', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); - const res = await authApi( - '/reset-password', - httpMethods.post({ - email: user.email, - code: '54322', - newPassword: 'NewPassword123!', - }), - ); + // Five consecutive bad passwords — simulates an attacker or a user who + // keeps mistyping before they remember. + for (let i = 0; i < 5; i++) { + await signIn(email, `WrongPass${i}!`); + } - expect(res.status).toBe(400); - }); + // Valid credentials must still work. + const res = await signIn(email, 'Password123!'); + expect(res.status).toBe(200); + const { token } = await res.json<{ token: string }>(); + expect(token).toBeTruthy(); }); - describe('GET /auth/me', () => { - it('requires authentication', async () => { - const res = await authApi('/me'); - expectUnauthorized(res); - }); + it('a sign-out does not delete the user account', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); + await signOut(token); + + // User still exists in the DB. + const db = createDb(); + const [user] = await db + .select({ email: schema.users.email }) + .from(schema.users) + .where(eq(schema.users.email, email)); + expect(user?.email).toBe(email); + }); - it('returns user data when authenticated', async () => { - const testUser = await createTestUser(); - const res = await apiWithAuthAs('/auth/me', { - user: { id: testUser.id, role: (testUser.role ?? 'USER') as 'USER' | 'ADMIN' }, - }); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); - expect(data.user.id).toBe(testUser.id); - expect(data.user.email).toBe(testUser.email); - }); + it('multiple concurrent sessions survive individual sign-outs', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + + // Create three sessions. + const [token0, token1, token2]: [string, string, string] = await Promise.all([ + getToken(email, 'Password123!'), + getToken(email, 'Password123!'), + getToken(email, 'Password123!'), + ]); + + // Sign out of the first session. + await signOut(token0); + + // The other two sessions must remain valid. + const results = await Promise.all([ + getSession(token1).then((r) => r.json<{ session: object | null }>()), + getSession(token2).then((r) => r.json<{ session: object | null }>()), + ]); + for (const { session } of results) { + expect(session).toBeTruthy(); + } }); - describe('POST /auth/refresh', () => { - it('requires refresh token', async () => { - const res = await authApi('/refresh', httpMethods.post({})); - expectBadRequest(res); - }); + it('changing password does not happen silently — a wrong token cannot reset', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const goodToken = await getToken(email, 'Password123!'); - it('returns a new access token for a valid refresh token', async () => { - const user = await createTestUser({ email: 'refresh-happy@example.com' }); - const loginRes = await authApi( - '/login', - httpMethods.post({ email: user.email, password: user.password }), - ); - expect(loginRes.status).toBe(200); - const { refreshToken } = await loginRes.json(); - expect(refreshToken).toBeDefined(); - - const res = await authApi('/refresh', httpMethods.post({ refreshToken })); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.accessToken).toBeDefined(); - expect(typeof data.accessToken).toBe('string'); + // Attempt reset with a bogus token — must fail, not silently succeed. + const resetRes = await authReq('reset-password', { + method: 'POST', + body: JSON.stringify({ token: 'bogus-reset-token', newPassword: 'Hacked1234!', email }), }); + expect(resetRes.status).toBeGreaterThanOrEqual(400); - it('rejects a bogus refresh token with 401 Invalid refresh token', async () => { - const res = await authApi('/refresh', httpMethods.post({ refreshToken: 'not-a-real-token' })); - expect(res.status).toBe(401); - const data = await res.json(); - expect(data.error).toBe('Invalid refresh token'); - }); + // Original credentials must still work after the failed reset attempt. + const loginRes = await signIn(email, 'Password123!'); + expect(loginRes.status).toBe(200); + expect(goodToken).toBeTruthy(); }); +}); - describe('DELETE /auth/', () => { - it('requires authentication', async () => { - const res = await authApi('', httpMethods.delete()); - expectUnauthorized(res); - }); - - it('deletes the user account when authenticated', async () => { - const res = await apiWithAuth('/auth', { - method: 'DELETE', +// ─── End-to-end: real session → Elysia route ───────────────────────────────── + +describe('end-to-end session token flow', () => { + it('session token from sign-in authenticates an Elysia-protected route', async () => { + // Register a user through the real Better Auth flow. + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); + expect(token).toBeTruthy(); + + // Override getAuth for this specific request so the Elysia authPlugin + // validates real Better Auth sessions instead of HS256 test JWTs. + vi.mocked(getAuth).mockResolvedValueOnce(realAuth); + + // Build a minimal Elysia app that uses the real auth middleware. + const testApp = new Elysia() + .use(authPlugin) + .get('/me', ({ user }) => ({ userId: user.userId, email: user.email }), { + isAuthenticated: true, }); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); - // Verify user is gone - const meRes = await apiWithAuth('/auth/me'); - expect(meRes.status).toBe(401); // Token is no longer valid - }); + const res = await testApp.handle( + new Request('http://localhost/me', { + headers: { Authorization: `Bearer ${token}` }, + }), + ); + expect(res.status).toBe(200); + const body = await res.json<{ email: string }>(); + expect(body.email).toBe(email); }); - describe('POST /auth/apple', () => { - it('requires identity token and authorization code', async () => { - const res = await authApi('/apple', httpMethods.post({})); - expectBadRequest(res); - }); + it('an invalid token returns 401 from an Elysia-protected route', async () => { + vi.mocked(getAuth).mockResolvedValueOnce(realAuth); - it('validates identity token format', async () => { - const res = await authApi( - '/apple', - httpMethods.post({ - identityToken: 'invalid-token', - authorizationCode: 'auth-code', - }), - ); - expectBadRequest(res); - }); + const testApp = new Elysia() + .use(authPlugin) + .get('/me', ({ user }) => ({ userId: user.userId }), { isAuthenticated: true }); - it('handles invalid apple token', async () => { - const res = await authApi( - '/apple', - httpMethods.post({ - identityToken: 'invalid-token', - }), - ); - expectBadRequest(res); - }); + const res = await testApp.handle( + new Request('http://localhost/me', { + headers: { Authorization: 'Bearer totally-fake-token' }, + }), + ); + expect(res.status).toBe(401); }); - describe('POST /auth/google', () => { - it('requires ID token', async () => { - const res = await authApi('/google', httpMethods.post({})); - expect(res.status).toBe(400); + it('a signed-out token returns 401 from an Elysia-protected route', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const token = await getToken(email, 'Password123!'); + await signOut(token); - const data = await res.json(); - expect(data.error || data.issues).toBeDefined(); - }); + vi.mocked(getAuth).mockResolvedValueOnce(realAuth); - it('validates Google ID token and returns user', async () => { - const res = await authApi( - '/google', - httpMethods.post({ - idToken: 'mock-google-token', - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); - expect(data.accessToken).toBeDefined(); - expect(data.refreshToken).toBeDefined(); - expect(data.user.email).toBe('test@gmail.com'); - expect(data.isNewUser).toBe(true); // First time seeing this user - }); + const testApp = new Elysia() + .use(authPlugin) + .get('/me', ({ user }) => ({ userId: user.userId }), { isAuthenticated: true }); - it('logs in an existing Google user', async () => { - // First login creates the user - await authApi( - '/google', - httpMethods.post({ - idToken: 'mock-google-token', - }), - ); - - // Second login should find the existing user - const res = await authApi( - '/google', - httpMethods.post({ - idToken: 'mock-google-token', - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.success).toBe(true); - expect(data.isNewUser).toBe(false); - }); + const res = await testApp.handle( + new Request('http://localhost/me', { + headers: { Authorization: `Bearer ${token}` }, + }), + ); + expect(res.status).toBe(401); + }); + + it('sign-out of one session leaves Elysia access intact for another', async () => { + const email = uniq(); + await signUp(email, 'Password123!'); + const tokenA = await getToken(email, 'Password123!'); + const tokenB = await getToken(email, 'Password123!'); + + await signOut(tokenA); + + // tokenB must still work for Elysia route access. + vi.mocked(getAuth).mockResolvedValueOnce(realAuth); + + const testApp = new Elysia() + .use(authPlugin) + .get('/me', ({ user }) => ({ userId: user.userId }), { isAuthenticated: true }); + + const res = await testApp.handle( + new Request('http://localhost/me', { + headers: { Authorization: `Bearer ${tokenB}` }, + }), + ); + expect(res.status).toBe(200); }); }); diff --git a/packages/api/test/fixtures/pack-fixtures.ts b/packages/api/test/fixtures/pack-fixtures.ts index ff6751f41b..0abc349be1 100644 --- a/packages/api/test/fixtures/pack-fixtures.ts +++ b/packages/api/test/fixtures/pack-fixtures.ts @@ -1,8 +1,8 @@ import type { InferInsertModel } from 'drizzle-orm'; import type { packItems, packs } from '../../src/db/schema'; -type PackOverrides = Partial> & { userId: number }; -type PackItemOverrides = Partial> & { userId: number }; +type PackOverrides = Partial> & { userId: string }; +type PackItemOverrides = Partial> & { userId: string }; /** * Test fixture for creating a minimal valid pack. diff --git a/packages/api/test/fixtures/pack-template-fixtures.ts b/packages/api/test/fixtures/pack-template-fixtures.ts index f4cf630e17..4afc7eae0e 100644 --- a/packages/api/test/fixtures/pack-template-fixtures.ts +++ b/packages/api/test/fixtures/pack-template-fixtures.ts @@ -1,7 +1,7 @@ import type { InferInsertModel } from 'drizzle-orm'; import type { packTemplateItems, packTemplates } from '../../src/db/schema'; -type PackTemplateOverrides = Partial> & { userId: number }; +type PackTemplateOverrides = Partial> & { userId: string }; /** * Test fixture for creating a minimal valid pack template. @@ -31,7 +31,7 @@ export const createTestPackTemplate = ( */ type PackTemplateItemOverrides = Partial> & { - userId: number; + userId: string; }; export const createTestPackTemplateItem = ( diff --git a/packages/api/test/middleware/adminMiddleware.test.ts b/packages/api/test/middleware/adminMiddleware.test.ts index ed4824efc9..4a5d3b09b4 100644 --- a/packages/api/test/middleware/adminMiddleware.test.ts +++ b/packages/api/test/middleware/adminMiddleware.test.ts @@ -1,11 +1,17 @@ +import { getAuth } from '@packrat/api/auth'; import { adminAuthPlugin } from '@packrat/api/middleware/auth'; -import { generateJWT } from '@packrat/api/utils/auth'; import { Elysia } from 'elysia'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -beforeAll(() => { - vi.stubEnv('JWT_SECRET', 'test-secret-at-least-32-chars-long-xx'); - vi.stubEnv('PACKRAT_API_KEY', 'test-api-key'); +// getAuth is mocked globally by test/setup.ts. Override here to control sessions. +const mockGetSession = vi.fn(); + +beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(getAuth).mockResolvedValue({ + api: { getSession: mockGetSession }, + } as never); }); function buildAdminApp() { @@ -16,45 +22,57 @@ function buildAdminApp() { describe('adminAuthPlugin / isAdmin', () => { it('returns 401 when no Authorization header is present', async () => { + mockGetSession.mockResolvedValue(null); const app = buildAdminApp(); - const res = await app.handle(new Request('http://x/admin-only')); + const res = await app.handle(new Request('http://localhost/admin-only')); expect(res.status).toBe(401); }); - it('returns 403 for a USER-role JWT', async () => { - const userJwt = await generateJWT({ payload: { userId: 1, role: 'USER' } }); + it('returns 403 for a USER-role session', async () => { + mockGetSession.mockResolvedValue({ + user: { id: 'user-1', email: 'user@test.com', name: 'User', role: 'USER' }, + }); const app = buildAdminApp(); const res = await app.handle( - new Request('http://x/admin-only', { headers: { authorization: `Bearer ${userJwt}` } }), + new Request('http://localhost/admin-only', { + headers: { authorization: 'Bearer user-token' }, + }), ); expect(res.status).toBe(403); }); - it('returns 403 for a JWT with missing role (defaults to non-admin)', async () => { - const jwt = await generateJWT({ payload: { userId: 1 } as { userId: number } }); + it('returns 403 when session user has no role', async () => { + mockGetSession.mockResolvedValue({ + user: { id: 'user-1', email: 'user@test.com', name: 'User' }, + }); const app = buildAdminApp(); const res = await app.handle( - new Request('http://x/admin-only', { headers: { authorization: `Bearer ${jwt}` } }), + new Request('http://localhost/admin-only', { headers: { authorization: 'Bearer token' } }), ); expect(res.status).toBe(403); }); - it('accepts a valid ADMIN-role JWT', async () => { - const adminJwt = await generateJWT({ payload: { userId: 99, role: 'ADMIN' } }); + it('accepts a valid ADMIN-role session', async () => { + mockGetSession.mockResolvedValue({ + user: { id: 'admin-99', email: 'admin@test.com', name: 'Admin', role: 'ADMIN' }, + }); const app = buildAdminApp(); const res = await app.handle( - new Request('http://x/admin-only', { headers: { authorization: `Bearer ${adminJwt}` } }), + new Request('http://localhost/admin-only', { + headers: { authorization: 'Bearer admin-token' }, + }), ); expect(res.status).toBe(200); const body = await res.json(); expect(body.role).toBe('ADMIN'); - expect(body.userId).toBe(99); + expect(body.userId).toBe('admin-99'); }); - it('rejects X-API-Key on admin routes (no API-key → ADMIN escalation)', async () => { + it('rejects X-API-Key on admin routes', async () => { + mockGetSession.mockResolvedValue(null); const app = buildAdminApp(); const res = await app.handle( - new Request('http://x/admin-only', { headers: { 'x-api-key': 'test-api-key' } }), + new Request('http://localhost/admin-only', { headers: { 'x-api-key': 'test-api-key' } }), ); expect(res.status).toBe(401); }); diff --git a/packages/api/test/middleware/apiKeyAuth.test.ts b/packages/api/test/middleware/apiKeyAuth.test.ts index d4d61d2428..95c6af6d23 100644 --- a/packages/api/test/middleware/apiKeyAuth.test.ts +++ b/packages/api/test/middleware/apiKeyAuth.test.ts @@ -1,10 +1,16 @@ import { apiKeyAuthPlugin } from '@packrat/api/middleware/auth'; +import { getEnv } from '@packrat/api/utils/env-validation'; import { Elysia } from 'elysia'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -beforeAll(() => { - vi.stubEnv('JWT_SECRET', 'test-secret-at-least-32-chars-long-xx'); - vi.stubEnv('PACKRAT_API_KEY', 'the-real-server-key-value'); +// getEnv is mocked globally by test/setup.ts. Override here to control PACKRAT_API_KEY. +const REAL_API_KEY = 'the-real-server-key-value'; + +beforeEach(() => { + vi.mocked(getEnv).mockReturnValue({ + ...vi.mocked(getEnv)(), + PACKRAT_API_KEY: REAL_API_KEY, + } as never); }); function buildCronApp() { @@ -16,14 +22,14 @@ function buildCronApp() { describe('apiKeyAuthPlugin / isValidApiKey', () => { it('returns 401 when the X-API-Key header is absent', async () => { const app = buildCronApp(); - const res = await app.handle(new Request('http://x/cron', { method: 'POST' })); + const res = await app.handle(new Request('http://localhost/cron', { method: 'POST' })); expect(res.status).toBe(401); }); it('returns 401 on a wrong API key', async () => { const app = buildCronApp(); const res = await app.handle( - new Request('http://x/cron', { + new Request('http://localhost/cron', { method: 'POST', headers: { 'x-api-key': 'obviously-wrong-value' }, }), @@ -35,37 +41,39 @@ describe('apiKeyAuthPlugin / isValidApiKey', () => { const app = buildCronApp(); const attempt = 'attacker-guess-0123456789'; const res = await app.handle( - new Request('http://x/cron', { + new Request('http://localhost/cron', { method: 'POST', headers: { 'x-api-key': attempt }, }), ); const body = await res.text(); expect(body).not.toContain(attempt); - expect(body).not.toContain('the-real-server-key-value'); + expect(body).not.toContain(REAL_API_KEY); }); it('accepts the configured PACKRAT_API_KEY', async () => { const app = buildCronApp(); const res = await app.handle( - new Request('http://x/cron', { + new Request('http://localhost/cron', { method: 'POST', - headers: { 'x-api-key': 'the-real-server-key-value' }, + headers: { 'x-api-key': REAL_API_KEY }, }), ); expect(res.status).toBe(200); }); it('fails closed when PACKRAT_API_KEY env is unset', async () => { - vi.stubEnv('PACKRAT_API_KEY', ''); + vi.mocked(getEnv).mockReturnValue({ + ...vi.mocked(getEnv)(), + PACKRAT_API_KEY: '', + } as never); const app = buildCronApp(); const res = await app.handle( - new Request('http://x/cron', { + new Request('http://localhost/cron', { method: 'POST', headers: { 'x-api-key': 'anything' }, }), ); expect(res.status).toBe(401); - vi.stubEnv('PACKRAT_API_KEY', 'the-real-server-key-value'); }); }); diff --git a/packages/api/test/middleware/auth.test.ts b/packages/api/test/middleware/auth.test.ts index fe710a6594..e3565a557f 100644 --- a/packages/api/test/middleware/auth.test.ts +++ b/packages/api/test/middleware/auth.test.ts @@ -1,18 +1,18 @@ +import { getAuth } from '@packrat/api/auth'; import { authPlugin } from '@packrat/api/middleware/auth'; -import { generateJWT } from '@packrat/api/utils/auth'; import { Elysia } from 'elysia'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -/** - * Unit tests for the authPlugin macro. Uses in-process `app.handle` to - * exercise the full Elysia macro chain without spinning up a Worker. - */ +// getAuth is mocked globally by test/setup.ts. Override it here to control +// what session is returned per-test. +const mockGetSession = vi.fn(); -// JWT_SECRET is read at generate + verify time from getEnv(); set it once -// before any token work happens. -beforeAll(() => { - vi.stubEnv('JWT_SECRET', 'test-secret-at-least-32-chars-long-xx'); - vi.stubEnv('PACKRAT_API_KEY', 'test-api-key'); +beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(getAuth).mockResolvedValue({ + api: { getSession: mockGetSession }, + } as never); }); function buildTestApp() { @@ -23,94 +23,70 @@ function buildTestApp() { describe('authPlugin', () => { it('returns 401 when Authorization header is missing', async () => { + mockGetSession.mockResolvedValue(null); const app = buildTestApp(); - const res = await app.handle(new Request('http://x/protected')); + const res = await app.handle(new Request('http://localhost/protected')); expect(res.status).toBe(401); }); it('returns 401 when bearer token is empty', async () => { + mockGetSession.mockResolvedValue(null); const app = buildTestApp(); const res = await app.handle( - new Request('http://x/protected', { headers: { authorization: 'Bearer ' } }), + new Request('http://localhost/protected', { headers: { authorization: 'Bearer ' } }), ); expect(res.status).toBe(401); }); - it('returns 401 on tampered JWT signature', async () => { - const valid = await generateJWT({ payload: { userId: 1, role: 'USER' } }); - const parts = valid.split('.'); - // Flip a character in the signature segment so HMAC verification fails. - const tamperedSig = parts[2]?.slice(0, -1) + (parts[2]?.endsWith('A') ? 'B' : 'A'); - const tampered = `${parts[0]}.${parts[1]}.${tamperedSig}`; + it('returns 401 when getSession returns null (invalid/expired token)', async () => { + mockGetSession.mockResolvedValue(null); const app = buildTestApp(); const res = await app.handle( - new Request('http://x/protected', { headers: { authorization: `Bearer ${tampered}` } }), + new Request('http://localhost/protected', { + headers: { authorization: 'Bearer invalid-token' }, + }), ); expect(res.status).toBe(401); }); - it('returns 401 on an expired JWT', async () => { - const expired = await generateJWT({ - payload: { userId: 1, role: 'USER', exp: Math.floor(Date.now() / 1000) - 60 }, - }); + it('does NOT accept X-API-Key on user-scoped routes', async () => { + mockGetSession.mockResolvedValue(null); const app = buildTestApp(); const res = await app.handle( - new Request('http://x/protected', { headers: { authorization: `Bearer ${expired}` } }), + new Request('http://localhost/protected', { headers: { 'x-api-key': 'test-api-key' } }), ); expect(res.status).toBe(401); }); - it('rejects alg:none JWTs', async () => { - // Hand-crafted alg:none token — header says none, signature is empty - const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url'); - const payload = Buffer.from(JSON.stringify({ userId: 1, role: 'ADMIN' })).toString('base64url'); - const noneJwt = `${header}.${payload}.`; - const app = buildTestApp(); - const res = await app.handle( - new Request('http://x/protected', { headers: { authorization: `Bearer ${noneJwt}` } }), - ); - expect(res.status).toBe(401); - }); - - it('does NOT accept X-API-Key on user-scoped routes (regression test for #2162)', async () => { - // The legacy behavior synthesized { userId: 0, role: 'ADMIN' } from - // X-API-Key and injected it into any `isAuthenticated` route. The - // removed fallback means API-key-only requests now fail 401 against - // user-scoped routes and must use `apiKeyAuthPlugin` instead. + it('accepts a valid session and injects user context', async () => { + mockGetSession.mockResolvedValue({ + user: { id: 'user-42', email: 'test@example.com', name: 'Test User', role: 'USER' }, + }); const app = buildTestApp(); const res = await app.handle( - new Request('http://x/protected', { headers: { 'x-api-key': 'test-api-key' } }), + new Request('http://localhost/protected', { + headers: { authorization: 'Bearer valid-session-token' }, + }), ); - expect(res.status).toBe(401); + const body = await res.json(); + expect(res.status).toBe(200); + expect(body.userId).toBe('user-42'); + expect(body.role).toBe('USER'); + expect(body.email).toBe('test@example.com'); }); - it('role claim cannot be forged via JWT payload spread', async () => { - // The resolver picks `role` from `payload.role` explicitly AFTER - // spreading `...rest`, so an adversary cannot shove a `role` field into - // rest. This guards that ordering. - const userToken = await generateJWT({ - payload: { userId: 7, role: 'USER', scope: 'read' }, + it('preserves role from session, not from caller-supplied claim', async () => { + mockGetSession.mockResolvedValue({ + user: { id: 'user-7', email: 'test@example.com', name: 'Test User', role: 'USER' }, }); const app = new Elysia() .use(authPlugin) .get('/role', ({ user }) => ({ role: user.role }), { isAuthenticated: true }); const res = await app.handle( - new Request('http://x/role', { headers: { authorization: `Bearer ${userToken}` } }), - ); - const body = await res.json(); - expect(res.status).toBe(200); - expect(body.role).toBe('USER'); - }); - - it('accepts a valid user JWT and injects user context', async () => { - const token = await generateJWT({ payload: { userId: 42, role: 'USER' } }); - const app = buildTestApp(); - const res = await app.handle( - new Request('http://x/protected', { headers: { authorization: `Bearer ${token}` } }), + new Request('http://localhost/role', { headers: { authorization: 'Bearer token' } }), ); const body = await res.json(); expect(res.status).toBe(200); - expect(body.userId).toBe(42); expect(body.role).toBe('USER'); }); }); diff --git a/packages/api/test/packs.test.ts b/packages/api/test/packs.test.ts index 9b058f5815..acf67167c7 100644 --- a/packages/api/test/packs.test.ts +++ b/packages/api/test/packs.test.ts @@ -29,11 +29,11 @@ vi.mock('@packrat/api/services/packService', async () => { return { ...actual, PackService: class PackService extends actual.PackService { - private readonly _userId: number; + private readonly _userId: string; constructor(...args: ConstructorParameters) { super(...args); // First argument is the userId in the Elysia-native PackService. - this._userId = args[0] as number; + this._userId = args[0] as string; } async generatePacks(count: number) { const mockPacks: Pack[] = []; @@ -54,7 +54,6 @@ vi.mock('@packrat/api/services/packService', async () => { localCreatedAt: new Date(), localUpdatedAt: new Date(), deleted: false, - deletedAt: null, }); } return mockPacks; diff --git a/packages/api/test/setup.ts b/packages/api/test/setup.ts index 3f28efdea6..8d3544cb6c 100644 --- a/packages/api/test/setup.ts +++ b/packages/api/test/setup.ts @@ -1,5 +1,5 @@ import { neonConfig, Pool } from '@neondatabase/serverless'; -import { isObject } from '@packrat/guards'; +import { isFunction, isObject } from '@packrat/guards'; import { sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/neon-serverless'; import { afterAll, beforeAll, beforeEach, vi } from 'vitest'; @@ -26,9 +26,12 @@ const testEnv = { NEON_DATABASE_URL: 'postgres://test_user:test_password@localhost:5432/packrat_test', NEON_DATABASE_URL_READONLY: 'postgres://test_user:test_password@localhost:5432/packrat_test', - JWT_SECRET: 'secret', - PASSWORD_RESET_SECRET: 'secret', + // Better Auth (replaces JWT_SECRET) + BETTER_AUTH_SECRET: 'test-better-auth-secret-32-chars-long!!', + BETTER_AUTH_URL: 'http://localhost:8787', GOOGLE_CLIENT_ID: 'test-client-id', + GOOGLE_CLIENT_SECRET: 'test-client-secret', + ADMIN_USERNAME: 'admin', ADMIN_PASSWORD: 'admin-password', PACKRAT_API_KEY: 'test-api-key', @@ -52,6 +55,7 @@ const testEnv = { PACKRAT_BUCKET_R2_BUCKET_NAME: 'test-bucket', PACKRAT_GUIDES_BUCKET_R2_BUCKET_NAME: 'test-guides-bucket', PACKRAT_SCRAPY_BUCKET_R2_BUCKET_NAME: 'test-scrapy-bucket', + R2_PUBLIC_URL: 'https://r2.test.example.com', PACKRAT_GUIDES_RAG_NAME: 'test-rag', PACKRAT_GUIDES_BASE_URL: 'https://guides.test.com', @@ -113,6 +117,44 @@ vi.mock('elysia/adapter/cloudflare-worker', async (importOriginal) => { }; }); +// Mock Better Auth's getAuth so integration tests don't need a real Better Auth +// instance. The mock validates the HS256 JWTs that test-helpers issues, mapping +// the JWT payload to a Better Auth-shaped session object. +vi.mock('@packrat/api/auth', async () => { + const { jwtVerify } = await import('jose'); + const testJwtSecret = new TextEncoder().encode('secret'); + + return { + getAuth: vi.fn(async () => ({ + api: { + getSession: vi.fn(async ({ headers }: { headers: Headers }) => { + const authHeader = isFunction(headers.get) + ? headers.get('authorization') + : (headers as unknown as Record)?.authorization; + if (!authHeader?.startsWith('Bearer ')) return null; + const token = authHeader.slice(7).trim(); + if (!token) return null; + try { + const { payload } = await jwtVerify(token, testJwtSecret, { algorithms: ['HS256'] }); + const userId = String(payload.userId ?? ''); + if (!userId) return null; + return { + user: { + id: userId, + email: 'test@example.com', + name: 'Test User', + role: (payload.role as string) ?? 'USER', + }, + }; + } catch { + return null; + } + }), + }, + })), + }; +}); + // Mock AWS SDK S3Client to prevent actual network calls vi.mock('@aws-sdk/s3-request-presigner', () => ({ getSignedUrl: vi.fn().mockResolvedValue('https://mock-signed-url.com/test.jpg'), @@ -654,9 +696,9 @@ beforeEach(async () => { // testPool.query, so cleanup and tests share one path. Surface errors rather // than swallowing them. const tablesToClean = [ - 'one_time_passwords', - 'refresh_tokens', - 'auth_providers', + 'session', + 'account', + 'verification', 'weight_history', 'pack_items', 'pack_template_items', diff --git a/packages/api/test/upload.test.ts b/packages/api/test/upload.test.ts index b040cb1973..8fd05aac32 100644 --- a/packages/api/test/upload.test.ts +++ b/packages/api/test/upload.test.ts @@ -23,7 +23,8 @@ describe('Upload Routes', () => { expectUnauthorized(res); }); - it('requires auth for direct upload', async () => { + it.skip('requires auth for direct upload', async () => { + // POST /upload route does not exist; only GET /upload/presigned is implemented. const res = await api('/upload', httpMethods.post({})); expectUnauthorized(res); }); diff --git a/packages/api/test/utils/db-helpers.ts b/packages/api/test/utils/db-helpers.ts index b7cbd0f894..046540f955 100644 --- a/packages/api/test/utils/db-helpers.ts +++ b/packages/api/test/utils/db-helpers.ts @@ -52,13 +52,18 @@ export async function seedTestUser( const email = overrides?.email ?? `test-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`; + const firstName = overrides?.firstName ?? 'Test'; + const lastName = overrides?.lastName ?? 'User'; + const [user] = await db .insert(schema.users) .values({ + id: overrides?.id ?? crypto.randomUUID(), email, passwordHash, - firstName: overrides?.firstName ?? 'Test', - lastName: overrides?.lastName ?? 'User', + name: overrides?.name ?? `${firstName} ${lastName}`, + firstName, + lastName, role: overrides?.role ?? 'USER', emailVerified: overrides?.emailVerified ?? true, }) @@ -147,7 +152,7 @@ export async function seedCatalogItems( * @returns The created pack template with id */ export async function seedPackTemplate( - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -167,7 +172,7 @@ export async function seedPackTemplate( export async function seedPackTemplates( count: number, - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -190,7 +195,7 @@ export async function seedPackTemplates( export async function seedPackTemplateItem( packTemplateId: string, - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -212,7 +217,7 @@ export async function seedPackTemplateItems( packTemplateId: string, opts: { count: number; - overrides: Partial> & { userId: number }; + overrides: Partial> & { userId: string }; }, ) { const { count, overrides } = opts; @@ -235,7 +240,7 @@ export async function seedPackTemplateItems( * @returns The created pack with id */ export async function seedPack( - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -255,7 +260,7 @@ export async function seedPack( export async function seedPacks( count: number, - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -278,7 +283,7 @@ export async function seedPacks( export async function seedPackItem( packId: string, - overrides: Partial> & { userId: number }, + overrides: Partial> & { userId: string }, ) { const db = createDb(); @@ -300,7 +305,7 @@ export async function seedPackItems( packId: string, opts: { count: number; - overrides: Partial> & { userId: number }; + overrides: Partial> & { userId: string }; }, ) { const { count, overrides } = opts; diff --git a/packages/api/test/utils/test-helpers.ts b/packages/api/test/utils/test-helpers.ts index 73489b1fae..aae2f41938 100644 --- a/packages/api/test/utils/test-helpers.ts +++ b/packages/api/test/utils/test-helpers.ts @@ -23,7 +23,7 @@ expect.extend({ }, }); -type AuthSubject = { id: number; role: 'USER' | 'ADMIN' }; +type AuthSubject = { id: string; role: 'USER' | 'ADMIN' }; // Current test user for JWT signing. Set by seedTestUser (#2180) — no hardcoded id. let currentTestUser: AuthSubject | null = null; @@ -65,8 +65,8 @@ const fetchWithUser = async (path: string, opts: { user: AuthSubject; init?: Req // Synthetic JWT subject for tests that sign a JWT but never touch users in DB // (e.g. catalog, guides, upload). Tests that need the user to exist in DB must // call seedTestUser() in beforeEach, which sets currentTestUser. -const SYNTHETIC_USER: AuthSubject = { id: 0, role: 'USER' }; -const SYNTHETIC_ADMIN: AuthSubject = { id: 0, role: 'ADMIN' }; +const SYNTHETIC_USER: AuthSubject = { id: 'synthetic-user-id', role: 'USER' }; +const SYNTHETIC_ADMIN: AuthSubject = { id: 'synthetic-admin-id', role: 'ADMIN' }; export const apiWithAuth = async (path: string, init?: RequestInit) => fetchWithUser(path, { user: currentTestUser ?? SYNTHETIC_USER, init }); diff --git a/packages/api/test/utils/user-helpers.ts b/packages/api/test/utils/user-helpers.ts index d004b1a015..946a41a4a4 100644 --- a/packages/api/test/utils/user-helpers.ts +++ b/packages/api/test/utils/user-helpers.ts @@ -13,11 +13,13 @@ export async function createTestUser( ) { const db = createDb(); - const { password = 'Password123!', ...userData } = overrides; + const { password = 'Password123!', id: overrideId, ...userData } = overrides; const passwordHash = await hashPassword(password); const finalUserData: InferInsertModel = { + id: overrideId ?? crypto.randomUUID(), + name: 'Test User', email: `test-${Date.now()}@example.com`, firstName: 'Test', lastName: 'User', diff --git a/packages/api/wrangler.jsonc b/packages/api/wrangler.jsonc index 9109ecd1a6..6ca2ec58df 100644 --- a/packages/api/wrangler.jsonc +++ b/packages/api/wrangler.jsonc @@ -25,6 +25,16 @@ // Cloudflare Zero Trust / Access (optional — set to enable JWT verification on /api/admin/*): // CF_ACCESS_TEAM_DOMAIN=.cloudflareaccess.com // CF_ACCESS_AUD= + // KV namespace for Better Auth session storage and rate limiting. + // Create via: wrangler kv namespace create AUTH_KV + // Then replace the placeholder IDs below with the real namespace IDs. + "kv_namespaces": [ + { + "binding": "AUTH_KV", + "id": "0d0dd76cec764c81be58ae7b871b47cb", + "preview_id": "f3441ec9f4b044e6b6c6a087251e3f00" + } + ], "rate_limiting": [ { "binding": "TOKEN_RATE_LIMITER", diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 0c74314c14..1b687ea620 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -12,9 +12,11 @@ "test:watch": "vitest" }, "dependencies": { + "@cloudflare/workers-oauth-provider": "^0.4.0", "@modelcontextprotocol/sdk": "^1.11.0", "@packrat/api-client": "workspace:*", "agents": "^0.11.0", + "magic-regexp": "catalog:", "zod": "catalog:" }, "devDependencies": { diff --git a/packages/mcp/src/__tests__/auth.test.ts b/packages/mcp/src/__tests__/auth.test.ts index aa45534ca4..1568b2e800 100644 --- a/packages/mcp/src/__tests__/auth.test.ts +++ b/packages/mcp/src/__tests__/auth.test.ts @@ -1,40 +1,29 @@ /** - * Tests for the Worker entry-point fetch handler in src/index.ts. + * Tests for the PackRat MCP Worker OAuth flow. * - * The Worker wraps the McpAgent with: - * - a health check at GET / and GET /health - * - bearer-auth guard for /mcp (no token → 401) - * - 404 for unknown paths + * The worker is now wrapped with OAuthProvider, which: + * - Serves GET/POST /token, POST /register, /.well-known/* automatically + * - Routes /mcp (and sub-paths) to mcpApiHandler after token validation + * - Routes everything else to PackRatAuthHandler (/, /health, /authorize, /login, /callback) * - * We test this by importing the default export and calling its fetch() method - * directly with a mocked Env and ExecutionContext. + * Because OAuthProvider requires a real KV namespace (OAUTH_KV) and performs + * cryptographic operations, we test the auth handler sub-units in isolation + * and use a lightweight integration harness that mocks OAuthProvider + KV. */ + import { describe, expect, it, vi } from 'vitest'; -import type { Env } from '../index'; -// ── minimal Env and ExecutionContext fakes ──────────────────────────────────── +// ── Mock cloudflare:workers before any imports ──────────────────────────────── -const fakeEnv = { - PACKRAT_API_URL: 'https://api.example.com', - PackRatMCP: { - // Durable Object namespace stub — `idFromName` + `get` + stub `fetch` - idFromName: vi.fn().mockReturnValue({ toString: () => 'stub-id' }), - get: vi.fn().mockReturnValue({ - fetch: vi.fn().mockResolvedValue(new Response('{}', { status: 200 })), - }), - }, -} as unknown as Env; - -const fakeCtx = { - waitUntil: vi.fn(), - passThroughOnException: vi.fn(), -} as unknown as ExecutionContext; +vi.mock('cloudflare:workers', () => ({ + WorkerEntrypoint: class {}, + DurableObject: class {}, +})); -// ── stub agents/mcp so we do NOT spin up a real Durable Object ─────────────── +// ── Mock agents/mcp ─────────────────────────────────────────────────────────── vi.mock('agents/mcp', () => { class McpAgent { - // Instance fetch stub mirrors the real McpAgent shape (Durable Object) fetch(_request: Request): Promise { return Promise.resolve(new Response('{}', { status: 200 })); } @@ -53,125 +42,371 @@ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ registerResource = vi.fn(); registerPrompt = vi.fn(); }, + ResourceTemplate: class { + constructor( + public uriTemplate: string, + _opts?: unknown, + ) {} + }, })); -// ── import the Worker after mocks are in place ──────────────────────────────── +// ── Mock OAuthProvider — returns a simple fetch handler for testing ──────────── -const { default: worker } = await import('../index'); +vi.mock('@cloudflare/workers-oauth-provider', () => { + class OAuthProvider { + private opts: Record; + constructor(opts: Record) { + this.opts = opts; + } + // biome-ignore lint/complexity/useMaxParams: mirrors Cloudflare Workers fetch signature + async fetch(request: Request, env: Record, ctx: unknown): Promise { + const url = new URL(request.url); + + // Simulate OAuthProvider routing: + // - /token → token endpoint (handled by OAuthProvider itself) + // - /mcp* → apiHandler (with props injected) + // - others → defaultHandler + + if (url.pathname === '/token') { + // Simulate token endpoint — return a minimal token response + return Response.json({ access_token: 'test-token', token_type: 'Bearer' }); + } + + if (url.pathname === '/mcp' || url.pathname.startsWith('/mcp/')) { + const authHeader = request.headers.get('Authorization'); + const token = authHeader?.match(/^Bearer\s+(\S+)/i)?.[1] ?? ''; + + if (!token) { + return Response.json( + { error: 'unauthorized', error_description: 'Missing access token' }, + { + status: 401, + headers: { 'WWW-Authenticate': 'Bearer realm="packrat-mcp"' }, + }, + ); + } + + // Call the apiHandler with a props-augmented ctx + const apiHandler = this.opts.apiHandler as { + fetch: (req: Request, env: unknown, ctx: unknown) => Promise; + }; + const augCtx = Object.assign({}, ctx, { props: { betterAuthToken: token, userId: 'u1' } }); + return apiHandler.fetch(request, env, augCtx); + } + + // Route all other paths to defaultHandler + const defaultHandler = this.opts.defaultHandler as { + fetch: (req: Request, env: unknown) => Promise; + }; + return defaultHandler.fetch(request, env); + } + } + return { OAuthProvider, default: OAuthProvider }; +}); -// ── helpers ─────────────────────────────────────────────────────────────────── +// ── Helpers ─────────────────────────────────────────────────────────────────── -function makeRequest(url: string, options: RequestInit = {}): Request { - return new Request(url, options); +function makeKv(initial: Record = {}): KVNamespace { + const store = new Map( + Object.entries(initial).map(([k, v]) => [k, { value: v }]), + ); + return { + get: vi.fn(async (key: string) => store.get(key)?.value ?? null), + // biome-ignore lint/complexity/useMaxParams: mirrors KVNamespace.put signature + put: vi.fn(async (key: string, value: string, opts?: { expirationTtl?: number }) => { + store.set(key, { value, expiration: opts?.expirationTtl }); + }), + delete: vi.fn(async (key: string) => { + store.delete(key); + }), + getWithMetadata: vi.fn(), + list: vi.fn(), + } as unknown as KVNamespace; } -// ── tests ───────────────────────────────────────────────────────────────────── +function makeOAuthProvider() { + return { + parseAuthRequest: vi.fn().mockResolvedValue({ + responseType: 'code', + clientId: 'test-client', + redirectUri: 'https://client.example.com/cb', + scope: ['mcp'], + state: 'xyz', + }), + lookupClient: vi.fn().mockResolvedValue({ + clientId: 'test-client', + redirectUris: ['https://client.example.com/cb'], + }), + completeAuthorization: vi.fn().mockResolvedValue({ + redirectTo: 'https://client.example.com/cb?code=abc&state=xyz', + }), + }; +} -describe('Worker fetch handler', () => { - describe('health check', () => { - it('returns 200 for GET /', async () => { - const res = await worker.fetch(makeRequest('https://worker.example.com/'), fakeEnv, fakeCtx); - expect(res.status).toBe(200); - const body = (await res.json()) as Record; - expect(body.status).toBe('ok'); - expect(body.service).toBe('packrat-mcp'); - }); +function makeEnv(kvOverrides: Record = {}): import('../types').Env { + return { + PACKRAT_API_URL: 'https://api.example.com', + OAUTH_KV: makeKv(kvOverrides), + OAUTH_PROVIDER: makeOAuthProvider() as unknown as import('../types').Env['OAUTH_PROVIDER'], + PackRatMCP: {} as unknown as DurableObjectNamespace, + }; +} - it('returns 200 for GET /health', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/health'), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(200); - const body = (await res.json()) as Record; - expect(body.status).toBe('ok'); - }); +function req(url: string, init: RequestInit = {}): Request { + return new Request(url, init); +} + +// ── Import worker after all mocks ───────────────────────────────────────────── + +const { default: worker } = await import('../index'); +const fakeCtx = { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), +} as unknown as ExecutionContext; + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('health check', () => { + it('returns 200 for GET /', async () => { + const env = makeEnv(); + const res = await worker.fetch(req('https://mcp.example.com/'), env, fakeCtx); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.status).toBe('ok'); + expect(body.service).toBe('packrat-mcp'); }); - describe('/mcp auth guard', () => { - it('returns 401 when Authorization header is absent', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/mcp'), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(401); - const body = (await res.json()) as Record; - expect(body.error).toBe('Unauthorized'); - expect(res.headers.get('WWW-Authenticate')).toMatch(/Bearer/); - }); + it('returns 200 for GET /health', async () => { + const env = makeEnv(); + const res = await worker.fetch(req('https://mcp.example.com/health'), env, fakeCtx); + expect(res.status).toBe(200); + }); +}); + +describe('/mcp auth guard', () => { + it('returns 401 when Authorization header is absent', async () => { + const env = makeEnv(); + const res = await worker.fetch(req('https://mcp.example.com/mcp'), env, fakeCtx); + expect(res.status).toBe(401); + expect(res.headers.get('WWW-Authenticate')).toMatch(/Bearer/); + }); + + it('returns 401 for empty Bearer token', async () => { + const env = makeEnv(); + const res = await worker.fetch( + req('https://mcp.example.com/mcp', { headers: { Authorization: 'Bearer ' } }), + env, + fakeCtx, + ); + expect(res.status).toBe(401); + }); - it('returns 401 when Authorization is not Bearer scheme', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/mcp', { - headers: { Authorization: 'Basic dXNlcjpwYXNz' }, - }), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(401); + it('forwards request to McpAgent when a valid Bearer token is provided', async () => { + const env = makeEnv(); + const res = await worker.fetch( + req('https://mcp.example.com/mcp', { + method: 'POST', + headers: { + Authorization: 'Bearer valid-token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }), + }), + env, + fakeCtx, + ); + expect(res.status).toBe(200); + }); +}); + +describe('PackRatAuthHandler – /authorize', () => { + it('redirects to /login with a generated state key', async () => { + const env = makeEnv(); + const res = await worker.fetch( + req( + 'https://mcp.example.com/authorize?response_type=code&client_id=test-client&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=mcp&state=abc', + ), + env, + fakeCtx, + ); + expect(res.status).toBe(302); + const location = res.headers.get('Location') ?? ''; + expect(location).toMatch(/\/login\?state=/); + }); + + it('stores OAuth state in KV', async () => { + const env = makeEnv(); + await worker.fetch( + req( + 'https://mcp.example.com/authorize?response_type=code&client_id=test-client&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=mcp&state=abc', + ), + env, + fakeCtx, + ); + expect(env.OAUTH_KV.put).toHaveBeenCalledWith( + expect.stringMatching(/^oauth_state:/), + expect.any(String), + expect.objectContaining({ expirationTtl: 600 }), + ); + }); +}); + +describe('PackRatAuthHandler – /login', () => { + it('GET /login serves an HTML form', async () => { + const env = makeEnv(); + const res = await worker.fetch( + req('https://mcp.example.com/login?state=some-key'), + env, + fakeCtx, + ); + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toMatch(/text\/html/); + const body = await res.text(); + expect(body).toContain(' { + const stateKey = 'test-state-key'; + const env = makeEnv({ + [`oauth_state:${stateKey}`]: JSON.stringify({ + clientId: 'test-client', + scope: ['mcp'], + state: 'xyz', + redirectUri: 'https://client.example.com/cb', + responseType: 'code', + }), }); - it('returns 401 for empty Bearer token', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/mcp', { - headers: { Authorization: 'Bearer ' }, - }), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(401); + const origFetch = globalThis.fetch; + globalThis.fetch = vi + .fn() + .mockResolvedValue( + new Response( + JSON.stringify({ user: { id: 'user-123' }, session: { token: 'ba-token-abc' } }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ), + ) as unknown as typeof fetch; + + const form = new URLSearchParams({ + email: 'test@example.com', + password: 'secret', + state: stateKey, }); + const res = await worker.fetch( + req('https://mcp.example.com/login', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: form.toString(), + }), + env, + fakeCtx, + ); + + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toMatch(/\/callback\?state=/); + expect(env.OAUTH_KV.put).toHaveBeenCalledWith( + `session:${stateKey}`, + expect.stringContaining('ba-token-abc'), + expect.any(Object), + ); + + globalThis.fetch = origFetch; + }); - it('forwards request to McpAgent when valid Bearer token is provided', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/mcp', { - method: 'POST', - headers: { - Authorization: 'Bearer valid-jwt-token', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }), - }), - fakeEnv, - fakeCtx, - ); - // The mock McpAgent.serve returns 200 — auth guard passed through - expect(res.status).toBe(200); + it('POST /login with invalid credentials returns 401 HTML', async () => { + const stateKey = 'test-state-key'; + const env = makeEnv({ + [`oauth_state:${stateKey}`]: JSON.stringify({ + clientId: 'c', + scope: ['mcp'], + state: 'x', + redirectUri: 'https://x.com', + responseType: 'code', + }), }); - it('also forwards sub-paths of /mcp with valid auth', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/mcp/session/abc', { - headers: { Authorization: 'Bearer some-token' }, - }), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(200); + const origFetch = globalThis.fetch; + globalThis.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ error: 'invalid_credentials' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }), + ) as unknown as typeof fetch; + + const form = new URLSearchParams({ + email: 'bad@example.com', + password: 'wrong', + state: stateKey, }); + const res = await worker.fetch( + req('https://mcp.example.com/login', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: form.toString(), + }), + env, + fakeCtx, + ); + + expect(res.status).toBe(401); + const body = await res.text(); + expect(body).toContain('Invalid email or password'); + + globalThis.fetch = origFetch; }); +}); - describe('unknown paths', () => { - it('returns 404 for unknown paths', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/unknown'), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(404); - const body = (await res.json()) as Record; - expect(body.error).toBe('Not Found'); +describe('PackRatAuthHandler – /callback', () => { + it('completes OAuth authorization and redirects', async () => { + const stateKey = 'cb-state-key'; + const env = makeEnv({ + [`oauth_state:${stateKey}`]: JSON.stringify({ + clientId: 'test-client', + scope: ['mcp'], + state: 'xyz', + redirectUri: 'https://client.example.com/cb', + responseType: 'code', + }), + [`session:${stateKey}`]: JSON.stringify({ token: 'ba-token', userId: 'user-123' }), }); - it('returns 404 for /api paths', async () => { - const res = await worker.fetch( - makeRequest('https://worker.example.com/api/v1/packs'), - fakeEnv, - fakeCtx, - ); - expect(res.status).toBe(404); - }); + const res = await worker.fetch( + req(`https://mcp.example.com/callback?state=${stateKey}`), + env, + fakeCtx, + ); + + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toContain('code=abc'); + expect(env.OAUTH_PROVIDER.completeAuthorization).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 'user-123', + props: { betterAuthToken: 'ba-token', userId: 'user-123' }, + }), + ); + }); + + it('returns 400 when state is missing from KV', async () => { + const env = makeEnv(); // empty KV + + const res = await worker.fetch( + req('https://mcp.example.com/callback?state=nonexistent'), + env, + fakeCtx, + ); + + expect(res.status).toBe(400); + const body = (await res.json()) as Record; + expect(body.error).toBe('invalid_request'); + }); +}); + +describe('unknown paths', () => { + it('returns 404 for unknown paths', async () => { + const env = makeEnv(); + const res = await worker.fetch(req('https://mcp.example.com/unknown'), env, fakeCtx); + expect(res.status).toBe(404); }); }); diff --git a/packages/mcp/src/auth.ts b/packages/mcp/src/auth.ts new file mode 100644 index 0000000000..aff3cc0d39 --- /dev/null +++ b/packages/mcp/src/auth.ts @@ -0,0 +1,304 @@ +/** + * PackRat MCP OAuth 2.1 authorization handler. + * + * Implements the user-facing parts of the OAuth flow: + * GET /authorize → parse OAuth request, redirect to /login + * GET /login → serve sign-in form + * POST /login → call Better Auth API, store session, redirect to /callback + * GET /callback → complete authorization, redirect client back with auth code + * GET / → health check (also /health) + * + * KV layout (all keys expire after 10 minutes): + * oauth_state: → JSON-serialised AuthRequest from parseAuthRequest() + * session: → JSON { token: string, userId: string } + */ + +import { isString } from '@packrat/guards'; +import { createRegExp, exactly, global as globalFlag } from 'magic-regexp'; +import { z } from 'zod'; +import type { Env, Props } from './types'; + +// ── HTML-escape regexes (magic-regexp so the pre-push hook is satisfied) ───── +const AMP_RE = createRegExp(exactly('&'), [globalFlag]); +const LT_RE = createRegExp(exactly('<'), [globalFlag]); +const GT_RE = createRegExp(exactly('>'), [globalFlag]); +const QUOT_RE = createRegExp(exactly('"'), [globalFlag]); + +// ── Zod schemas for external data ───────────────────────────────────────────── + +const OAuthStateSchema = z.object({ + responseType: z.string(), + clientId: z.string(), + redirectUri: z.string(), + scope: z.array(z.string()), + state: z.string(), +}); + +const SessionKvSchema = z.object({ + token: z.string(), + userId: z.string(), +}); + +const SignInResponseSchema = z.object({ + session: z.object({ token: z.string() }).optional(), + user: z.object({ id: z.string() }).optional(), +}); + +// ── KV key helpers ──────────────────────────────────────────────────────────── + +const STATE_TTL = 600; // 10 minutes in seconds + +function oauthStateKey(key: string) { + return `oauth_state:${key}`; +} +function sessionKey(key: string) { + return `session:${key}`; +} + +// ── HTML helpers ────────────────────────────────────────────────────────────── + +function escapeHtml(s: string): string { + return s + .replace(AMP_RE, '&') + .replace(LT_RE, '<') + .replace(GT_RE, '>') + .replace(QUOT_RE, '"'); +} + +function loginPage(state: string, error?: string): string { + return ` + + + + + Sign in · PackRat + + + +

Sign in to PackRat

+

An MCP client is requesting access to your PackRat account.

+ ${error ? `
${escapeHtml(error)}
` : ''} +
+ + + + +
+ +`; +} + +/** FormData.get() returns FormDataEntryValue | null (string | File | null). Extract string only. */ +function getFormString(data: { get(name: string): string | File | null }, key: string): string { + const val = data.get(key); + return isString(val) ? val : ''; +} + +// ── Handler ─────────────────────────────────────────────────────────────────── + +export const PackRatAuthHandler = { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Health check + if (url.pathname === '/' || url.pathname === '/health') { + return Response.json({ + status: 'ok', + service: 'packrat-mcp', + version: '1.0.0', + transport: 'streamable-http', + endpoint: '/mcp', + docs: 'https://packrat.world/docs/mcp', + }); + } + + if (url.pathname === '/authorize') { + return handleAuthorize(request, env); + } + + if (url.pathname === '/login') { + return request.method === 'POST' ? handleLoginPost(request, env) : handleLoginGet(request); + } + + if (url.pathname === '/callback') { + return handleCallback(request, env); + } + + return Response.json({ error: 'Not Found' }, { status: 404 }); + }, +}; + +// ── /authorize ──────────────────────────────────────────────────────────────── + +async function handleAuthorize(request: Request, env: Env): Promise { + let oauthReq: z.infer; + try { + const parsed = await env.OAUTH_PROVIDER.parseAuthRequest(request); + const result = OAuthStateSchema.safeParse(parsed); + if (!result.success) throw new Error('Invalid OAuth request'); + oauthReq = result.data; + } catch { + return Response.json( + { error: 'invalid_request', error_description: 'Malformed authorization request' }, + { status: 400 }, + ); + } + + const stateKey = crypto.randomUUID(); + await env.OAUTH_KV.put(oauthStateKey(stateKey), JSON.stringify(oauthReq), { + expirationTtl: STATE_TTL, + }); + + const loginUrl = new URL('/login', request.url); + loginUrl.searchParams.set('state', stateKey); + return Response.redirect(loginUrl.toString(), 302); +} + +// ── /login GET ──────────────────────────────────────────────────────────────── + +function handleLoginGet(request: Request): Response { + const state = new URL(request.url).searchParams.get('state') ?? ''; + return new Response(loginPage(state), { + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); +} + +// ── /login POST ─────────────────────────────────────────────────────────────── + +async function handleLoginPost(request: Request, env: Env): Promise { + let email: string; + let password: string; + let state: string; + + try { + const form = await request.formData(); + email = getFormString(form, 'email'); + password = getFormString(form, 'password'); + state = getFormString(form, 'state'); + } catch { + return new Response(loginPage('', 'Invalid form submission.'), { + status: 400, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + if (!email || !password || !state) { + return new Response(loginPage(state, 'Email and password are required.'), { + status: 400, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + const oauthReqStr = await env.OAUTH_KV.get(oauthStateKey(state)); + if (!oauthReqStr) { + return new Response(loginPage(state, 'Session expired. Please start over.'), { + status: 400, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + let signInRes: Response; + try { + signInRes = await fetch(`${env.PACKRAT_API_URL}/api/auth/sign-in/email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + } catch { + return new Response(loginPage(state, 'Could not reach PackRat. Try again.'), { + status: 502, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + if (!signInRes.ok) { + return new Response(loginPage(state, 'Invalid email or password.'), { + status: 401, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + const signInResult = SignInResponseSchema.safeParse(await signInRes.json().catch(() => null)); + const betterAuthToken = signInResult.success ? signInResult.data.session?.token : undefined; + const userId = signInResult.success ? signInResult.data.user?.id : undefined; + + if (!betterAuthToken || !userId) { + return new Response(loginPage(state, 'Sign-in succeeded but session data was missing.'), { + status: 502, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }); + } + + await env.OAUTH_KV.put(sessionKey(state), JSON.stringify({ token: betterAuthToken, userId }), { + expirationTtl: STATE_TTL, + }); + + const callbackUrl = new URL('/callback', request.url); + callbackUrl.searchParams.set('state', state); + return Response.redirect(callbackUrl.toString(), 302); +} + +// ── /callback ───────────────────────────────────────────────────────────────── + +async function handleCallback(request: Request, env: Env): Promise { + const state = new URL(request.url).searchParams.get('state') ?? ''; + + const [oauthReqStr, sessionStr] = await Promise.all([ + env.OAUTH_KV.get(oauthStateKey(state)), + env.OAUTH_KV.get(sessionKey(state)), + ]); + + if (!oauthReqStr || !sessionStr) { + return Response.json( + { error: 'invalid_request', error_description: 'Invalid or expired state' }, + { status: 400 }, + ); + } + + const oauthReqResult = OAuthStateSchema.safeParse(JSON.parse(oauthReqStr)); + const sessionResult = SessionKvSchema.safeParse(JSON.parse(sessionStr)); + + if (!oauthReqResult.success || !sessionResult.success) { + return Response.json( + { error: 'invalid_request', error_description: 'Corrupted state data' }, + { status: 400 }, + ); + } + + const oauthReq = oauthReqResult.data; + const { token: betterAuthToken, userId } = sessionResult.data; + + // Clean up KV state (best-effort) + void Promise.all([ + env.OAUTH_KV.delete(oauthStateKey(state)), + env.OAUTH_KV.delete(sessionKey(state)), + ]); + + const props: Props = { betterAuthToken, userId }; + + const { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({ + request: oauthReq, + userId, + metadata: {}, + scope: oauthReq.scope, + props, + }); + + return Response.redirect(redirectTo, 302); +} diff --git a/packages/mcp/src/constants.ts b/packages/mcp/src/constants.ts index e89779737e..690acdd684 100644 --- a/packages/mcp/src/constants.ts +++ b/packages/mcp/src/constants.ts @@ -3,6 +3,11 @@ export const WorkerRoute = { Root: '/', Health: '/health', Mcp: '/mcp', + Authorize: '/authorize', + Login: '/login', + Callback: '/callback', + Token: '/token', + Register: '/register', } as const; /** Service identification metadata */ diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 57e439d49e..0cd3bafc0d 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -9,19 +9,25 @@ * - MCP resources: pack/trip/gear data accessible by URI * - Guided prompts: trip planning, pack optimization, gear recommendations * - Stateful sessions with hibernation (via Durable Objects) - * - JWT Bearer token authentication forwarded to PackRat API + * - OAuth 2.1 + PKCE authorization via @cloudflare/workers-oauth-provider * * Transport: Streamable HTTP (default) and SSE - * Auth: Authorization: Bearer * - * Connection URL: https://.workers.dev/mcp + * OAuth flow: + * GET /authorize → login form redirect + * POST /login → Better Auth sign-in, session stored in KV + * GET /callback → issue auth code, redirect to client + * POST /token → exchange code for access token (handled by OAuthProvider) + * POST /register → dynamic client registration (handled by OAuthProvider) */ +import { OAuthProvider } from '@cloudflare/workers-oauth-provider'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpAgent } from 'agents/mcp'; +import { z } from 'zod'; +import { PackRatAuthHandler } from './auth'; import type { PackRatApiClient } from './client'; import { createPackRatClient } from './client'; -import { ServiceMeta, WorkerRoute } from './constants'; import { registerPrompts } from './prompts'; import { registerResources } from './resources'; import { registerCatalogTools } from './tools/catalog'; @@ -31,24 +37,19 @@ import { registerTrailConditionTools } from './tools/trail-conditions'; import { registerTrailTools } from './tools/trails'; import { registerTripTools } from './tools/trips'; import { registerWeatherTools } from './tools/weather'; +import type { Env } from './types'; -// ── Environment type ────────────────────────────────────────────────────────── - -export interface Env { - /** Durable Object binding for MCP sessions */ - PackRatMCP: DurableObjectNamespace; - /** Base URL of the PackRat API (e.g. "https://packrat.world") */ - PACKRAT_API_URL: string; -} +// Re-export Env for consumers (e.g. tests) +export type { Env }; // ── Session state ───────────────────────────────────────────────────────────── export interface State { - /** JWT Bearer token extracted from the initial Authorization header */ + /** Better Auth session token, injected per-request from OAuth props or legacy Bearer header */ authToken: string; } -// ── MCP Agent ───────────────────────────────────────────────────────────────── +// ── MCP Agent (Durable Object) ──────────────────────────────────────────────── export class PackRatMCP extends McpAgent> { server = new McpServer({ @@ -58,10 +59,6 @@ export class PackRatMCP extends McpAgent> { initialState: State = { authToken: '' }; - /** - * Typed API client, lazily initialised on first use. - * Reads the current auth token from Durable Object state on every request. - */ private _api: PackRatApiClient | null = null; get api(): PackRatApiClient { @@ -71,19 +68,10 @@ export class PackRatMCP extends McpAgent> { return this._api; } - /** - * Override the DO's fetch to capture the Authorization header and persist - * it in state before the MCP protocol layer processes each message. - * This ensures tools always have access to the current session's auth token. - */ override async fetch(request: Request): Promise { const authHeader = request.headers.get('Authorization'); const token = authHeader?.match(BEARER_REGEX)?.[1] ?? ''; - // Persist the latest auth token in state (including clearing stale tokens - // when a request arrives without a valid bearer token). - // setState is synchronous — state is updated before super.fetch processes - // the MCP protocol message and calls any tool handlers. if (token !== this.state.authToken) { this.setState({ ...this.state, authToken: token }); } @@ -91,10 +79,6 @@ export class PackRatMCP extends McpAgent> { return super.fetch(request); } - /** - * Called once when the Durable Object starts up. - * Register all tools, resources, and prompts here. - */ async init(): Promise { registerPackTools(this); registerCatalogTools(this); @@ -108,70 +92,58 @@ export class PackRatMCP extends McpAgent> { } } -// ── Worker entry point ──────────────────────────────────────────────────────── +// ── Constants ───────────────────────────────────────────────────────────────── const BEARER_REGEX = /^Bearer\s+(\S+)/i; -/** - * The Cloudflare Worker fetch handler. - * - * Validates the Authorization header before routing to the McpAgent Durable Object. - * The token is forwarded via the request and stored in DO state for tool calls. - */ -const mcpHandler = PackRatMCP.serve('/mcp'); - -export default { - fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise { - const url = new URL(request.url); - - // ── Health check ────────────────────────────────────────────────────── - if (url.pathname === WorkerRoute.Root || url.pathname === WorkerRoute.Health) { - return Response.json({ - status: 'ok', - service: ServiceMeta.Name, - version: ServiceMeta.Version, - transport: ServiceMeta.Transport, - endpoint: WorkerRoute.Mcp, - docs: 'https://packrat.world/docs/mcp', - }); - } +const mcpDoHandler = PackRatMCP.serve('/mcp'); + +// ── Props schema (OAuthProvider injects this at runtime via ctx) ────────────── + +const PropsSchema = z.object({ + betterAuthToken: z.string(), + userId: z.string(), +}); + +// ── API handler: wraps McpAgent, injecting the Better Auth token from OAuth props ── - // ── MCP endpoint ────────────────────────────────────────────────────── - if (url.pathname === WorkerRoute.Mcp || url.pathname.startsWith(`${WorkerRoute.Mcp}/`)) { - const authHeader = request.headers.get('Authorization'); - const token = authHeader?.match(BEARER_REGEX)?.[1] ?? ''; - - if (!token) { - return Response.json( - { - error: 'Unauthorized', - message: - 'Provide your PackRat JWT as: Authorization: Bearer . ' + - 'Get your token from https://packrat.world/settings/api', - }, - { - status: 401, - headers: { - 'WWW-Authenticate': 'Bearer realm="PackRat MCP Server"', - 'Content-Type': 'application/json', - }, - }, - ); - } - - return mcpHandler.fetch(request, env, ctx); +const mcpApiHandler = { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const rawCtx = ctx as unknown as Record; // safe-cast: OAuth provider injects props at runtime; ExecutionContext has no index signature + const propsResult = PropsSchema.safeParse(rawCtx.props); + const token = propsResult.success ? propsResult.data.betterAuthToken : ''; + + const headers = new Headers(request.headers); + if (token) { + headers.set('Authorization', `Bearer ${token}`); } - // ── 404 ─────────────────────────────────────────────────────────────── - return Response.json( - { - error: 'Not Found', - availableEndpoints: [ - { method: 'GET', path: WorkerRoute.Root, description: 'Health check' }, - { method: '*', path: WorkerRoute.Mcp, description: 'MCP endpoint (Streamable HTTP)' }, - ], - }, - { status: 404 }, - ); + return mcpDoHandler.fetch(new Request(request, { headers }), env, ctx); }, }; + +// ── OAuthProvider — the Worker entrypoint ───────────────────────────────────── + +export default new OAuthProvider({ + // /mcp and sub-paths are API routes: require a valid access token + apiRoute: '/mcp', + apiHandler: mcpApiHandler, + + // All other routes (/, /health, /authorize, /login, /callback) go to the auth handler + defaultHandler: PackRatAuthHandler, + + // OAuth 2.1 endpoints (token + register are served by OAuthProvider itself) + authorizeEndpoint: '/authorize', + tokenEndpoint: '/token', + clientRegistrationEndpoint: '/register', + + // Security: S256 PKCE only; no implicit flow + allowPlainPKCE: false, + allowImplicitFlow: false, + + // Token lifetimes: 60-min access tokens, 30-day refresh tokens + accessTokenTTL: 3600, + refreshTokenTTL: 2592000, + + scopesSupported: ['mcp'], +}); diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index f89b53aa87..327a608d8e 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -5,6 +5,8 @@ * the circular dependency: index.ts → tools/* → index.ts. * PackRatMCP satisfies this interface structurally via its `server` and `api` fields. */ + +import type { OAuthHelpers } from '@cloudflare/workers-oauth-provider'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { PackRatApiClient } from './client'; @@ -12,3 +14,25 @@ export interface AgentContext { server: McpServer; api: PackRatApiClient; } + +/** Cloudflare Worker environment bindings */ +export interface Env { + /** Durable Object binding for MCP sessions */ + PackRatMCP: DurableObjectNamespace; + /** Base URL of the PackRat API (e.g. "https://packrat.world") */ + PACKRAT_API_URL: string; + /** KV namespace for OAuth token storage (required by workers-oauth-provider) */ + OAUTH_KV: KVNamespace; + /** OAuth helpers injected by OAuthProvider at runtime */ + OAUTH_PROVIDER: OAuthHelpers; + /** Optional pre-shared secret for dynamic client registration */ + MCP_INITIAL_ACCESS_TOKEN?: string; +} + +/** Properties embedded in OAuth access tokens and passed to API handlers */ +export interface Props { + /** Better Auth session token used to authenticate PackRat API calls */ + betterAuthToken: string; + /** PackRat user ID */ + userId: string; +} diff --git a/packages/mcp/wrangler.jsonc b/packages/mcp/wrangler.jsonc index 8f6c443b68..2afe42186a 100644 --- a/packages/mcp/wrangler.jsonc +++ b/packages/mcp/wrangler.jsonc @@ -10,6 +10,15 @@ "enabled": true, "head_sampling_rate": 1 }, + // KV namespace for OAuth token storage (required by @cloudflare/workers-oauth-provider) + // Create with: wrangler kv namespace create OAUTH_KV + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "__TODO_OAUTH_KV_PROD_ID__", + "preview_id": "__TODO_OAUTH_KV_DEV_ID__" + } + ], // Durable Objects power each MCP session (stateful per client connection) "durable_objects": { "bindings": [ @@ -28,9 +37,17 @@ ], // Environment variables are set via Cloudflare dashboard or .dev.vars locally // Required: PACKRAT_API_URL + // Optional: MCP_INITIAL_ACCESS_TOKEN (pre-shared secret for dynamic client registration) "env": { "dev": { "name": "packrat-mcp-dev", + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "__TODO_OAUTH_KV_DEV_ID__", + "preview_id": "__TODO_OAUTH_KV_DEV_ID__" + } + ], "durable_objects": { "bindings": [ { diff --git a/patches/@packrat-ai+nativewindui@1.1.0.patch b/patches/@packrat-ai+nativewindui@1.1.0.patch deleted file mode 100644 index ee48dd1fa5..0000000000 --- a/patches/@packrat-ai+nativewindui@1.1.0.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/src/components/List.tsx b/src/components/List.tsx -index e22bc13..7b6d0e0 100644 ---- a/src/components/List.tsx -+++ b/src/components/List.tsx -@@ -31,7 +31,7 @@ - type ListDataItem = string | { title: string; subTitle?: string }; - type ListVariant = 'insets' | 'full-width'; - --type ListRef = React.Ref>; -+type ListRef = React.Ref>; - - type ListRenderItemProps = ListRenderItemInfo & { - variant?: ListVariant;