From bff73794f577cac921a81cc49abe2b08c83c0a32 Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:06:06 +0400 Subject: [PATCH 1/2] fix(db): update proxies on password rotation --- .../fixes/7703-proxy-password-rotation.md | 1 + src/lib/db/proxies.ts | 38 +++++++++++++------ .../unit/proxy-bulk-import-dedup-7594.test.ts | 34 ++++++++++++++--- 3 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 changelog.d/fixes/7703-proxy-password-rotation.md diff --git a/changelog.d/fixes/7703-proxy-password-rotation.md b/changelog.d/fixes/7703-proxy-password-rotation.md new file mode 100644 index 000000000000..05382e34ee38 --- /dev/null +++ b/changelog.d/fixes/7703-proxy-password-rotation.md @@ -0,0 +1 @@ +- **fix(db):** bulk proxy imports now update an existing proxy when only its password changes instead of creating a duplicate. diff --git a/src/lib/db/proxies.ts b/src/lib/db/proxies.ts index d0742dfd1722..10f9ed653368 100755 --- a/src/lib/db/proxies.ts +++ b/src/lib/db/proxies.ts @@ -296,14 +296,17 @@ export async function createProxy(payload: ProxyPayload) { } /** - * Upsert a proxy by its credential tuple (host+port+username+password). - * If a proxy with the same host, port, username AND password already exists, - * update it. Otherwise, create a new one. Used by the bulk import feature. + * Upsert a proxy by its identity tuple (host+port+username). + * If a proxy with the same host, port, and username already exists, update it. + * Otherwise, create a new one. Used by the bulk import feature. * * #7594: host+port alone is NOT a stable identity. Rotating residential/gateway * proxies route every credential through one shared host:port, so keying only on * host+port collapsed distinct-credential imports onto the first existing row * (the same entry got "updated" N times instead of N entries being created). + * + * #7703: password is mutable and must not be part of the identity key. Including + * it caused password-only credential rotations to create duplicate entries. */ export async function upsertProxy(payload: ProxyPayload): Promise<{ proxy: ProxyRegistryRecord | null; @@ -313,13 +316,10 @@ export async function upsertProxy(payload: ProxyPayload): Promise<{ const host = (payload.host || "").trim(); const port = Number(payload.port); const username = (payload.username || "").trim(); - const password = (payload.password || "").trim(); const existing = db - .prepare( - "SELECT id FROM proxy_registry WHERE host = ? AND port = ? AND username = ? AND password = ? LIMIT 1" - ) - .get(host, port, username, password) as { id?: string } | undefined; + .prepare("SELECT id FROM proxy_registry WHERE host = ? AND port = ? AND username = ? LIMIT 1") + .get(host, port, username) as { id?: string } | undefined; if (existing?.id) { const updated = await updateProxy(existing.id, payload); @@ -701,13 +701,23 @@ function getOrCreateRotationRow( db: ReturnType, normalizedScope: string, rotationScopeId: string -): { strategy: ProxyRotationStrategy; cursor: number; stickyWindowMinutes: number; rotatedAt: string | null } { +): { + strategy: ProxyRotationStrategy; + cursor: number; + stickyWindowMinutes: number; + rotatedAt: string | null; +} { const row = db .prepare( "SELECT strategy, cursor, sticky_window_minutes, rotated_at FROM proxy_scope_rotation WHERE scope = ? AND scope_id IS ?" ) .get(normalizedScope, rotationScopeId) as - | { strategy?: string; cursor?: number; sticky_window_minutes?: number; rotated_at?: string | null } + | { + strategy?: string; + cursor?: number; + sticky_window_minutes?: number; + rotated_at?: string | null; + } | undefined; if (row) { @@ -766,7 +776,13 @@ function pickFromCandidates( cursor = state.cursor + 1; db.prepare( "UPDATE proxy_scope_rotation SET cursor = ?, rotated_at = ?, updated_at = ? WHERE scope = ? AND scope_id IS ?" - ).run(cursor, new Date().toISOString(), new Date().toISOString(), normalizedScope, rotationScopeId); + ).run( + cursor, + new Date().toISOString(), + new Date().toISOString(), + normalizedScope, + rotationScopeId + ); } const idx = ((cursor % candidates.length) + candidates.length) % candidates.length; return candidates[idx]; diff --git a/tests/unit/proxy-bulk-import-dedup-7594.test.ts b/tests/unit/proxy-bulk-import-dedup-7594.test.ts index 1ae2c0497b58..b2fe44d48e43 100644 --- a/tests/unit/proxy-bulk-import-dedup-7594.test.ts +++ b/tests/unit/proxy-bulk-import-dedup-7594.test.ts @@ -62,10 +62,7 @@ test("upsertProxy creates distinct entries for same host:port with different cre const listed = await proxiesDb.listProxies({ includeSecrets: true }); assert.equal(listed.length, 3); - assert.deepEqual( - listed.map((p) => p.username).sort(), - ["user-1", "user-2", "user-3"] - ); + assert.deepEqual(listed.map((p) => p.username).sort(), ["user-1", "user-2", "user-3"]); }); test("upsertProxy still updates when the full credential tuple matches (#7594)", async () => { @@ -90,8 +87,35 @@ test("upsertProxy still updates when the full credential tuple matches (#7594)", assert.equal(listed[0].name, "Gateway Renamed"); }); +test("upsertProxy updates an existing proxy when only its password changes (#7703)", async () => { + const payload = { + name: "Rotating Gateway", + type: "http" as const, + host: "rotate.proxy.local", + port: 8080, + username: "stable-user", + password: "pass-a", + }; + + const created = await proxiesDb.upsertProxy(payload); + const rotated = await proxiesDb.upsertProxy({ ...payload, password: "pass-b" }); + + assert.equal(created.action, "created"); + assert.equal(rotated.action, "updated"); + assert.equal(rotated.proxy?.id, created.proxy?.id); + + const listed = await proxiesDb.listProxies({ includeSecrets: true }); + assert.equal(listed.length, 1); + assert.equal(listed[0].password, "pass-b"); +}); + test("upsertProxy treats auth-less proxies on same host:port as the same entry (#7594)", async () => { - const base = { name: "Open Gateway", type: "http" as const, host: "open.proxy.local", port: 3128 }; + const base = { + name: "Open Gateway", + type: "http" as const, + host: "open.proxy.local", + port: 3128, + }; const first = await proxiesDb.upsertProxy(base); const second = await proxiesDb.upsertProxy({ ...base, name: "Open Gateway 2" }); From 4fb38cd56de86a3390f847e81da4a2a8270e286a Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:08:06 +0400 Subject: [PATCH 2/2] docs(changelog): link proxy rotation fix --- changelog.d/fixes/7703-proxy-password-rotation.md | 1 - changelog.d/fixes/7707-proxy-password-rotation.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changelog.d/fixes/7703-proxy-password-rotation.md create mode 100644 changelog.d/fixes/7707-proxy-password-rotation.md diff --git a/changelog.d/fixes/7703-proxy-password-rotation.md b/changelog.d/fixes/7703-proxy-password-rotation.md deleted file mode 100644 index 05382e34ee38..000000000000 --- a/changelog.d/fixes/7703-proxy-password-rotation.md +++ /dev/null @@ -1 +0,0 @@ -- **fix(db):** bulk proxy imports now update an existing proxy when only its password changes instead of creating a duplicate. diff --git a/changelog.d/fixes/7707-proxy-password-rotation.md b/changelog.d/fixes/7707-proxy-password-rotation.md new file mode 100644 index 000000000000..21297b5eebcf --- /dev/null +++ b/changelog.d/fixes/7707-proxy-password-rotation.md @@ -0,0 +1 @@ +- **fix(db):** bulk proxy imports now update an existing proxy when only its password changes instead of creating a duplicate ([#7707](https://github.com/diegosouzapw/OmniRoute/pull/7707)) — thanks @floze-the-genius