@package-broker/core 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,12 +17,22 @@ export interface ComposerRouteEnv {
17
17
  requestId?: string;
18
18
  };
19
19
  }
20
+ /** Default maximum versions per package to avoid CPU timeout on Cloudflare Workers */
21
+ export declare const DEFAULT_MAX_VERSIONS = 50;
20
22
  /**
21
23
  * GET /packages.json
22
24
  * Serve aggregated packages.json for all private repositories
23
25
  * Uses KV caching with stale-while-revalidate strategy
24
26
  */
25
27
  export declare function packagesJsonRoute(c: Context<ComposerRouteEnv>): Promise<Response>;
28
+ /**
29
+ * Lazy load package metadata from all available repositories
30
+ * Returns package data and repo_id if found, null otherwise
31
+ */
32
+ export declare function lazyLoadPackageFromRepositories(db: DatabasePort, packageName: string, encryptionKey: string, kv?: KVNamespace | null): Promise<{
33
+ packageData: any;
34
+ repoId: string;
35
+ } | null>;
26
36
  /**
27
37
  * GET /p2/:vendor/:package.json
28
38
  * Serve individual package metadata (Composer 2 provider format)
@@ -33,12 +43,17 @@ export declare function p2PackageRoute(c: Context<ComposerRouteEnv>): Promise<Re
33
43
  * Build Composer 2 provider response for a single package from stored metadata.
34
44
  * Composer 2 (p2) format expects versions as an ARRAY, not a dict keyed by version.
35
45
  */
36
- export declare function buildP2Response(packageName: string, packageVersions: Array<typeof packages.$inferSelect>, maxVersions?: number): ComposerP2Response;
46
+ export declare function buildP2Response(packageName: string, packageVersions: Array<typeof packages.$inferSelect>, maxVersions?: number, baseUrl?: string): ComposerP2Response;
47
+ export declare function deriveVersionNormalized(version: string): string | undefined;
37
48
  /**
38
49
  * Ensure Packagist repository exists in database
39
50
  * Creates it if it doesn't exist
40
51
  */
41
52
  export declare function ensurePackagistRepository(db: DatabasePort, encryptionKey: string, kv?: KVNamespace): Promise<void>;
53
+ export declare function normalizePackageVersions(versions: any): Array<{
54
+ version: string;
55
+ metadata: any;
56
+ }>;
42
57
  /**
43
58
  * Transform package dist URLs to proxy URLs and store in database
44
59
  * Waits for all database writes to complete before returning
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/routes/composer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/routes/composer.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@ import { encryptCredentials } from '../utils/encryption';
13
13
  import { getLogger } from '../utils/logger';
14
14
  import { getAnalytics } from '../utils/analytics';
15
15
  /** Default maximum versions per package to avoid CPU timeout on Cloudflare Workers */
