// Experimental exceptions for requests naturally made by finite official UI // actions. This module never issues a request or inspects bodies/headers. import { validateSelector } from './action-contract.mjs'; import {assertNativeUiAction,validNativePage,nativeNoteId,nativeSearchKeyword,NAVIGATION_UI_PROFILE,navigationTarget,validNavigationPage,navigationDestinationMatches} from './execution-profile.mjs'; export const SEARCH_POLICY_VERSION = 'xhs-search-experimental-v3'; export const NOTE_POLICY_VERSION = 'xhs-note-open-experimental-v2'; export const READONLY_POLICY_VERSION = 'xhs-readonly-ui-experimental-v1'; export const SEARCH_SCROLL_POLICY_VERSION = 'xhs-search-scroll-experimental-v1'; export const NATIVE_UI_POLICY_VERSION = 'xhs-page-native-v1'; export const NAVIGATION_UI_POLICY_VERSION = 'xhs-page-navigation-v1'; const OFFICIAL_ORIGIN = 'https://www.xiaohongshu.com'; const issued = new WeakSet(); const searchScrollPages = new WeakMap(); const nativeUiPages = new WeakMap(); const MAX_WINDOW_MS = 15_000; const SEARCH_RULES = Object.freeze([ ['collect', 'https://t2.xiaohongshu.com', '/api/v2/collect', 'xhr'], ['search_history', 'https://so.xiaohongshu.com', '/api/sns/web/search/history/sync', 'xhr'], ['search_notes', 'https://so.xiaohongshu.com', '/api/sns/web/v2/search/notes', 'xhr'], ['search_onebox', 'https://edith.xiaohongshu.com', '/api/sns/web/v1/search/onebox', 'xhr'], ['search_dqa', 'https://so.xiaohongshu.com', '/api/sns/web/v1/dqa/instant', 'xhr'], ['apm_data', 'https://apm-fe.xiaohongshu.com', '/api/data', 'fetch'], ['apm_data_xhr', 'https://apm-fe.xiaohongshu.com', '/api/data', 'xhr'], ].map(rule => Object.freeze(rule))); const NOTE_RULES = Object.freeze([ ['collect', 'https://t2.xiaohongshu.com', '/api/v2/collect', 'xhr'], ['note_share_code', 'https://edith.xiaohongshu.com', '/api/sns/web/share/code', 'xhr'], ['note_widgets', 'https://edith.xiaohongshu.com', '/api/sns/web/v2/widgets', 'xhr'], ['note_feed', 'https://edith.xiaohongshu.com', '/api/sns/web/v1/feed', 'xhr'], ['note_seo', 'https://edith.xiaohongshu.com', '/api/sns/web/worldcup/note/seo', 'xhr'], ['note_metrics', 'https://edith.xiaohongshu.com', '/api/sns/web/v1/note/metrics_report', 'xhr'], ['note_history_report', 'https://edith.xiaohongshu.com', '/api/sns/v1/history/report_web', 'xhr'], ['apm_data', 'https://apm-fe.xiaohongshu.com', '/api/data', 'fetch'], ].map(rule => Object.freeze(rule))); const READONLY_RULES = Object.freeze([ ['collect', 'https://t2.xiaohongshu.com', '/api/v2/collect', 'xhr'], ['apm_data', 'https://apm-fe.xiaohongshu.com', '/api/data', 'fetch'], ].map(rule => Object.freeze(rule))); // Pagination is the existing exact search-notes rule. Other search-start // requests (history, onebox, dqa) are deliberately outside this scope. const SEARCH_SCROLL_RULES = Object.freeze([...READONLY_RULES, SEARCH_RULES.find(([name]) => name === 'search_notes')]); function officialPage(value) { try { const url = new URL(value); return url.origin === OFFICIAL_ORIGIN && !url.username && !url.password; } catch { return false; } } export function beginNativeUiRequestPolicy(context){ if(context.sessionOrigin!==OFFICIAL_ORIGIN || !Number.isFinite(context.now ?? Date.now())) return null; try{assertNativeUiAction(context);}catch{return null;} const now=context.now ?? Date.now(); const navigation=context.executionProfile===NAVIGATION_UI_PROFILE; const policy=Object.freeze({version:navigation ? NAVIGATION_UI_POLICY_VERSION : NATIVE_UI_POLICY_VERSION,scope:'native_ui',expiresAt:now+MAX_WINDOW_MS}); issued.add(policy); nativeUiPages.set(policy,Object.freeze({pageUrl:context.pageUrl,startsAt:now,kind:context.kind,navigation, keyword:['fill','press'].includes(context.kind) ? context.keyword.trim() : null, noteId:context.kind==='click' ? nativeNoteId(context.href) : null, navigationTarget:navigation && context.kind==='click' ? navigationTarget(context.href) : null})); return policy; } function nativePageMatches(policy,pageUrl,now){ const bound=nativeUiPages.get(policy); if(!bound || !(bound.navigation ? validNavigationPage(pageUrl) : validNativePage(pageUrl))) return 'page_changed'; if(now=policy.expiresAt) return 'policy_expired'; if(pageUrl===bound.pageUrl) return null; if(bound.kind==='press' && nativeSearchKeyword(pageUrl)===bound.keyword) return null; if(bound.kind==='click'){ if(bound.navigation){ if(navigationDestinationMatches(bound.navigationTarget?.href,pageUrl)) return null; }else if(nativeNoteId(pageUrl)===bound.noteId) return null; } return 'page_changed'; } export function beginSearchRequestPolicy({ sessionOrigin, pageUrl, kind, key, observed, actual, matchCount, now = Date.now() }) { if (sessionOrigin !== OFFICIAL_ORIGIN || !officialPage(pageUrl) || !(kind === 'fill' || (kind === 'press' && key === 'Enter')) || matchCount !== 1 || observed?.kind !== 'fill' || observed.unique !== true || observed.search_input !== true || observed.sensitive !== false || !['input', 'textarea'].includes(observed.tag) || actual?.tag !== observed.tag || actual.search_input !== true || actual.sensitive !== false || actual.visible !== true || actual.in_viewport !== true || actual.unobscured !== true || !Number.isFinite(now)) return null; const policy = Object.freeze({ version: SEARCH_POLICY_VERSION, scope: 'search', expiresAt: now + MAX_WINDOW_MS }); issued.add(policy); return policy; } export function beginNoteOpenRequestPolicy({ sessionOrigin, pageUrl, kind, observed, actual, matchCount, href, targetResolvedUnique = false, now = Date.now() }) { if (sessionOrigin !== OFFICIAL_ORIGIN || !officialPage(pageUrl) || kind !== 'click' || matchCount !== 1 || observed?.kind !== 'click' || observed.tag !== 'a' || (observed.unique !== true && targetResolvedUnique !== true) || observed.sensitive !== false || actual?.tag !== 'a' || actual.sensitive !== false || actual.visible !== true || actual.in_viewport !== true || actual.unobscured !== true || actual.href !== href || !officialPage(href) || !Number.isFinite(now)) return null; const url = new URL(href); if (!/^\/(?:explore|search_result)\/[a-fA-F0-9]{24}$/.test(url.pathname)) return null; const policy = Object.freeze({ version: NOTE_POLICY_VERSION, scope: 'note_open', expiresAt: now + MAX_WINDOW_MS }); issued.add(policy); return policy; } function boundedSearchScroll({ pageUrl, kind, selector, observed, actualRoots, scrollScope, untilTarget }) { if (kind !== 'scroll' || scrollScope !== 'viewport' || !untilTarget || typeof untilTarget !== 'object' || Array.isArray(untilTarget) || Object.keys(untilTarget).some(key => !['selector','field','value'].includes(key)) || untilTarget.field !== 'text' || typeof untilTarget.value !== 'string' || !untilTarget.value.trim() || untilTarget.value.length > 2000 || observed?.kind !== 'read' || observed.unique !== true || observed.sensitive !== false || actualRoots.length !== 1 || typeof observed.tag !== 'string' || !/^[a-z][a-z0-9-]*$/.test(observed.tag) || ['body','html','a'].includes(observed.tag) || actualRoots[0].tag !== observed.tag || actualRoots[0].visible !== true || actualRoots[0].in_viewport !== true) return false; try { if (!['/search_result','/search_result_ai'].includes(new URL(pageUrl).pathname)) return false; validateSelector(selector, { relative:true }); validateSelector(untilTarget.selector, { relative:true }); } catch { return false; } return true; } export function beginReadonlyRequestPolicy({ sessionOrigin, pageUrl, kind, selector, selectorObserved, actualRoots, observed, scrollScope, untilTarget, now = Date.now() }) { if (sessionOrigin !== OFFICIAL_ORIGIN || !officialPage(pageUrl) || !['read','extract','scroll','wait'].includes(kind) || typeof selector !== 'string' || !selector.trim() || selector.length>1000 || !Number.isFinite(now) || !Array.isArray(actualRoots) || !actualRoots.length || actualRoots.length > 500) return null; const bodyException = selector === 'body' && ['scroll','wait'].includes(kind); if ((selector === 'body' && !bodyException) || (!bodyException && selectorObserved !== true) || actualRoots.some(root => root?.readonly_root !== true || root.sensitive !== false || root.forbidden !== false || ['input','textarea','select','button','form','script','style','noscript','iframe'].includes(root.tag) || (root.tag === 'body' && !bodyException) || (bodyException && root.tag !== 'body'))) return null; const searchScroll = boundedSearchScroll({pageUrl,kind,selector,observed,actualRoots,scrollScope,untilTarget}); const policy = Object.freeze({ version:searchScroll ? SEARCH_SCROLL_POLICY_VERSION : READONLY_POLICY_VERSION, scope:searchScroll ? 'search_scroll' : 'readonly_ui',expiresAt:now+MAX_WINDOW_MS }); issued.add(policy); if (searchScroll) searchScrollPages.set(policy, Object.freeze({pageUrl,startsAt:now})); return policy; } // Fixed diagnostic reasons explain the existing decision; they never authorize // another request, replay a request, or inspect headers or bodies. export function evaluatePlatformRequest({ policy, automating, sessionOrigin, pageUrl, activeMainFrame, method, resourceType, url, now = Date.now() }) { const active=Boolean(policy && issued.has(policy)); const reject=reason=>({allowed:false,reason,...(active ? {policy_version:policy.version} : {})}); if(!policy) return reject('missing_policy'); if(!active) return reject('inactive_policy'); if(!Number.isFinite(now)) return reject('invalid_time'); if(now>policy.expiresAt) return reject('policy_expired'); if(automating!==true) return reject('automation_inactive'); if(sessionOrigin!==OFFICIAL_ORIGIN) return reject('session_out_of_scope'); if(!officialPage(pageUrl)) return reject('page_out_of_scope'); if(activeMainFrame!==true) return reject('not_main_frame'); if(method!=='POST') return reject('method_not_allowed'); if (policy.scope === 'search_scroll') { const bound = searchScrollPages.get(policy); if(!bound || pageUrl!==bound.pageUrl) return reject('page_changed'); if(now=policy.expiresAt) return reject('policy_expired'); } let parsed; try { parsed = new URL(url); } catch { return reject('invalid_url'); } if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return reject('invalid_url'); if(policy.scope==='native_ui'){ const pageFailure=nativePageMatches(policy,pageUrl,now); if(pageFailure) return reject(pageFailure); // This permits the trusted site's own page traffic, not a tool/API call. // It does not classify POST as read-only or suppress platform security. if(parsed.port || !(parsed.hostname==='xiaohongshu.com' || parsed.hostname.endsWith('.xiaohongshu.com'))) return reject('request_out_of_scope'); if(!['xhr','fetch'].includes(resourceType)) return {...reject('resource_type_not_allowed'),expected_resource_types:['xhr','fetch']}; return {allowed:true,policy_version:policy.version,policy_rule:'native_page_traffic'}; } const rules = policy.scope === 'search' ? SEARCH_RULES : policy.scope === 'note_open' ? NOTE_RULES : policy.scope === 'readonly_ui' ? READONLY_RULES : policy.scope === 'search_scroll' ? SEARCH_SCROLL_RULES : []; const locationRules=rules.filter(([,origin,pathname])=>parsed.origin===origin && parsed.pathname===pathname); if(!locationRules.length) return reject('request_out_of_scope'); const rule=locationRules.find(([, , , type])=>resourceType===type); return rule ? {allowed:true,policy_version:policy.version,policy_rule:rule[0]} : {...reject('resource_type_not_allowed'),expected_resource_types:[...new Set(locationRules.map(([, , , type])=>type))]}; } export function matchPlatformRequest(context) { const decision=evaluatePlatformRequest(context); return decision.allowed ? {policy_version:decision.policy_version,policy_rule:decision.policy_rule} : null; } export function endPlatformRequestPolicy(policy) { if (policy) { issued.delete(policy); searchScrollPages.delete(policy); nativeUiPages.delete(policy); } }