@@ -135,19 +135,46 @@ async def list_tenant_bundles(
135135async def list_bundle_names (
136136 db : AsyncSession ,
137137 tenant_id : uuid .UUID ,
138+ * ,
139+ env : str | None = None ,
138140) -> list [dict [str , object ]]:
139- """Return distinct bundle names with aggregates (version_count, latest_version)."""
140- result = await db .execute (
141- select (
142- Bundle .name ,
143- func .count (Bundle .id ).label ("version_count" ),
144- func .max (Bundle .version ).label ("latest_version" ),
145- func .max (Bundle .created_at ).label ("last_updated" ),
141+ """Return distinct bundle names with aggregates (version_count, latest_version).
142+
143+ When *env* is provided, only versions deployed to that environment are
144+ counted — prevents cross-env metadata leakage for env-scoped API keys.
145+ """
146+ if env is not None :
147+ # Scoped: only count versions that have a deployment to this env
148+ stmt = (
149+ select (
150+ Bundle .name ,
151+ func .count (func .distinct (Bundle .id )).label ("version_count" ),
152+ func .max (Bundle .version ).label ("latest_version" ),
153+ func .max (Bundle .created_at ).label ("last_updated" ),
154+ )
155+ .join (
156+ Deployment ,
157+ (Bundle .tenant_id == Deployment .tenant_id )
158+ & (Bundle .name == Deployment .bundle_name )
159+ & (Bundle .version == Deployment .bundle_version ),
160+ )
161+ .where (Bundle .tenant_id == tenant_id , Deployment .env == env )
162+ .group_by (Bundle .name )
163+ .order_by (func .max (Bundle .created_at ).desc ())
146164 )
147- .where (Bundle .tenant_id == tenant_id )
148- .group_by (Bundle .name )
149- .order_by (func .max (Bundle .created_at ).desc ())
150- )
165+ else :
166+ stmt = (
167+ select (
168+ Bundle .name ,
169+ func .count (Bundle .id ).label ("version_count" ),
170+ func .max (Bundle .version ).label ("latest_version" ),
171+ func .max (Bundle .created_at ).label ("last_updated" ),
172+ )
173+ .where (Bundle .tenant_id == tenant_id )
174+ .group_by (Bundle .name )
175+ .order_by (func .max (Bundle .created_at ).desc ())
176+ )
177+ result = await db .execute (stmt )
151178 return [
152179 {
153180 "name" : row .name ,
@@ -235,9 +262,7 @@ async def get_deployed_envs_by_bundle_name(
235262 .subquery ()
236263 )
237264
238- result = await db .execute (
239- select (ranked .c .bundle_name , ranked .c .env ).where (ranked .c .rn == 1 )
240- )
265+ result = await db .execute (select (ranked .c .bundle_name , ranked .c .env ).where (ranked .c .rn == 1 ))
241266
242267 mapping : dict [str , list [str ]] = defaultdict (list )
243268 for name , env in result .all ():
@@ -250,28 +275,48 @@ async def get_deployed_envs_by_bundle_name(
250275async def get_bundle_enrichment (
251276 db : AsyncSession ,
252277 tenant_id : uuid .UUID ,
278+ * ,
279+ env : str | None = None ,
253280) -> dict [str , dict [str , object ]]:
254281 """Return contract_count and last_deployed_at per bundle name.
255282
256- contract_count is parsed from the latest version's YAML.
283+ contract_count is parsed from the latest deployed version's YAML.
257284 last_deployed_at comes from the deployments table.
285+
286+ When *env* is provided, both the "latest version" subquery and the
287+ deployment timestamp are scoped to that environment — prevents
288+ cross-env metadata leakage for env-scoped API keys.
258289 """
259- # Get latest version per bundle name
260- latest_subq = (
261- select (
262- Bundle .name ,
263- func .max (Bundle .version ).label ("max_version" ),
290+ # Get latest version per bundle name (optionally scoped to env)
291+ if env is not None :
292+ # Latest version deployed to this specific env
293+ latest_subq = (
294+ select (
295+ Deployment .bundle_name .label ("name" ),
296+ func .max (Deployment .bundle_version ).label ("max_version" ),
297+ )
298+ .where (Deployment .tenant_id == tenant_id , Deployment .env == env )
299+ .group_by (Deployment .bundle_name )
300+ .subquery ()
301+ )
302+ else :
303+ # Latest version across all envs (dashboard view)
304+ latest_subq = (
305+ select (
306+ Bundle .name ,
307+ func .max (Bundle .version ).label ("max_version" ),
308+ )
309+ .where (Bundle .tenant_id == tenant_id )
310+ .group_by (Bundle .name )
311+ .subquery ()
264312 )
265- .where (Bundle .tenant_id == tenant_id )
266- .group_by (Bundle .name )
267- .subquery ()
268- )
269313 latest_bundles = await db .execute (
270- select (Bundle ).join (
314+ select (Bundle )
315+ .join (
271316 latest_subq ,
272- (Bundle .name == latest_subq .c .name )
273- & ( Bundle . version == latest_subq . c . max_version ),
274- ) .where (Bundle .tenant_id == tenant_id )
317+ (Bundle .name == latest_subq .c .name ) & ( Bundle . version == latest_subq . c . max_version ),
318+ )
319+ .where (Bundle .tenant_id == tenant_id )
275320 )
276321
277322 enrichment : dict [str , dict [str , object ]] = {}
@@ -287,13 +332,16 @@ async def get_bundle_enrichment(
287332 pass
288333 enrichment [bundle .name ] = {"contract_count" : contract_count , "last_deployed_at" : None }
289334
290- # Get last deployment date per bundle name
335+ # Get last deployment date per bundle name (scoped to env if provided)
336+ dep_filters = [Deployment .tenant_id == tenant_id ]
337+ if env is not None :
338+ dep_filters .append (Deployment .env == env )
291339 dep_result = await db .execute (
292340 select (
293341 Deployment .bundle_name ,
294342 func .max (Deployment .created_at ).label ("last_deployed_at" ),
295343 )
296- .where (Deployment . tenant_id == tenant_id )
344+ .where (* dep_filters )
297345 .group_by (Deployment .bundle_name )
298346 )
299347 for row in dep_result .all ():
0 commit comments