16
- const DEFAULT_MAX_VERSIONS = 50;
16
+ export const DEFAULT_MAX_VERSIONS = 50;
17
17
  /**
18
18
  * Schedule background storage of package data.
19
19
  * Uses Cloudflare Workflows when available, falls back to inline processing.
@@ -156,6 +156,116 @@ export async function packagesJsonRoute(c) {
156
156
  headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
157
157
  return new Response(JSON.stringify(packagesJson), { status: 200, headers });
158
158
  }
159
+ function sortRepositoriesByPriority(repos) {
160
+ return [...repos].sort((a, b) => {
161
+ if (a.id === 'manual' && b.id !== 'manual')
162
+ return -1;
163
+ if (a.id !== 'manual' && b.id === 'manual')
164
+ return 1;
165
+ if (a.id === 'packagist' && b.id !== 'packagist')
166
+ return 1;
167
+ if (a.id !== 'packagist' && b.id === 'packagist')
168
+ return -1;
169
+ return (a.created_at || 0) - (b.created_at || 0);
170
+ });
171
+ }
172
+ function matchesPackageFilter(packageName, filter) {
173
+ if (!filter)
174
+ return true;
175
+ const patterns = filter.split(',').map((p) => p.trim().toLowerCase());
176
+ const pkgLower = packageName.toLowerCase();
177
+ return patterns.some((pattern) => {
178
+ if (pattern.endsWith('/*')) {
179
+ const prefix = pattern.slice(0, -1);
180
+ return pkgLower.startsWith(prefix);
181
+ }
182
+ if (pattern.endsWith('*')) {
183
+ const prefix = pattern.slice(0, -1);
184
+ return pkgLower.startsWith(prefix);
185
+ }
186
+ return pkgLower === pattern;
187
+ });
188
+ }
189
+ /**
190
+ * Lazy load package metadata from all available repositories
191
+ * Returns package data and repo_id if found, null otherwise
192
+ */
193
+ export async function lazyLoadPackageFromRepositories(db, packageName, encryptionKey, kv) {
194
+ const allRepos = await db
195
+ .select()
196
+ .from(repositories)
197
+ .where(inArray(repositories.status, ['active', 'pending', 'syncing']));
198
+ const sortedRepos = sortRepositoriesByPriority(allRepos);
199
+ for (const repo of sortedRepos) {
200
+ try {
201
+ if (!matchesPackageFilter(packageName, repo.package_filter)) {
202
+ continue;
203
+ }
204
+ let packageData = null;
205
+ if (repo.vcs_type === 'composer') {
206
+ const { fetchPackageFromUpstream } = await import('../utils/upstream-fetch');
207
+ packageData = await fetchPackageFromUpstream({
208
+ id: repo.id,
209
+ url: repo.url,
210
+ vcs_type: repo.vcs_type,
211
+ credential_type: repo.credential_type,
212
+ auth_credentials: repo.auth_credentials,
213
+ package_filter: repo.package_filter,
214
+ }, packageName, encryptionKey);
215
+ }
216
+ else if (repo.vcs_type === 'git') {
217
+ const { fetchPackageFromGitHub, isGitHubUrl } = await import('../utils/upstream-fetch');
218
+ if (!isGitHubUrl(repo.url)) {
219
+ continue;
220
+ }
221
+ packageData = await fetchPackageFromGitHub({
222
+ id: repo.id,
223
+ url: repo.url,
224
+ vcs_type: repo.vcs_type,
225
+ credential_type: repo.credential_type,
226
+ auth_credentials: repo.auth_credentials,
227
+ package_filter: repo.package_filter,
228
+ }, packageName, encryptionKey);
229
+ }
230
+ if (packageData) {
231
+ return { packageData, repoId: repo.id };
232
+ }
233
+ }
234
+ catch (error) {
235
+ const logger = getLogger();
236
+ logger.warn('Error fetching package from repo', {
237
+ packageName,
238
+ repoId: repo.id,
239
+ vcsType: repo.vcs_type,
240
+ error: error instanceof Error ? error.message : String(error)
241
+ });
242
+ }
243
+ }
244
+ const { isPackagistMirroringEnabled } = await import('../modules/admin');
245
+ const mirroringEnabled = await isPackagistMirroringEnabled(kv);
246
+ if (mirroringEnabled) {
247
+ try {
248
+ const packagistUrl = `https://repo.packagist.org/p2/${packageName}.json`;
249
+ const response = await fetch(packagistUrl, {
250
+ headers: {
251
+ 'User-Agent': COMPOSER_USER_AGENT,
252
+ },
253
+ });
254
+ if (response.ok) {
255
+ const packageData = await response.json();
256
+ return { packageData, repoId: 'packagist' };
257
+ }
258
+ }
259
+ catch (error) {
260
+ const logger = getLogger();
261
+ logger.warn('Error fetching from Packagist', {
262
+ packageName,
263
+ error: error instanceof Error ? error.message : String(error)
264
+ });
265
+ }
266
+ }
267
+ return null;
268
+ }
159
269
  /**
160
270
  * GET /p2/:vendor/:package.json
161
271
  * Serve individual package metadata (Composer 2 provider format)
@@ -241,7 +351,9 @@ export async function p2PackageRoute(c) {
241
351
  if (existingPackages.length > 0) {
242
352
  // Build response from database packages
243
353
  const maxVersions = getMaxVersions(c.env);
244
- const packageData = buildP2Response(packageName, existingPackages, maxVersions);
354
+ const url = new URL(c.req.url);
355
+ const baseUrl = `${url.protocol}//${url.host}`;
356
+ const packageData = buildP2Response(packageName, existingPackages, maxVersions, baseUrl);
245
357
  // Cache the result (fire-and-forget to avoid blocking on KV rate limits)
246
358
  const cachingEnabled = await isPackageCachingEnabled(c.env.KV);
247
359
  if (cachingEnabled && c.env.KV) {
@@ -267,112 +379,36 @@ export async function p2PackageRoute(c) {
267
379
  return new Response(JSON.stringify(packageData), { status: 200, headers });
268
380
  }
269
381
  // Not in database - try lazy loading from upstream repositories
270
- const activeRepos = await db
271
- .select()
272
- .from(repositories)
273
- .where(eq(repositories.status, 'active'));
274
382
  const url = new URL(c.req.url);
275
383
  const baseUrl = `${url.protocol}//${url.host}`;
276
384
  const maxVersions = getMaxVersions(c.env);
277
- // Try to fetch from upstream repositories (Composer and GitHub)
278
- for (const repo of activeRepos) {
279
- try {
280
- // Early filter: skip repo if package_filter is set and package doesn't match
281
- // This avoids unnecessary API calls to upstream repositories
282
- // Supports wildcards: "mirasvit/*" matches all mirasvit packages
283
- if (repo.package_filter) {
284
- const patterns = repo.package_filter.split(',').map((p) => p.trim().toLowerCase());
285
- const pkgLower = packageName.toLowerCase();
286
- const matches = patterns.some((pattern) => {
287
- if (pattern.endsWith('/*')) {
288
- // Wildcard pattern: "vendor/*" matches "vendor/anything"
289
- const prefix = pattern.slice(0, -1); // "vendor/"
290
- return pkgLower.startsWith(prefix);
291
- }
292
- if (pattern.endsWith('*')) {
293
- // Prefix wildcard: "mirasvit*" matches "mirasvit-module", "mirasvit/foo"
294
- const prefix = pattern.slice(0, -1);
295
- return pkgLower.startsWith(prefix);
296
- }
297
- // Exact match
298
- return pkgLower === pattern;
299
- });
300
- if (!matches) {
301
- continue; // Skip this repo - package not in filter list
302
- }
303
- }
304
- let packageData = null;
305
- if (repo.vcs_type === 'composer') {
306
- // Fetch from Composer repository (Satis, Private Packagist, etc.)
307
- const { fetchPackageFromUpstream } = await import('../utils/upstream-fetch');
308
- packageData = await fetchPackageFromUpstream({
309
- id: repo.id,
310
- url: repo.url,
311
- vcs_type: repo.vcs_type,
312
- credential_type: repo.credential_type,
313
- auth_credentials: repo.auth_credentials,
314
- package_filter: repo.package_filter,
315
- }, packageName, c.env.ENCRYPTION_KEY);
316
- }
317
- else if (repo.vcs_type === 'git') {
318
- // Fetch from GitHub repository (public or private)
319
- const { fetchPackageFromGitHub, isGitHubUrl } = await import('../utils/upstream-fetch');
320
- // Validate GitHub URL using proper hostname check (security)
321
- if (!isGitHubUrl(repo.url)) {
322
- // Skip non-GitHub git repos (e.g., GitLab, Bitbucket - not yet supported)
323
- continue;
324
- }
325
- packageData = await fetchPackageFromGitHub({
326
- id: repo.id,
327
- url: repo.url,
328
- vcs_type: repo.vcs_type,
329
- credential_type: repo.credential_type,
330
- auth_credentials: repo.auth_credentials,
331
- package_filter: repo.package_filter,
332
- }, packageName, c.env.ENCRYPTION_KEY);
333
- }
334
- if (packageData) {
335
- const transformedData = transformDistUrlsInMemory(packageData, repo.id, baseUrl, maxVersions);
336
- // Store package versions in background
337
- schedulePackageStorage(c, packageName, packageData, repo.id, baseUrl);
338
- // Track metadata request (cache miss, from upstream)
339
- const analytics = getAnalytics();
340
- const requestId = c.get('requestId');
341
- const packageCount = transformedData.packages?.[packageName] ? Object.keys(transformedData.packages[packageName]).length : 0;
342
- analytics.trackPackageMetadataRequest({
343
- requestId,
344
- cacheHit: false,
345
- packageCount,
346
- });
347
- const headers = new Headers();
348
- headers.set('Content-Type', 'application/json');
349
- headers.set('Last-Modified', new Date().toUTCString());
350
- headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
351
- headers.set('X-Cache', 'MISS-UPSTREAM');
352
- return new Response(JSON.stringify(transformedData), { status: 200, headers });
353
- }
354
- }
355
- catch (error) {
356
- const logger = getLogger();
357
- logger.warn('Error fetching package from repo', {
358
- packageName,
359
- repoId: repo.id,
360
- vcsType: repo.vcs_type,
361
- error: error instanceof Error ? error.message : String(error)
362
- });
363
- // Continue to next repository
364
- }
365
- }
366
- // Not found in any upstream repo - check if Packagist mirroring is enabled
367
- const mirroringEnabled = await isPackagistMirroringEnabled(c.env.KV);
368
- if (!mirroringEnabled) {
369
- return c.json({
370
- error: 'Not Found',
371
- message: 'Package not found. Public Packagist mirroring is disabled.',
372
- }, 404);
385
+ const lazyLoadResult = await lazyLoadPackageFromRepositories(db, packageName, c.env.ENCRYPTION_KEY, c.env.KV);
386
+ if (lazyLoadResult) {
387
+ const { packageData, repoId } = lazyLoadResult;
388
+ const transformedData = transformDistUrlsInMemory(packageData, repoId, baseUrl, maxVersions);
389
+ // Store package versions in background
390
+ schedulePackageStorage(c, packageName, packageData, repoId, baseUrl);
391
+ // Track metadata request (cache miss, from upstream)
392
+ const analytics = getAnalytics();
393
+ const requestId = c.get('requestId');
394
+ const packageCount = transformedData.packages?.[packageName] ? Object.keys(transformedData.packages[packageName]).length : 0;
395
+ analytics.trackPackageMetadataRequest({
396
+ requestId,
397
+ cacheHit: false,
398
+ packageCount,
399
+ });
400
+ const headers = new Headers();
401
+ headers.set('Content-Type', 'application/json');
402
+ headers.set('Last-Modified', new Date().toUTCString());
403
+ headers.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
404
+ headers.set('X-Cache', 'MISS-UPSTREAM');
405
+ return new Response(JSON.stringify(transformedData), { status: 200, headers });
373
406
  }
374
- // Proxy to public Packagist
375
- return proxyToPackagist(c, packageName);
407
+ // Not found in any repository (including Packagist)
408
+ return c.json({
409
+ error: 'Not Found',
410
+ message: 'Package not found in any repository.',
411
+ }, 404);
376
412
  }
377
413
  /**
378
414
  * Build aggregated packages.json from all repositories
@@ -413,15 +449,53 @@ async function buildPackagesJson(c) {
413
449
  if (!packagesMap[pkg.name]) {
414
450
  packagesMap[pkg.name] = {};
415
451
  }
416
- // Use dist_url (proxy URL) and transform to mirror format
417
- // source_dist_url is the original external URL - don't expose it to clients
452
+ // Preserve original dist from metadata - do NOT modify dist.url or dist.type
453
+ let dist;
454
+ if (pkg.metadata) {
455
+ try {
456
+ const metadata = JSON.parse(pkg.metadata);
457
+ if (metadata.dist && typeof metadata.dist === 'object' && !Array.isArray(metadata.dist)) {
458
+ // Use original dist from upstream metadata exactly as-is
459
+ dist = { ...metadata.dist };
460
+ }
461
+ else {
462
+ // Fallback: create minimal dist if metadata doesn't have dist
463
+ dist = {
464
+ type: 'zip',
465
+ url: pkg.source_dist_url || transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
466
+ };
467
+ }
468
+ }
469
+ catch {
470
+ // If metadata parsing fails, use fallback
471
+ dist = {
472
+ type: 'zip',
473
+ url: pkg.source_dist_url || transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
474
+ };
475
+ }
476
+ }
477
+ else {
478
+ // No metadata available - use fallback
479
+ dist = {
480
+ type: 'zip',
481
+ url: pkg.source_dist_url || transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
482
+ };
483
+ }
484
+ // Add mirrors section to dist object if not already present
485
+ // This ensures Composer can use the mirror while preserving original dist.url
486
+ // This prevents Composer from treating packages as upgrades when dist.url differs
487
+ if (!dist.mirrors || !Array.isArray(dist.mirrors) || dist.mirrors.length === 0) {
488
+ dist.mirrors = [
489
+ {
490
+ url: `${baseUrl}/dist/m/%package%/%version%.%type%`,
491
+ preferred: true,
492
+ },
493
+ ];
494
+ }
418
495
  packagesMap[pkg.name][pkg.version] = {
419
496
  name: pkg.name,
420
497
  version: pkg.version,
421
- dist: {
422
- type: 'zip',
423
- url: transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
424
- },
498
+ dist,
425
499
  };
426
500
  }
427
501
  return {
@@ -432,7 +506,7 @@ async function buildPackagesJson(c) {
432
506
  * Build Composer 2 provider response for a single package from stored metadata.
433
507
  * Composer 2 (p2) format expects versions as an ARRAY, not a dict keyed by version.
434
508
  */
