import * as plugins from '../plugins.js'; import { BaseUpstream } from '../upstream/classes.baseupstream.js'; import type { IProtocolUpstreamConfig, IUpstreamFetchContext, IUpstreamRegistryConfig, } from '../upstream/interfaces.upstream.js'; /** * Composer-specific upstream implementation. * * Handles: * - Package metadata fetching (packages.json, provider-includes) * - Package version metadata (p2/{vendor}/{package}.json) * - Dist file (zip) proxying * - Packagist v2 API support */ export class ComposerUpstream extends BaseUpstream { protected readonly protocolName = 'composer'; constructor( config: IProtocolUpstreamConfig, logger?: plugins.smartlog.Smartlog, ) { super(config, logger); } /** * Fetch the root packages.json from upstream. */ public async fetchPackagesJson(): Promise { const context: IUpstreamFetchContext = { protocol: 'composer', resource: '*', resourceType: 'root', path: '/packages.json', method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch package metadata using v2 API (p2/{vendor}/{package}.json). */ public async fetchPackageMetadata(vendor: string, packageName: string): Promise { const fullName = `${vendor}/${packageName}`; const path = `/p2/${vendor}/${packageName}.json`; const context: IUpstreamFetchContext = { protocol: 'composer', resource: fullName, resourceType: 'metadata', path, method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch package metadata with dev versions (p2/{vendor}/{package}~dev.json). */ public async fetchPackageDevMetadata(vendor: string, packageName: string): Promise { const fullName = `${vendor}/${packageName}`; const path = `/p2/${vendor}/${packageName}~dev.json`; const context: IUpstreamFetchContext = { protocol: 'composer', resource: fullName, resourceType: 'metadata-dev', path, method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch a provider-includes file. */ public async fetchProviderIncludes(path: string): Promise { const context: IUpstreamFetchContext = { protocol: 'composer', resource: '*', resourceType: 'provider', path: path.startsWith('/') ? path : `/${path}`, method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch a dist file (zip) from upstream. */ public async fetchDist(url: string): Promise { // Parse the URL to get the path let path: string; try { const parsed = new URL(url); path = parsed.pathname; } catch { path = url; } const context: IUpstreamFetchContext = { protocol: 'composer', resource: '*', resourceType: 'dist', path, method: 'GET', headers: { 'accept': 'application/zip, application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Override URL building for Composer-specific handling. */ protected buildUpstreamUrl( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): string { let baseUrl = upstream.url; // Remove trailing slash if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } return `${baseUrl}${context.path}`; } }