2026-04-23 22:54:56 +08:00
|
|
|
function normalizeText(value: unknown): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
const trimmed = value.trim()
|
|
|
|
|
return trimmed ? trimmed : null
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hashText(value: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
let hash = 0
|
2026-04-23 22:54:56 +08:00
|
|
|
for (let index = 0; index < value.length; index += 1) {
|
2026-05-01 20:39:09 +08:00
|
|
|
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
return Math.abs(hash).toString(16).padStart(8, '0')
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function boundedIdentity(prefix: string, raw: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
const normalizedPrefix = prefix.trim().replace(/:+$/, '')
|
|
|
|
|
const normalizedRaw = raw.trim()
|
2026-04-23 22:54:56 +08:00
|
|
|
if (!normalizedRaw) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return `${normalizedPrefix}:${hashText(prefix)}`
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const candidate = `${normalizedPrefix}:${normalizedRaw}`
|
2026-04-23 22:54:56 +08:00
|
|
|
if (candidate.length <= 120) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return candidate
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
return `${normalizedPrefix}:${hashText(normalizedRaw)}`
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeURLPath(pathname: string): string {
|
2026-05-01 20:39:09 +08:00
|
|
|
const normalized = pathname.replace(/\/+$/, '')
|
|
|
|
|
return normalized === '/' ? '' : normalized
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function safeParseURL(input: string): URL | null {
|
|
|
|
|
try {
|
2026-05-01 20:39:09 +08:00
|
|
|
return new URL(input)
|
2026-04-23 22:54:56 +08:00
|
|
|
} catch {
|
2026-05-01 20:39:09 +08:00
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function genericAIConversationPath(pathname: string): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
return /^\/(?:chat|c|conversation)(?:\/|$)/i.test(pathname)
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GenericAIPageState {
|
2026-05-01 20:39:09 +08:00
|
|
|
displayName: string | null
|
|
|
|
|
avatarUrl: string | null
|
|
|
|
|
fingerprint: string | null
|
|
|
|
|
credentialFingerprint: string | null
|
|
|
|
|
currentPath: string | null
|
|
|
|
|
strongAuthSignalCount: number
|
|
|
|
|
loginSignalCount: number
|
|
|
|
|
loggedOutSignalCount: number
|
|
|
|
|
challengeSignalCount: number
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function genericAIPageLooksAuthenticated(
|
|
|
|
|
currentURL: string,
|
|
|
|
|
pageState: GenericAIPageState | null,
|
|
|
|
|
): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
const current = safeParseURL(currentURL)
|
|
|
|
|
const currentPath = normalizeURLPath(current?.pathname ?? pageState?.currentPath ?? '')
|
|
|
|
|
const strongAuthSignalCount = pageState?.strongAuthSignalCount ?? 0
|
|
|
|
|
const loginSignalCount = pageState?.loginSignalCount ?? 0
|
|
|
|
|
const loggedOutSignalCount = pageState?.loggedOutSignalCount ?? 0
|
|
|
|
|
const challengeSignalCount = pageState?.challengeSignalCount ?? 0
|
|
|
|
|
const hasCredentialFingerprint = Boolean(normalizeText(pageState?.credentialFingerprint))
|
|
|
|
|
const onConversationPath = genericAIConversationPath(currentPath)
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
|
|
|
if (challengeSignalCount > 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return false
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loggedOutSignalCount > 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return false
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
if (
|
|
|
|
|
loginSignalCount > 0 &&
|
|
|
|
|
strongAuthSignalCount === 0 &&
|
|
|
|
|
!onConversationPath &&
|
|
|
|
|
!hasCredentialFingerprint
|
|
|
|
|
) {
|
|
|
|
|
return false
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loginSignalCount > 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return (strongAuthSignalCount >= 2 || hasCredentialFingerprint) && !onConversationPath
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
return strongAuthSignalCount > 0 || onConversationPath || hasCredentialFingerprint
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildGenericAIPageFingerprintFallback(
|
|
|
|
|
platformId: string,
|
|
|
|
|
pageState?: GenericAIPageState | null,
|
|
|
|
|
): string | null {
|
2026-05-01 20:39:09 +08:00
|
|
|
const credentialFingerprint = normalizeText(pageState?.credentialFingerprint)
|
2026-04-23 22:54:56 +08:00
|
|
|
if (credentialFingerprint) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return boundedIdentity(`${platformId}-session`, credentialFingerprint)
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const fallbackFingerprint = normalizeText(pageState?.fingerprint)
|
2026-04-23 22:54:56 +08:00
|
|
|
if (!fallbackFingerprint) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const strongAuthSignalCount = pageState?.strongAuthSignalCount ?? 0
|
2026-04-23 22:54:56 +08:00
|
|
|
if (strongAuthSignalCount <= 0) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return null
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
return boundedIdentity(`${platformId}-page`, fallbackFingerprint)
|
2026-04-23 22:54:56 +08:00
|
|
|
}
|