fix(desktop): scope account access to owning client and extend identity verification
Backend CI / Backend (push) Successful in 14m18s

Restrict account tracking, probing, and health reporting to accounts owned
by the current client (client_id match). Extend identity-match verification
to bilibili, juejin, and smzdm so session validity is confirmed locally
rather than deferred to remote health state. Server-side account view now
uses stored client_id as authoritative owner instead of presence data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Xu Liang
2026-05-05 22:09:10 +08:00
parent 337bb6f9ac
commit 680adf7b93
7 changed files with 151 additions and 34 deletions
+39 -3
View File
@@ -222,6 +222,8 @@ type BilibiliSpaceInfoResponse = {
} }
type JuejinProfileResponse = { type JuejinProfileResponse = {
err_no?: number
err_msg?: string
data?: { data?: {
bui_user?: { bui_user?: {
user_id?: string user_id?: string
@@ -2540,6 +2542,16 @@ export async function silentRefreshPublishAccountSession(
return await verifyZolConsoleAccess(window, handle.session, account) return await verifyZolConsoleAccess(window, handle.session, account)
} }
if (platformRequiresDetectedAccountIdentityMatch(account.platform)) {
const detected = await definition
.detect({
session: handle.session,
webContents: window.webContents,
})
.catch(() => null)
return detected ? detectedAccountMatchesIdentity(detected, account) : false
}
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
const detected = await definition const detected = await definition
.detect({ .detect({
@@ -2860,7 +2872,14 @@ function platformRequiresDetectedAccountForConsoleAccess(platformId: string): bo
} }
function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean { function platformRequiresDetectedAccountIdentityMatch(platformId: string): boolean {
return platformId === 'qiehao' || platformId === 'wangyihao' || platformId === 'zol' return (
platformId === 'qiehao' ||
platformId === 'wangyihao' ||
platformId === 'zol' ||
platformId === 'bilibili' ||
platformId === 'juejin' ||
platformId === 'smzdm'
)
} }
async function verifyZolConsoleAccess( async function verifyZolConsoleAccess(
@@ -2980,6 +2999,16 @@ async function verifyPublishAccountConsoleAccess(
return await verifyZolConsoleAccess(window, handle.session, account) return await verifyZolConsoleAccess(window, handle.session, account)
} }
if (platformRequiresDetectedAccountIdentityMatch(account.platform)) {
const detected = await definition
.detect({
session: handle.session,
webContents: window.webContents,
})
.catch(() => null)
return detected ? detectedAccountMatchesIdentity(detected, account) : false
}
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
const detected = await definition const detected = await definition
.detect({ .detect({
@@ -3207,6 +3236,10 @@ async function detectJuejin({ session }: DetectContext): Promise<DetectedAccount
}, },
).catch(() => null) ).catch(() => null)
if (!response || (typeof response.err_no === 'number' && response.err_no !== 0)) {
return null
}
const user = response?.data?.bui_user const user = response?.data?.bui_user
if (!user?.user_id || !user.screen_name) { if (!user?.user_id || !user.screen_name) {
return null return null
@@ -3467,7 +3500,7 @@ async function detectSmzdm({
return fromCookieRequest return fromCookieRequest
} }
return await deriveSmzdmCookieAccount(session, pageState) return null
} }
async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAccount | null> { async function detectWeixinGzh({ session }: DetectContext): Promise<DetectedAccount | null> {
@@ -4012,7 +4045,10 @@ export async function openPublishAccountConsole(account: PublishAccountIdentity)
throw new Error(`desktop_platform_not_supported:${account.platform}`) throw new Error(`desktop_platform_not_supported:${account.platform}`)
} }
const handle = await ensurePublishAccountSessionHandle(account) const handle = await findLocalPublishAccountSessionHandle(account)
if (!handle) {
throw new Error(`desktop_account_session_expired:${account.platform}`)
}
const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session) const consoleURL = await resolvePublishAccountConsoleURL(definition, handle.session)
if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) { if (platformRequiresDetectedAccountForConsoleAccess(account.platform)) {
const detected = await definition const detected = await definition
+48 -1
View File
@@ -98,6 +98,10 @@ function resolvePartition(accountId: string): string {
) )
} }
function resolveKnownPartition(accountId: string): string | null {
return getSessionHandle(accountId)?.partition ?? getPersistedPartition(accountId) ?? null
}
function nextInitialProbeAt(now = Date.now()): number { function nextInitialProbeAt(now = Date.now()): number {
return now return now
} }
@@ -375,7 +379,19 @@ async function performProbeLocked(
record.probeState = 'probing' record.probeState = 'probing'
commitRecord(record, previousSignature) commitRecord(record, previousSignature)
const partition = resolvePartition(account.id) const partition = resolveKnownPartition(account.id)
if (!partition) {
return applyExpiredResult(
record,
{
verdict: 'expired',
reason: 'missing_partition',
profile: null,
sessionFingerprint: null,
},
Date.now(),
)
}
if (options.allowSilentRefresh) { if (options.allowSilentRefresh) {
const refreshed = await adapter.silentRefresh(account, partition).catch(() => false) const refreshed = await adapter.silentRefresh(account, partition).catch(() => false)
@@ -748,6 +764,37 @@ export function markTrackedAccountBound(
return snapshot return snapshot
} }
export function markTrackedAccountMissingPartition(
account: PublishAccountIdentity,
): AccountHealthSnapshot {
const record = ensureRecord(account)
const previousSignature = snapshotSignature(record)
const now = Date.now()
trackedAccounts.delete(account.id)
record.authRevision += 1
record.platform = account.platform
record.authState = 'expired'
record.probeState = 'idle'
record.authReason = 'missing_partition'
record.lastProbeAt = now
record.nextProbeAt = null
record.lastAuthFailureAt = now
record.profile = null
record.sessionFingerprint = null
record.suspectedExpiredAt = null
record.confirmFailureCount = 0
const snapshot = commitRecord(record, previousSignature)
const queuedJob = probeJobs.get(account.id)
if (queuedJob?.status === 'queued') {
removeQueuedProbeJob(account.id)
queuedJob.resolve(snapshot)
}
return snapshot
}
export function reconcileTrackedAccountRemoteState( export function reconcileTrackedAccountRemoteState(
account: PublishAccountIdentity, account: PublishAccountIdentity,
input: { input: {
@@ -214,6 +214,9 @@ async function fetchCurrentAccount(context: PublishAdapterContext): Promise<Smzd
} }
} }
return null
/*
const cookieUid = normalizeSmzdmUid( const cookieUid = normalizeSmzdmUid(
await sessionCookieValue(context.session, 'smzdm.com', 'smzdm_id').catch(() => ''), await sessionCookieValue(context.session, 'smzdm.com', 'smzdm_id').catch(() => ''),
) )
@@ -229,6 +232,7 @@ async function fetchCurrentAccount(context: PublishAdapterContext): Promise<Smzd
uid: cookieUid, uid: cookieUid,
name: cookieName || `值友${cookieUid}`, name: cookieName || `值友${cookieUid}`,
} }
*/
} }
export function extractSmzdmArticleIdFromHref(value: string | null | undefined): string { export function extractSmzdmArticleIdFromHref(value: string | null | undefined): string {
@@ -28,6 +28,7 @@ import {
forgetTrackedAccountHealth, forgetTrackedAccountHealth,
getAccountHealthSnapshot, getAccountHealthSnapshot,
getProjectedAccountHealth, getProjectedAccountHealth,
markTrackedAccountMissingPartition,
markTrackedAccountBound, markTrackedAccountBound,
probeTrackedAccount, probeTrackedAccount,
reconcileTrackedAccountRemoteState, reconcileTrackedAccountRemoteState,
@@ -319,8 +320,24 @@ function hasLocalAccountSession(accountId: string): boolean {
return Boolean(getSessionHandle(accountId) || getPersistedPartition(accountId)) return Boolean(getSessionHandle(accountId) || getPersistedPartition(accountId))
} }
function isAccountOwnedByCurrentClient(account: DesktopAccountInfo): boolean {
return Boolean(account.client_id && account.client_id === state.client?.id)
}
function hasLocalAccountAuthorization(account: DesktopAccountInfo): boolean {
return isAccountOwnedByCurrentClient(account)
}
function accountRequiresLocalAuthorization(account: DesktopAccountInfo): boolean {
return Boolean(account.client_id)
}
function accountRequiresStrictProbe(account: DesktopAccountInfo): boolean {
return ['bilibili', 'juejin', 'smzdm'].includes(account.platform)
}
function shouldReportAccountPresence(account: DesktopAccountInfo): boolean { function shouldReportAccountPresence(account: DesktopAccountInfo): boolean {
return hasLocalAccountSession(account.id) || account.client_id === state.client?.id return hasLocalAccountSession(account.id) && isAccountOwnedByCurrentClient(account)
} }
function runtimePlatformLabel(platform: string): string { function runtimePlatformLabel(platform: string): string {
@@ -955,7 +972,9 @@ async function syncAccounts(source: 'startup' | 'manual'): Promise<void> {
noteSchedulerAccountsSync() noteSchedulerAccountsSync()
setSchedulerError(null) setSchedulerError(null)
const identities = accounts.map((account) => accountIdentityFromDesktopAccount(account)) const identities = accounts
.filter((account) => hasLocalAccountAuthorization(account))
.map((account) => accountIdentityFromDesktopAccount(account))
syncTrackedAccounts(identities) syncTrackedAccounts(identities)
state.accountRemoteHealth = new Map(accounts.map((account) => [account.id, account.health])) state.accountRemoteHealth = new Map(accounts.map((account) => [account.id, account.health]))
@@ -968,15 +987,19 @@ async function syncAccounts(source: 'startup' | 'manual'): Promise<void> {
...account, ...account,
platform_uid: normalizedPlatformUid, platform_uid: normalizedPlatformUid,
}) })
reconcileTrackedAccountRemoteState(accountIdentity, { if (hasLocalAccountAuthorization(account) && !accountRequiresStrictProbe(account)) {
health: account.health, reconcileTrackedAccountRemoteState(accountIdentity, {
verifiedAt: account.verified_at, health: account.health,
profile: { verifiedAt: account.verified_at,
subjectUid: normalizedPlatformUid, profile: {
displayName: account.display_name, subjectUid: normalizedPlatformUid,
avatarUrl: account.avatar_url, displayName: account.display_name,
}, avatarUrl: account.avatar_url,
}) },
})
} else if (accountRequiresLocalAuthorization(account)) {
markTrackedAccountMissingPartition(accountIdentity)
}
const projected = getProjectedAccountHealth({ const projected = getProjectedAccountHealth({
accountId: account.id, accountId: account.id,
platform: account.platform, platform: account.platform,
@@ -1140,6 +1163,9 @@ function enqueueAccountHealthReport(accountId: string): void {
if (!account) { if (!account) {
return return
} }
if (!hasLocalAccountAuthorization(account)) {
return
}
const report = buildAccountHealthReport(account) const report = buildAccountHealthReport(account)
if (!shouldQueueAccountHealthReport(account, report)) { if (!shouldQueueAccountHealthReport(account, report)) {
@@ -1270,6 +1296,10 @@ export async function requestRuntimeAccountProbe(
if (!account) { if (!account) {
throw new Error('runtime_account_not_found') throw new Error('runtime_account_not_found')
} }
if (existingAccount && !hasLocalAccountAuthorization(existingAccount)) {
markTrackedAccountMissingPartition(account)
return
}
try { try {
await probeTrackedAccount(account, { await probeTrackedAccount(account, {
@@ -1318,7 +1348,11 @@ export function noteRuntimeAccountBound(account: DesktopAccountInfo): void {
avatarUrl: normalizedAccount.avatar_url, avatarUrl: normalizedAccount.avatar_url,
}) })
state.lastAccountsSyncAt = Date.now() state.lastAccountsSyncAt = Date.now()
syncTrackedAccounts(state.accounts.map((item) => accountIdentityFromDesktopAccount(item))) syncTrackedAccounts(
state.accounts
.filter((item) => hasLocalAccountAuthorization(item))
.map((item) => accountIdentityFromDesktopAccount(item)),
)
markTrackedAccountBound(accountIdentityFromDesktopAccount(normalizedAccount), { markTrackedAccountBound(accountIdentityFromDesktopAccount(normalizedAccount), {
subjectUid: normalizedAccount.platform_uid, subjectUid: normalizedAccount.platform_uid,
displayName: normalizedAccount.display_name, displayName: normalizedAccount.display_name,
@@ -57,6 +57,9 @@ function createRuntimeAccountView(
health: account.health, health: account.health,
verifiedAt: account.verified_at, verifiedAt: account.verified_at,
}) })
const belongsToCurrentClient = Boolean(
account.client_id && account.client_id === context.primaryClientId,
)
return { return {
id: account.id, id: account.id,
@@ -70,7 +73,7 @@ function createRuntimeAccountView(
authReason: projected.authReason, authReason: projected.authReason,
tags: account.tags, tags: account.tags,
syncVersion: account.sync_version, syncVersion: account.sync_version,
clientId: account.client_id ?? context.primaryClientId, clientId: account.client_id ?? '',
partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`, partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`,
sessionState: isHot ? 'hot' : sessionHandle ? 'warm' : 'cold', sessionState: isHot ? 'hot' : sessionHandle ? 'warm' : 'cold',
lastSyncAt: context.lastAccountsSyncAt || parseTimestamp(account.verified_at) || context.now, lastSyncAt: context.lastAccountsSyncAt || parseTimestamp(account.verified_at) || context.now,
@@ -78,7 +81,7 @@ function createRuntimeAccountView(
lastProbeAt: projected.lastProbeAt, lastProbeAt: projected.lastProbeAt,
nextProbeAt: projected.nextProbeAt, nextProbeAt: projected.nextProbeAt,
queueDepth: accountQueueDepth, queueDepth: accountQueueDepth,
online: context.clientOnline, online: context.clientOnline && belongsToCurrentClient,
} }
} }
@@ -557,26 +557,19 @@ func (s *DesktopAccountService) buildDesktopAccountView(
var clientLastSeenAt *time.Time var clientLastSeenAt *time.Time
var clientDeviceName *string var clientDeviceName *string
resolvedClientID := account.ClientID if account.ClientID != nil {
if presenceClientMap != nil { value := account.ClientID.String()
if activeClientID, ok := presenceClientMap[account.DesktopID]; ok {
resolvedClientID = &activeClientID
}
}
if resolvedClientID != nil {
value := resolvedClientID.String()
clientID = &value clientID = &value
online := false online := false
clientOnline = &online clientOnline = &online
if clientMap != nil { if clientMap != nil {
if client, ok := clientMap[*resolvedClientID]; ok && client != nil { if client, ok := clientMap[*account.ClientID]; ok && client != nil {
clientLastSeenAt = client.LastSeenAt clientLastSeenAt = client.LastSeenAt
clientDeviceName = client.DeviceName clientDeviceName = client.DeviceName
if presenceClientMap != nil { if presenceClientMap != nil {
if activeClientID, ok := presenceClientMap[account.DesktopID]; ok && activeClientID == *resolvedClientID { if activeClientID, ok := presenceClientMap[account.DesktopID]; ok && activeClientID == *account.ClientID {
online = true online = true
} }
} }
@@ -45,7 +45,7 @@ func TestResolveDesktopClientOnline_StaleHeartbeatIsOffline(t *testing.T) {
assert.False(t, online) assert.False(t, online)
} }
func TestBuildDesktopAccountView_PrefersAccountPresenceClient(t *testing.T) { func TestBuildDesktopAccountView_StoredClientOwnershipWinsOverPresence(t *testing.T) {
service := &DesktopAccountService{} service := &DesktopAccountService{}
accountID := uuid.New() accountID := uuid.New()
staleClientID := uuid.New() staleClientID := uuid.New()
@@ -75,14 +75,14 @@ func TestBuildDesktopAccountView_PrefersAccountPresenceClient(t *testing.T) {
) )
if assert.NotNil(t, view.ClientID) { if assert.NotNil(t, view.ClientID) {
assert.Equal(t, activeClientID.String(), *view.ClientID) assert.Equal(t, staleClientID.String(), *view.ClientID)
} }
if assert.NotNil(t, view.ClientOnline) { if assert.NotNil(t, view.ClientOnline) {
assert.True(t, *view.ClientOnline) assert.False(t, *view.ClientOnline)
} }
assert.Equal(t, "4181862206", view.PlatformUID) assert.Equal(t, "4181862206", view.PlatformUID)
assert.Equal(t, &lastSeen, view.ClientLastSeenAt) assert.Nil(t, view.ClientLastSeenAt)
assert.Equal(t, &deviceName, view.ClientDeviceName) assert.Nil(t, view.ClientDeviceName)
} }
func TestBuildDesktopAccountView_FallsBackToStoredClientWhenNoPresence(t *testing.T) { func TestBuildDesktopAccountView_FallsBackToStoredClientWhenNoPresence(t *testing.T) {