import assert from 'node:assert/strict'; import { mkdtemp, rm } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { build } from 'esbuild'; // Pure host-policy checks. No Electron process, real account, UI, or network. const root = fileURLToPath(new URL('../', import.meta.url)); const directory = await mkdtemp(path.join(os.tmpdir(), 'juzi-profile-popup-')); const note = 'https://www.xiaohongshu.com/explore/'+'a'.repeat(24)+'?xsec_token=private-note-token'; const profile = 'https://www.xiaohongshu.com/user/profile/'+'b'.repeat(24)+'?xsec_token=private-profile-token#keep'; try { const output = path.join(directory, 'profile-popup.mjs'); await build({ entryPoints: [path.join(root, 'electron/profile-popup.ts')], platform: 'node', bundle: true, format: 'esm', outfile: output, logLevel: 'silent' }); const { createProfilePopupHandler } = await import(pathToFileURL(output).href); const checks = []; const setup = ({ url = note, allowed = true } = {}) => { const state = { url, destroyed: false, current: true, time: 100, history: [url], fail: false }; const loads = [], logs = [], deferred = []; const page = { getURL: () => state.url, isDestroyed: () => state.destroyed, loadURL: async value => { loads.push(value); if (state.fail) throw new Error('private failure URL '+value); state.history.push(value); state.url = value; }, }; const handler = createProfilePopupHandler(page, { taskId: 'fixture-task', allowed: () => allowed, isCurrent: () => state.current, diagnose: value => logs.push(value), now: () => state.time, defer: callback => deferred.push(callback) }); const click = () => { handler.input('mouseDown'); handler.input('mouseUp'); }; const open = (value = profile, extra = {}) => handler.open({ url: value, ...extra }); const flush = async () => { deferred.splice(0).forEach(callback => callback()); await new Promise(setImmediate); }; return { state, loads, logs, deferred, handler, click, open, flush }; }; { const test = setup(); test.click(); assert.deepEqual(test.open(), { action: 'deny' }); assert.equal(test.loads.length, 0); await test.flush(); assert.deepEqual(test.loads, [profile]); assert.deepEqual(test.state.history, [note, profile]); assert.equal(test.logs.at(-1).action, 'opened_same_view'); checks.push('native-input profile routes exact actual URL in the same view, retaining the note history entry'); } for (const invalid of [ 'https://evil.example/user/profile/'+'b'.repeat(24), profile.replace('https:', 'http:'), profile.replace('www.xiaohongshu.com', 'www.xiaohongshu.com.evil.example'), profile.replace('www.xiaohongshu.com', 'www.xiaohongshu.com:9443'), profile.replace('www.xiaohongshu.com', 'private-user@www.xiaohongshu.com'), profile.replace('user/profile/', 'explore/'), profile.replace('b'.repeat(24), 'guessed-name'), 'javascript:window.alert(1)', 'data:text/html,hello', 'https://www.xiaohongshu.com/user/profile/%62'+'b'.repeat(23), ]) { const test = setup(); test.click(); assert.deepEqual(test.open(invalid), { action: 'deny' }); await test.flush(); assert.equal(test.loads.length, 0); assert.equal(test.logs.at(-1).category, 'other'); } checks.push('external, HTTP, lookalike, port, credential, non-profile, malformed-ID, encoded-path and non-web popups are denied'); { const test = setup(); test.open(); await test.flush(); assert.equal(test.logs.at(-1).action, 'deny_no_native_input'); test.handler.input('mouseMove'); test.open(); await test.flush(); test.handler.input('mouseUp'); test.open(); await test.flush(); assert.equal(test.loads.length, 0); checks.push('only a paired native press and release, not movement or a lone release, can authorize navigation'); } for (const elapsed of [-1, 751]) { const test = setup(); test.click(); test.state.time += elapsed; test.open(); await test.flush(); assert.equal(test.loads.length, 0); } checks.push('expired input and a reversed clock cannot authorize a popup'); for (const url of ['about:blank', 'https://www.xiaohongshu.com/search_result?keyword=test', 'https://www.xiaohongshu.com/user/profile/'+'a'.repeat(24), note.replace('www.xiaohongshu.com', 'evil.example')]) { const test = setup({ url }); test.click(); test.open(); await test.flush(); assert.equal(test.loads.length, 0); } checks.push('only a same-site concrete note can supply the input context'); for (const update of [test => { test.state.url = note.replace('a'.repeat(24), 'c'.repeat(24)); }, test => { test.state.current = false; }, test => { test.state.destroyed = true; }]) { const test = setup(); test.click(); test.open(); update(test); await test.flush(); assert.equal(test.loads.length, 0); assert.equal(test.logs.at(-1).action, 'cancel_stale'); } checks.push('queued routing rechecks the live managed view and original current note'); { const test = setup({ allowed: false }); test.click(); test.open(); await test.flush(); assert.equal(test.loads.length, 0); assert.equal(test.logs.at(-1).action, 'deny_context'); checks.push('the existing allowed-domain guard remains required'); } { const test = setup(); test.click(); test.open(profile, { postBody: { data: ['private form'] } }); await test.flush(); assert.equal(test.loads.length, 0); checks.push('form POST data is never replayed as a profile navigation'); } { const test = setup(); test.click(); test.open('https://evil.example/'); test.open(); await test.flush(); assert.equal(test.loads.length, 0); checks.push('one native input cannot be reused after another popup consumes it'); } { const test = setup(); test.click(); test.open(); test.click(); test.open(); await test.flush(); assert.deepEqual(test.loads, [profile]); checks.push('concurrent popup requests never duplicate the managed-view navigation'); } { const test = setup(); test.state.fail = true; test.click(); test.open(); await test.flush(); assert.equal(test.logs.at(-1).action, 'open_failed'); assert.deepEqual(test.state.history, [note]); const text = JSON.stringify(test.logs); assert.ok(!/https:|xsec_token|private|xiaohongshu|user\/profile/.test(text)); assert.ok(test.logs.every(value => Object.keys(value).sort().join(',') === 'action,category,taskId')); checks.push('failures retain the current view and log only task ID, safe category and action'); } process.stdout.write(JSON.stringify({ kind: 'pure profile-popup host policy check', passed: checks.length, checks, externalActions: 0 }, null, 2)+'\n'); } finally { await rm(directory, { recursive: true, force: true }); }