435
- export function buildP2Response(packageName, packageVersions, maxVersions = DEFAULT_MAX_VERSIONS) {
509
+ export function buildP2Response(packageName, packageVersions, maxVersions = DEFAULT_MAX_VERSIONS, baseUrl) {
436
510
  // Apply version limiting to DB records as well
437
511
  // Convert to normalized format for limiting
438
512
  const normalizedForLimiting = packageVersions.map(pkg => ({
@@ -448,21 +522,42 @@ export function buildP2Response(packageName, packageVersions, maxVersions = DEFA
448
522
  const filteredPackageVersions = packageVersions.filter(pkg => limitedVersionSet.has(pkg.version));
449
523
  const versions = [];
450
524
  for (const pkg of filteredPackageVersions) {
451
- const dist = {
452
- type: 'zip',
453
- url: transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
454
- };
455
- if (pkg.dist_reference) {
456
- dist.reference = pkg.dist_reference;
457
- }
458
525
  let fullMetadata = null;
459
526
  if (pkg.metadata) {
460
527
  try {
461
528
  fullMetadata = JSON.parse(pkg.metadata);
462
529
  }
463
530
  catch {
531
+ // Ignore invalid JSON; use fallback metadata
464
532
  }
465
533
  }
534
+ // Preserve original dist from metadata - do NOT modify dist.url or dist.type
535
+ let dist;
536
+ if (fullMetadata?.dist && typeof fullMetadata.dist === 'object' && !Array.isArray(fullMetadata.dist)) {
537
+ // Use original dist from upstream metadata exactly as-is
538
+ dist = { ...fullMetadata.dist };
539
+ }
540
+ else {
541
+ // Fallback: only create minimal dist if metadata doesn't exist (shouldn't happen in practice)
542
+ dist = {
543
+ type: 'zip',
544
+ url: pkg.source_dist_url || transformDistUrlToMirrorFormat(pkg.dist_url) || pkg.dist_url,
545
+ };
546
+ if (pkg.dist_reference) {
547
+ dist.reference = pkg.dist_reference;
548
+ }
549
+ }
550
+ // Add mirrors section to dist object if not already present
551
+ // This ensures Composer can use the mirror while preserving original dist.url
552
+ // This prevents Composer from treating packages as upgrades when dist.url differs
553
+ if (baseUrl && (!dist.mirrors || !Array.isArray(dist.mirrors) || dist.mirrors.length === 0)) {
554
+ dist.mirrors = [
555
+ {
556
+ url: `${baseUrl}/dist/m/%package%/%version%.%type%`,
557
+ preferred: true,
558
+ },
559
+ ];
560
+ }
466
561
  const displayVersion = typeof fullMetadata?.version === 'string' ? fullMetadata.version : pkg.version;
467
562
  const normalizedVersion = typeof fullMetadata?.version_normalized === 'string'
468
563
  ? fullMetadata.version_normalized
@@ -484,6 +579,7 @@ export function buildP2Response(packageName, packageVersions, maxVersions = DEFA
484
579
  }
485
580
  }
486
581
  catch {
582
+ // Ignore license processing errors; use original value
487
583
  versionDataBase.license = pkg.license;
488
584
  }
489
585
  }
@@ -510,12 +606,6 @@ export function buildP2Response(packageName, packageVersions, maxVersions = DEFA
510
606
  ...(fullMetadata.source.reference && { reference: fullMetadata.source.reference }),
511
607
  };
512
608
  }
513
- if (fullMetadata.dist?.type && fullMetadata.dist.type !== 'zip') {
514
- dist.type = fullMetadata.dist.type;
515
- }
516
- if (fullMetadata.dist?.shasum) {
517
- dist.shasum = fullMetadata.dist.shasum;
518
- }
519
609
  if (fullMetadata.require && typeof fullMetadata.require === 'object' && !Array.isArray(fullMetadata.require)) {
520
610
  versionDataBase.require = fullMetadata.require;
521
611
  }
@@ -582,21 +672,52 @@ export function buildP2Response(packageName, packageVersions, maxVersions = DEFA
582
672
  },
583
673
  };
584
674
  }
585
- function deriveVersionNormalized(version) {
675
+ export function deriveVersionNormalized(version) {
586
676
  const v = version.startsWith('v') ? version.slice(1) : version;
677
+ // Handle patch versions (e.g., "103.0.7-p8" → "103.0.7.0-patch8")
587
678
  const patchMatch = v.match(/^(\d+\.\d+\.\d+)-p(\d+)$/);
588
679
  if (patchMatch) {
589
680
  const [, base, patch] = patchMatch;
590
681
  return `${base}.0-patch${patch}`;
591
682
  }
592
- const numericMatch = v.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?$/);
593
- if (numericMatch) {
594
- const major = numericMatch[1];
595
- const minor = numericMatch[2] ?? '0';
596
- const patch = numericMatch[3] ?? '0';
597
- const build = numericMatch[4] ?? '0';
598
- return `${major}.${minor}.${patch}.${build}`;
683
+ // Explicit parser avoids regex backtracking and ReDoS concerns:
684
+ // 1. Parse up to 4 numeric segments separated by dots from the start of the string.
685
+ // 2. Treat everything after the parsed numeric part as the suffix (e.g., "-beta", "-alpha", "-dev").
686
+ let index = 0;
687
+ const length = v.length;
688
+ const segments = [];
689
+ while (index < length && segments.length < 4) {
690
+ const start = index;
691
+ // Read a numeric segment
692
+ while (index < length) {
693
+ const ch = v.charCodeAt(index);
694
+ if (ch < 48 || ch > 57) { // not '0'–'9'
695
+ break;
696
+ }
697
+ index++;
698
+ }
699
+ if (index === start) {
700
+ // No digits found where a segment was expected
701
+ break;
702
+ }
703
+ segments.push(v.slice(start, index));
704
+ // If we have 4 segments or we've reached the end, stop
705
+ if (segments.length === 4 || index >= length) {
706
+ break;
707
+ }
708
+ // Expect a dot between segments; if not present, stop parsing numeric part
709
+ if (v[index] === '.') {
710
+ index++;
711
+ continue;
712
+ }
713
+ break;
714
+ }
715
+ if (segments.length > 0) {
716
+ const [major, minor = '0', patch = '0', build = '0'] = segments;
717
+ const suffix = v.slice(index); // everything after the numeric part
718
+ return `${major}.${minor}.${patch}.${build}${suffix}`;
599
719
  }
720
+ // Fallback: return undefined for non-standard versions
600
721
  return undefined;
601
722
  }
602
723
  /**
@@ -914,17 +1035,33 @@ function transformDistUrlsInMemory(packageData, repoId, proxyBaseUrl, maxVersion
914
1035
  ? metadata.version_normalized
915
1036
  : deriveVersionNormalized(version);
916
1037
  // Build transformed version with original display version
1038
+ // Preserve original dist from metadata - do NOT modify dist.url or dist.type
1039
+ let dist = metadata.dist || {
1040
+ type: 'zip',
1041
+ url: `${proxyBaseUrl}/dist/m/${pkgName}/${version}.zip`,
1042
+ reference: distReference,
1043
+ };
1044
+ // Add mirrors section to dist object if not already present
1045
+ // This ensures Composer can use the mirror while preserving original dist.url
1046
+ // This prevents Composer from treating packages as upgrades when dist.url differs
1047
+ if (!dist.mirrors || !Array.isArray(dist.mirrors) || dist.mirrors.length === 0) {
1048
+ dist = {
1049
+ ...dist,
1050
+ mirrors: [
1051
+ {
1052
+ url: `${proxyBaseUrl}/dist/m/%package%/%version%.%type%`,
1053
+ preferred: true,
1054
+ },
1055
+ ],
1056
+ };
1057
+ }
917
1058
  const versionData = {
918
1059
  ...metadata,
919
1060
  name: pkgName,
920
1061
  version,
921
1062
  ...(normalizedVersion ? { version_normalized: normalizedVersion } : {}),
922
- dist: {
923
- ...metadata.dist,
924
- type: metadata.dist?.type || 'zip',
925
- url: `${proxyBaseUrl}/dist/m/${pkgName}/${version}.zip`,
926
- reference: distReference,
927
- },
1063
+ // Use dist with mirrors section
1064
+ dist,
928
1065
  };
929
1066
  // Clean invalid source field if present
930
1067
  if (versionData.source === '__unset' ||
@@ -947,7 +1084,7 @@ function transformDistUrlsInMemory(packageData, repoId, proxyBaseUrl, maxVersion
947
1084
  }
948
1085
  return result;
949
1086
  }
950
- function normalizePackageVersions(versions) {
1087
+ export function normalizePackageVersions(versions) {
951
1088
  if (Array.isArray(versions)) {
952
1089
  return versions.map((metadata) => ({
953
1090
  // Use original `version` field, NOT `version_normalized`