Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
diff --git a/node_modules/@atproto/api/dist/moderation/decision.js b/node_modules/@atproto/api/dist/moderation/decision.js
index aaac177..d27c0be 100644
index 6dd9d4d..59df896 100644
--- a/node_modules/@atproto/api/dist/moderation/decision.js
+++ b/node_modules/@atproto/api/dist/moderation/decision.js
@@ -67,6 +67,8 @@ class ModerationDecision {
@@ -67,6 +67,10 @@ class ModerationDecision {
ui(context) {
const ui = new ui_1.ModerationUI();
for (const cause of this.causes) {
+ if (cause?.label?.val === '!no-unauthenticated') continue;
+ // Skip !no-unauthenticated label only if user is authenticated
+ // If userDid is present in opts, user is logged in
+ if (cause?.label?.val === '!no-unauthenticated' && this.opts?.userDid) continue;
+
if (cause.type === 'blocking' ||
cause.type === 'blocked-by' ||
Expand Down
23 changes: 14 additions & 9 deletions src/state/queries/microcosm-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,26 +118,31 @@ export async function fetchConstellationCounts(
*
* IMPORTANT: This determines whether fallback should be triggered.
* We should NOT trigger fallback for intentional blocking/privacy errors.
*
* SECURITY: We distinguish between:
* - AppView failures (suspended users, server errors) → trigger fallback
* - Access denials (privacy settings, blocking) → respect AppView decision
*/
export function isAppViewError(error: any): boolean {
if (!error) return false

const msg = error.message?.toLowerCase() || ''

// Do NOT trigger fallback for intentional blocking
// "Requester has blocked actor" means the user intentionally blocked someone
// This is NOT an AppView outage - it's privacy enforcement
// Do NOT trigger fallback for intentional blocking/privacy enforcement
// These are NOT AppView outages - they are access control decisions
if (msg.includes('blocked actor')) return false
if (msg.includes('requester has blocked')) return false
if (msg.includes('blocking')) return false
if (msg.includes('not available')) return false // Privacy: logged-out visibility
if (msg.includes('logged out')) return false // Privacy: requires authentication
if (msg.includes('requires auth')) return false // Privacy: authentication required
if (msg.includes('unauthorized')) return false // Privacy: access denied

// Check HTTP status codes
if (error.status === 400 || error.status === 404) return true

// Check error messages for actual AppView issues
if (msg.includes('not found')) return true
// Trigger fallback for actual AppView failures (suspended users, etc.)
// Note: We removed blanket 400/404 handling to avoid bypassing access controls
if (msg.includes('suspended')) return true
if (msg.includes('could not locate')) return true
if (msg.includes('could not locate record')) return true
if (msg.includes('profile not found')) return true

return false
}
Expand Down
56 changes: 46 additions & 10 deletions src/state/queries/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ import {
type ComAtprotoRepoUploadBlob,
type Un$Typed,
} from '@atproto/api'
import {
keepPreviousData,
prefetchQueryWithFallback,
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from './useQueryWithFallback'

import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
Expand All @@ -42,6 +34,14 @@ import {
import {RQKEY_ROOT as RQKEY_LIST_CONVOS} from './messages/list-conversations'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {
keepPreviousData,
prefetchQueryWithFallback,
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from './useQueryWithFallback'

export * from '#/state/queries/unstable-profile-cache'
/**
Expand Down Expand Up @@ -354,11 +354,21 @@ function useProfileUnfollowMutation(
logContext: LogEvents['profile:unfollow']['logContext'],
) {
const agent = useAgent()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
logEvent('profile:unfollow', {logContext})
return await agent.deleteFollow(followUri)
},
onSuccess(_, {did}) {
// Invalidate profile and feed caches to reflect unfollow
resetProfilePostsQueries(queryClient, did, 1000)

// Add delay to handle AppView indexing lag
setTimeout(() => {
queryClient.invalidateQueries({queryKey: RQKEY(did)})
}, 500)
},
})
}

Expand Down Expand Up @@ -418,8 +428,21 @@ function useProfileMuteMutation() {
mutationFn: async ({did}) => {
await agent.mute(did)
},
onSuccess() {
onSuccess(_, {did}) {
// Invalidate mute list cache
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})

// Invalidate profile and feed caches to reflect mute
resetProfilePostsQueries(queryClient, did, 1000)
queryClient.invalidateQueries({queryKey: RQKEY(did)})

// Invalidate feeds that might contain muted user's posts
queryClient.invalidateQueries({queryKey: ['post-feed']})

// Add delay to handle AppView indexing lag
setTimeout(() => {
queryClient.invalidateQueries({queryKey: RQKEY(did)})
}, 500)
},
})
}
Expand All @@ -431,8 +454,21 @@ function useProfileUnmuteMutation() {
mutationFn: async ({did}) => {
await agent.unmute(did)
},
onSuccess() {
onSuccess(_, {did}) {
// Invalidate mute list cache
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})

// Invalidate profile and feed caches to reflect unmute
resetProfilePostsQueries(queryClient, did, 1000)
queryClient.invalidateQueries({queryKey: RQKEY(did)})

// Invalidate feeds that might contain unmuted user's posts
queryClient.invalidateQueries({queryKey: ['post-feed']})

// Add delay to handle AppView indexing lag
setTimeout(() => {
queryClient.invalidateQueries({queryKey: RQKEY(did)})
}, 500)
},
})
}
Expand Down
21 changes: 21 additions & 0 deletions src/state/queries/useQueryWithFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type UseQueryResult,
} from '@tanstack/react-query'

import {useSession} from '#/state/session'
import {
buildSyntheticFeedPage,
buildSyntheticPostView,
Expand Down Expand Up @@ -109,6 +110,7 @@ export function useQuery<TData = unknown, TError = Error>(
} = options

const queryClient = useQueryClient()
const {hasSession} = useSession()

// Wrap the original queryFn with fallback logic
const wrappedQueryFn: typeof queryFn = async context => {
Expand All @@ -123,6 +125,15 @@ export function useQuery<TData = unknown, TError = Error>(
throw error
}

// SECURITY: Do NOT trigger fallback for logged-out users
// This prevents bypassing AppView access controls like logged-out visibility settings
if (!hasSession) {
console.log(
'[Fallback] Skipping fallback for logged-out user (respecting access controls)',
)
throw error
}

console.log('[Fallback] Attempting PDS + Microcosm fallback:', {
fallbackType,
fallbackIdentifier,
Expand Down Expand Up @@ -210,6 +221,7 @@ export function useInfiniteQuery<
} = options

const queryClient = useQueryClient()
const {hasSession} = useSession()

// Wrap the original queryFn with fallback logic
const wrappedQueryFn = async (
Expand All @@ -226,6 +238,15 @@ export function useInfiniteQuery<
throw error
}

// SECURITY: Do NOT trigger fallback for logged-out users
// This prevents bypassing AppView access controls like logged-out visibility settings
if (!hasSession) {
console.log(
'[Fallback] Skipping fallback for logged-out user (respecting access controls)',
)
throw error
}

const pageParam = 'pageParam' in context ? context.pageParam : undefined

console.log(
Expand Down
Loading