Skip to content

Commit 3f9a965

Browse files
ulemonsclaude
andcommitted
fix: handle versioned PURLs and reject unimplemented busFactor1Only filter (CM-1220)
- Strip @Version (and ?qualifiers/#subpath) from PURLs before DB lookup in both detail and batch-stewardship endpoints. DB stores versionless PURLs so pkg:npm/lodash@4.17.21 now resolves to the same row as pkg:npm/lodash. Extracted shared normalizePurl() to packages/purl.ts. - Return 400 when busFactor1Only=true is passed — filter is accepted by the schema but was silently ignored, giving misleading full-result responses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 20abb59 commit 3f9a965

4 files changed

Lines changed: 27 additions & 3 deletions

File tree

backend/src/api/public/v1/packages/batchGetStewardship.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getPackagesQx } from '@/db/packagesDb'
77
import { ok } from '@/utils/api'
88
import { validateOrThrow } from '@/utils/validation'
99

10+
import { normalizePurl } from './purl'
1011
import type { StewardshipSummary } from './types'
1112

1213
const MAX_PURLS = 100
@@ -26,7 +27,7 @@ const bodySchema = z.object({
2627

2728
export async function batchGetStewardship(req: Request, res: Response): Promise<void> {
2829
const { purls: rawPurls } = validateOrThrow(bodySchema, req.body)
29-
const normalizedPurls = rawPurls.map((p) => p.replace(/@/g, '%40'))
30+
const normalizedPurls = rawPurls.map(normalizePurl)
3031

3132
const qx = await getPackagesQx()
3233
const rows = await getPackagesByStewardshipPurls(qx, normalizedPurls)

backend/src/api/public/v1/packages/getPackage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getPackagesQx } from '@/db/packagesDb'
88
import { ok } from '@/utils/api'
99
import { validateOrThrow } from '@/utils/validation'
1010

11+
import { normalizePurl } from './purl'
1112
import type { StewardshipStatus } from './types'
1213

1314
const querySchema = z.object({
@@ -16,7 +17,7 @@ const querySchema = z.object({
1617
.trim()
1718
.min(1)
1819
.refine((v) => v.startsWith('pkg:'), { message: 'purl must start with pkg:' })
19-
.transform((v) => v.replace(/@/g, '%40')),
20+
.transform(normalizePurl),
2021
})
2122

2223
export async function getPackage(req: Request, res: Response): Promise<void> {

backend/src/api/public/v1/packages/listPackages.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const querySchema = z.object({
2121
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
2222
ecosystem: z.string().trim().optional(),
2323
lifecycle: z.enum(lifecycleValues).optional(), // TODO: filter not yet implemented in DAL
24-
busFactor1Only: booleanQueryParam, // TODO: filter not yet implemented in DAL
24+
busFactor1Only: booleanQueryParam.refine((v) => !v, {
25+
message: 'busFactor1Only filter is not yet implemented',
26+
}),
2527
staleOnly: booleanQueryParam,
2628
unstewardedOnly: booleanQueryParam,
2729
sortBy: z.enum(['name', 'health', 'impact', 'openVulns']).default('name'),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Normalize a PURL for lookup against the packages table.
3+
*
4+
* The DB stores versionless PURLs with npm scope @ encoded as %40.
5+
* Clients may send purls with versions (pkg:npm/lodash@4.17.21) and qualifiers.
6+
*
7+
* Transform order:
8+
* 1. strip ?qualifiers and #subpath — not stored in DB
9+
* 2. strip @version suffix — DB stores versionless PURLs
10+
* 3. encode @ in namespace/scope (e.g. npm @babel → %40babel)
11+
*
12+
* The version regex matches @ followed by non-/ non-@ chars at end of string.
13+
* This is always the version separator, not an npm scope (pkg:npm/@babel/core
14+
* has @babel followed by /core, so it never matches the end-of-string pattern).
15+
*/
16+
export function normalizePurl(purl: string): string {
17+
const withoutQualifiers = purl.replace(/[?#].*$/, '')
18+
const withoutVersion = withoutQualifiers.replace(/@[^/@]+$/, '')
19+
return withoutVersion.replace(/@/g, '%40')
20+
}

0 commit comments

Comments
 (0)