From e32bb9826c5bde653b92ca0d3e3bdcdaa0d2c80d Mon Sep 17 00:00:00 2001 From: rimkusaurimas Date: Thu, 11 Jun 2026 19:29:55 +0300 Subject: [PATCH 1/2] fix(handoff): resolve database by connection host, not name alone resolve_existing_database_id matched a database registration by name and returned the first hit. When two registrations share a name (e.g. two `neondb`s pointing at different hosts), the SDK could bind verification to a registration whose stored connection points at a different physical database, so proofs were generated against the wrong rows. Pass the target connection host through from _onboard_database and prefer the registration whose stored `uri` matches it (confirmed via the database detail endpoint, since list payloads omit `uri`). Falls back to the previous first-match behavior when no host is supplied or none matches. --- src/provably/handoff/_bootstrap.py | 2 +- src/provably/handoff/_discovery.py | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/provably/handoff/_bootstrap.py b/src/provably/handoff/_bootstrap.py index a4daa44..007d7f1 100644 --- a/src/provably/handoff/_bootstrap.py +++ b/src/provably/handoff/_bootstrap.py @@ -94,7 +94,7 @@ def _onboard_database(current_org: str, middleware_id: str, postgres_url: str) - # padding step ran above to ensure provably_intercepts exists, and the backend's # schema introspection will pick it up so discover_intercepts_table() can find it. _log.info("database_already_exists_reusing") - database_id = resolve_existing_database_id(current_org, middleware_id, db_name) + database_id = resolve_existing_database_id(current_org, middleware_id, db_name, body["uri"]) if not database_id: log_failed_response(resp) raise RuntimeError("Database exists but could not resolve existing database_id") diff --git a/src/provably/handoff/_discovery.py b/src/provably/handoff/_discovery.py index c5df118..7f2d628 100644 --- a/src/provably/handoff/_discovery.py +++ b/src/provably/handoff/_discovery.py @@ -27,7 +27,8 @@ def discover_intercepts_table(org_id: str, middleware_id: str, database_id: str) return _node_to_bundle(node) -def resolve_existing_database_id(org_id: str, middleware_id: str, db_name: str) -> str: +def resolve_existing_database_id(org_id: str, middleware_id: str, db_name: str, host: str = "") -> str: + candidates: list[str] = [] for path in ( f"/api/v1/organizations/{org_id}/middlewares/{middleware_id}/databases", f"/api/v1/organizations/{org_id}/databases", @@ -43,8 +44,22 @@ def resolve_existing_database_id(org_id: str, middleware_id: str, db_name: str) db_id = extract_id(item, ["id", "database_id"]) except ValueError: continue - if db_id: - return db_id + if db_id and db_id not in candidates: + candidates.append(db_id) + # Multiple registrations can share a name (e.g. two `neondb`s pointing at different + # hosts). Prefer the one whose stored connection host matches `host`, else verification + # could bind to a different physical database. List payloads omit `uri`, so confirm via + # the detail endpoint. With no `host` (or no match) we keep the legacy first-match. + if host: + for db_id in candidates: + try: + detail = get_json(f"/api/v1/organizations/{org_id}/middlewares/{middleware_id}/databases/{db_id}") + except Exception: # noqa: BLE001 + continue + if str(detail.get("uri") or "").strip() == host: + return db_id + if candidates: + return candidates[0] try: data = get_json(f"/api/v1/organizations/{org_id}/data") return find_first_id(data, ("database_id",)) From 57e81b59d0c0abd7cb947033e5e0d936be115643 Mon Sep 17 00:00:00 2001 From: rimkusaurimas Date: Thu, 11 Jun 2026 19:39:51 +0300 Subject: [PATCH 2/2] refactor(handoff): tighten db host-match (no blind pick, trim comment) --- src/provably/handoff/_discovery.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/provably/handoff/_discovery.py b/src/provably/handoff/_discovery.py index 7f2d628..e76a72d 100644 --- a/src/provably/handoff/_discovery.py +++ b/src/provably/handoff/_discovery.py @@ -46,18 +46,14 @@ def resolve_existing_database_id(org_id: str, middleware_id: str, db_name: str, continue if db_id and db_id not in candidates: candidates.append(db_id) - # Multiple registrations can share a name (e.g. two `neondb`s pointing at different - # hosts). Prefer the one whose stored connection host matches `host`, else verification - # could bind to a different physical database. List payloads omit `uri`, so confirm via - # the detail endpoint. With no `host` (or no match) we keep the legacy first-match. + # Same name can map to several registrations on different hosts; pick the one whose + # stored host matches `host` so we don't bind to a different physical DB. if host: - for db_id in candidates: - try: - detail = get_json(f"/api/v1/organizations/{org_id}/middlewares/{middleware_id}/databases/{db_id}") - except Exception: # noqa: BLE001 - continue - if str(detail.get("uri") or "").strip() == host: - return db_id + matched = [db_id for db_id in candidates if _database_host(org_id, middleware_id, db_id) == host] + if matched: + return matched[0] + if len(candidates) > 1: + return "" # ambiguous and none matched the host — refuse to guess if candidates: return candidates[0] try: @@ -67,6 +63,15 @@ def resolve_existing_database_id(org_id: str, middleware_id: str, db_name: str, return "" +def _database_host(org_id: str, middleware_id: str, database_id: str) -> str: + """Stored connection host for a database (the list endpoint omits it; the detail one has it).""" + try: + detail = get_json(f"/api/v1/organizations/{org_id}/middlewares/{middleware_id}/databases/{database_id}") + except Exception: # noqa: BLE001 + return "" + return str(detail.get("uri") or "").strip() + + def resolve_existing_collection_id(org_id: str, middleware_id: str, database_id: str, table_id: str) -> str: try: payload = get_json(f"/api/v1/organizations/{org_id}/collections")