- every outbound site request (adapter list/search/detail/download/health, RSS, login, user data) carries the account UA via SourcesRuntime.siteFetch; no more Deno default UA - priority: account manual > pending > global setting > browser identity (fallback: same-shape Windows Firefox UA); settings UA default is empty - migration 0056: existing cookie/password accounts keep their old UA as ua_pending until re-login or browser cookie write-back switches cookie+UA in one transaction; browser_provider.identity caches the browser identity - cf_clearance/__cf_bm stripped on browser write-back (names kept only) - browser sessions get per-site egress (same proxy as HTTP, or direct) - site account page shows UA source; raw UA in tech info Co-Authored-By: Claude Opus 5.5 <[email protected]>
101 lines
5.1 KiB
TypeScript
101 lines
5.1 KiB
TypeScript
import { createMigrator, type DB, migrateToLatest, sql } from "@umt/db";
|
|
import { Kysely, PostgresDialect } from "kysely";
|
|
import pg from "pg";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
import { PG_URL, pgAvailable, withSchemaLock } from "../auth-settings/harness.ts";
|
|
|
|
// 0056:存量 Cookie / 账密账号标"待切换"(沿用旧缺省 UA);手动 UA 与 API Key 账号不动;设置里的旧缺省 UA 清掉 = 跟随浏览器。
|
|
|
|
const hasPg = await pgAvailable();
|
|
const LEGACY =
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
|
|
describe.skipIf(!hasPg)("0056 浏览器身份迁移", () => {
|
|
const schemas: string[] = [];
|
|
const dbs: Kysely<DB>[] = [];
|
|
const admin = async (q: string) => {
|
|
const c = new pg.Client({ connectionString: PG_URL });
|
|
await c.connect();
|
|
await c.query(q).finally(() => c.end());
|
|
};
|
|
afterAll(async () => {
|
|
for (const d of dbs) await d.destroy();
|
|
await withSchemaLock(async () => {
|
|
for (const s of schemas) await admin(`drop schema if exists ${s} cascade`);
|
|
});
|
|
});
|
|
|
|
const setup = async (settingsUa: string | null) => {
|
|
const schema = `br4_mig_${process.pid.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
schemas.push(schema);
|
|
await admin(`create schema ${schema}`);
|
|
const db = new Kysely<DB>({
|
|
dialect: new PostgresDialect({
|
|
pool: new pg.Pool({ connectionString: PG_URL, max: 2, options: `-c search_path=${schema},public` }),
|
|
}),
|
|
});
|
|
dbs.push(db);
|
|
await withSchemaLock(async () => {
|
|
const r = await createMigrator(db, { migrationTableSchema: schema }).migrateTo("0055_site_kind");
|
|
if (r.error) throw r.error;
|
|
});
|
|
await sql`insert into site (key, name, adapter, base_url) values ('audiences', '观众', 'nexusphp', 'https://a.example/')`.execute(
|
|
db,
|
|
);
|
|
await sql`insert into secret (id, name, purpose, ciphertext, nonce) values
|
|
('00000000-0000-4000-8000-000000000001', 's1', 'site_cookie', 'x', 'x'),
|
|
('00000000-0000-4000-8000-000000000002', 's2', 'site_password', 'x', 'x'),
|
|
('00000000-0000-4000-8000-000000000003', 's3', 'site_api_key', 'x', 'x'),
|
|
('00000000-0000-4000-8000-000000000004', 's4', 'site_cookie', 'x', 'x')`.execute(db);
|
|
await sql`insert into site_account (site_id, label, auth_kind, secret_ref, user_agent) values
|
|
((select id from site), 'cookie', 'cookie', '00000000-0000-4000-8000-000000000001', null),
|
|
((select id from site), 'password', 'password', '00000000-0000-4000-8000-000000000002', null),
|
|
((select id from site), 'apikey', 'api_key', '00000000-0000-4000-8000-000000000003', null),
|
|
((select id from site), 'manual', 'cookie', '00000000-0000-4000-8000-000000000004', 'My-UA/1'),
|
|
((select id from site), 'empty', 'cookie', null, null)`.execute(db);
|
|
if (settingsUa !== null)
|
|
await sql`insert into settings (key, value) values ('sites', ${JSON.stringify({ userAgent: settingsUa, proxyUrl: null })}::jsonb)`.execute(
|
|
db,
|
|
);
|
|
await withSchemaLock(() => migrateToLatest(db, { migrationTableSchema: schema }));
|
|
const accounts = await sql<{ label: string; ua_pending: string | null }>`
|
|
select label, ua_pending from site_account order by label`.execute(db);
|
|
const settings = await sql<{
|
|
value: Record<string, unknown>;
|
|
}>`select value from settings where key = 'sites'`.execute(db);
|
|
return { db, accounts: Object.fromEntries(accounts.rows.map((r) => [r.label, r.ua_pending])), settings };
|
|
};
|
|
|
|
it("设置仍是旧缺省:Cookie / 账密账号待切换到旧 UA,设置里的 UA 清掉(跟随浏览器)", async () => {
|
|
const r = await setup(LEGACY);
|
|
expect(r.accounts).toEqual({ apikey: null, cookie: LEGACY, empty: null, manual: null, password: LEGACY });
|
|
expect(r.settings.rows[0]!.value).not.toHaveProperty("userAgent");
|
|
expect(r.settings.rows[0]!.value).toHaveProperty("proxyUrl", null);
|
|
const cols = await sql<{ column_name: string }>`select column_name from information_schema.columns
|
|
where table_schema = current_schema() and table_name = 'browser_provider' and column_name = 'identity'`.execute(
|
|
r.db,
|
|
);
|
|
expect(cols.rows).toHaveLength(1);
|
|
});
|
|
|
|
it("用户改过全局 UA:保留为全局 UA,存量账号待切换到它;没有设置行时按旧缺省", async () => {
|
|
const r = await setup("Custom-UA/9");
|
|
expect(r.accounts.cookie).toBe("Custom-UA/9");
|
|
expect(r.settings.rows[0]!.value).toHaveProperty("userAgent", "Custom-UA/9");
|
|
const r2 = await setup(null);
|
|
expect(r2.accounts.password).toBe(LEGACY);
|
|
expect(r2.settings.rows).toHaveLength(0);
|
|
});
|
|
|
|
it("回滚只删列", async () => {
|
|
const r = await setup(LEGACY);
|
|
await withSchemaLock(async () => {
|
|
const m = await createMigrator(r.db, { migrationTableSchema: schemas.at(-1)! }).migrateTo("0055_site_kind");
|
|
if (m.error) throw m.error;
|
|
});
|
|
const cols = await sql<{ column_name: string }>`select column_name from information_schema.columns
|
|
where table_schema = current_schema() and column_name in ('ua_pending', 'identity')`.execute(r.db);
|
|
expect(cols.rows).toHaveLength(0);
|
|
});
|
|
});
|