@push.rocks/smartregistry 1.4.0 → 1.5.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.
@@ -0,0 +1,470 @@
1
+ import { Smartlog } from '@push.rocks/smartlog';
2
+ import { BaseRegistry } from '../core/classes.baseregistry.js';
3
+ import { RegistryStorage } from '../core/classes.registrystorage.js';
4
+ import { AuthManager } from '../core/classes.authmanager.js';
5
+ import * as helpers from './helpers.pypi.js';
6
+ /**
7
+ * PyPI registry implementation
8
+ * Implements PEP 503 (Simple API), PEP 691 (JSON API), and legacy upload API
9
+ */
10
+ export class PypiRegistry extends BaseRegistry {
11
+ storage;
12
+ authManager;
13
+ basePath = '/pypi';
14
+ registryUrl;
15
+ logger;
16
+ constructor(storage, authManager, basePath = '/pypi', registryUrl = 'http://localhost:5000') {
17
+ super();
18
+ this.storage = storage;
19
+ this.authManager = authManager;
20
+ this.basePath = basePath;
21
+ this.registryUrl = registryUrl;
22
+ // Initialize logger
23
+ this.logger = new Smartlog({
24
+ logContext: {
25
+ company: 'push.rocks',
26
+ companyunit: 'smartregistry',
27
+ containerName: 'pypi-registry',
28
+ environment: process.env.NODE_ENV || 'development',
29
+ runtime: 'node',
30
+ zone: 'pypi'
31
+ }
32
+ });
33
+ this.logger.enableConsole();
34
+ }
35
+ async init() {
36
+ // Initialize root Simple API index if not exists
37
+ const existingIndex = await this.storage.getPypiSimpleRootIndex();
38
+ if (!existingIndex) {
39
+ const html = helpers.generateSimpleRootHtml([]);
40
+ await this.storage.putPypiSimpleRootIndex(html);
41
+ this.logger.log('info', 'Initialized PyPI root index');
42
+ }
43
+ }
44
+ getBasePath() {
45
+ return this.basePath;
46
+ }
47
+ async handleRequest(context) {
48
+ let path = context.path.replace(this.basePath, '');
49
+ // Also handle /simple path prefix
50
+ if (path.startsWith('/simple')) {
51
+ path = path.replace('/simple', '');
52
+ return this.handleSimpleRequest(path, context);
53
+ }
54
+ // Extract token (Basic Auth or Bearer)
55
+ const token = await this.extractToken(context);
56
+ this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
57
+ method: context.method,
58
+ path,
59
+ hasAuth: !!token
60
+ });
61
+ // Root upload endpoint (POST /)
62
+ if ((path === '/' || path === '') && context.method === 'POST') {
63
+ return this.handleUpload(context, token);
64
+ }
65
+ // Package metadata JSON API: GET /pypi/{package}/json
66
+ const jsonMatch = path.match(/^\/pypi\/([^\/]+)\/json$/);
67
+ if (jsonMatch && context.method === 'GET') {
68
+ return this.handlePackageJson(jsonMatch[1]);
69
+ }
70
+ // Version-specific JSON API: GET /pypi/{package}/{version}/json
71
+ const versionJsonMatch = path.match(/^\/pypi\/([^\/]+)\/([^\/]+)\/json$/);
72
+ if (versionJsonMatch && context.method === 'GET') {
73
+ return this.handleVersionJson(versionJsonMatch[1], versionJsonMatch[2]);
74
+ }
75
+ // Package file download: GET /packages/{package}/{filename}
76
+ const downloadMatch = path.match(/^\/packages\/([^\/]+)\/(.+)$/);
77
+ if (downloadMatch && context.method === 'GET') {
78
+ return this.handleDownload(downloadMatch[1], downloadMatch[2]);
79
+ }
80
+ // Delete package: DELETE /packages/{package}
81
+ if (path.match(/^\/packages\/([^\/]+)$/) && context.method === 'DELETE') {
82
+ const packageName = path.match(/^\/packages\/([^\/]+)$/)?.[1];
83
+ return this.handleDeletePackage(packageName, token);
84
+ }
85
+ // Delete version: DELETE /packages/{package}/{version}
86
+ const deleteVersionMatch = path.match(/^\/packages\/([^\/]+)\/([^\/]+)$/);
87
+ if (deleteVersionMatch && context.method === 'DELETE') {
88
+ return this.handleDeleteVersion(deleteVersionMatch[1], deleteVersionMatch[2], token);
89
+ }
90
+ return {
91
+ status: 404,
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: Buffer.from(JSON.stringify({ message: 'Not Found' })),
94
+ };
95
+ }
96
+ /**
97
+ * Check if token has permission for resource
98
+ */
99
+ async checkPermission(token, resource, action) {
100
+ if (!token)
101
+ return false;
102
+ return this.authManager.authorize(token, `pypi:package:${resource}`, action);
103
+ }
104
+ /**
105
+ * Handle Simple API requests (PEP 503 HTML or PEP 691 JSON)
106
+ */
107
+ async handleSimpleRequest(path, context) {
108
+ // Ensure path ends with / (PEP 503 requirement)
109
+ if (!path.endsWith('/') && !path.includes('.')) {
110
+ return {
111
+ status: 301,
112
+ headers: { 'Location': `${this.basePath}/simple${path}/` },
113
+ body: Buffer.from(''),
114
+ };
115
+ }
116
+ // Root index: /simple/
117
+ if (path === '/' || path === '') {
118
+ return this.handleSimpleRoot(context);
119
+ }
120
+ // Package index: /simple/{package}/
121
+ const packageMatch = path.match(/^\/([^\/]+)\/$/);
122
+ if (packageMatch) {
123
+ return this.handleSimplePackage(packageMatch[1], context);
124
+ }
125
+ return {
126
+ status: 404,
127
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
128
+ body: Buffer.from('<html><body><h1>404 Not Found</h1></body></html>'),
129
+ };
130
+ }
131
+ /**
132
+ * Handle Simple API root index
133
+ * Returns HTML (PEP 503) or JSON (PEP 691) based on Accept header
134
+ */
135
+ async handleSimpleRoot(context) {
136
+ const acceptHeader = context.headers['accept'] || context.headers['Accept'] || '';
137
+ const preferJson = acceptHeader.includes('application/vnd.pypi.simple') &&
138
+ acceptHeader.includes('json');
139
+ const packages = await this.storage.listPypiPackages();
140
+ if (preferJson) {
141
+ // PEP 691: JSON response
142
+ const response = helpers.generateJsonRootResponse(packages);
143
+ return {
144
+ status: 200,
145
+ headers: {
146
+ 'Content-Type': 'application/vnd.pypi.simple.v1+json',
147
+ 'Cache-Control': 'public, max-age=600'
148
+ },
149
+ body: Buffer.from(JSON.stringify(response)),
150
+ };
151
+ }
152
+ else {
153
+ // PEP 503: HTML response
154
+ const html = helpers.generateSimpleRootHtml(packages);
155
+ // Update stored index
156
+ await this.storage.putPypiSimpleRootIndex(html);
157
+ return {
158
+ status: 200,
159
+ headers: {
160
+ 'Content-Type': 'text/html; charset=utf-8',
161
+ 'Cache-Control': 'public, max-age=600'
162
+ },
163
+ body: Buffer.from(html),
164
+ };
165
+ }
166
+ }
167
+ /**
168
+ * Handle Simple API package index
169
+ * Returns HTML (PEP 503) or JSON (PEP 691) based on Accept header
170
+ */
171
+ async handleSimplePackage(packageName, context) {
172
+ const normalized = helpers.normalizePypiPackageName(packageName);
173
+ // Get package metadata
174
+ const metadata = await this.storage.getPypiPackageMetadata(normalized);
175
+ if (!metadata) {
176
+ return {
177
+ status: 404,
178
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
179
+ body: Buffer.from('<html><body><h1>404 Not Found</h1></body></html>'),
180
+ };
181
+ }
182
+ // Build file list from all versions
183
+ const files = [];
184
+ for (const [version, versionMeta] of Object.entries(metadata.versions || {})) {
185
+ for (const file of versionMeta.files || []) {
186
+ files.push({
187
+ filename: file.filename,
188
+ url: `${this.registryUrl}/pypi/packages/${normalized}/${file.filename}`,
189
+ hashes: file.hashes,
190
+ 'requires-python': file['requires-python'],
191
+ yanked: file.yanked || versionMeta.yanked,
192
+ size: file.size,
193
+ 'upload-time': file['upload-time'],
194
+ });
195
+ }
196
+ }
197
+ const acceptHeader = context.headers['accept'] || context.headers['Accept'] || '';
198
+ const preferJson = acceptHeader.includes('application/vnd.pypi.simple') &&
199
+ acceptHeader.includes('json');
200
+ if (preferJson) {
201
+ // PEP 691: JSON response
202
+ const response = helpers.generateJsonPackageResponse(normalized, files);
203
+ return {
204
+ status: 200,
205
+ headers: {
206
+ 'Content-Type': 'application/vnd.pypi.simple.v1+json',
207
+ 'Cache-Control': 'public, max-age=300'
208
+ },
209
+ body: Buffer.from(JSON.stringify(response)),
210
+ };
211
+ }
212
+ else {
213
+ // PEP 503: HTML response
214
+ const html = helpers.generateSimplePackageHtml(normalized, files, this.registryUrl);
215
+ // Update stored index
216
+ await this.storage.putPypiSimpleIndex(normalized, html);
217
+ return {
218
+ status: 200,
219
+ headers: {
220
+ 'Content-Type': 'text/html; charset=utf-8',
221
+ 'Cache-Control': 'public, max-age=300'
222
+ },
223
+ body: Buffer.from(html),
224
+ };
225
+ }
226
+ }
227
+ /**
228
+ * Extract authentication token from request
229
+ */
230
+ async extractToken(context) {
231
+ const authHeader = context.headers['authorization'] || context.headers['Authorization'];
232
+ if (!authHeader)
233
+ return null;
234
+ // Handle Basic Auth (username:password or __token__:token)
235
+ if (authHeader.startsWith('Basic ')) {
236
+ const base64 = authHeader.substring(6);
237
+ const decoded = Buffer.from(base64, 'base64').toString('utf-8');
238
+ const [username, password] = decoded.split(':');
239
+ // PyPI token authentication: username = __token__
240
+ if (username === '__token__') {
241
+ return this.authManager.validateToken(password, 'pypi');
242
+ }
243
+ // Username/password authentication (would need user lookup)
244
+ // For now, not implemented
245
+ return null;
246
+ }
247
+ // Handle Bearer token
248
+ if (authHeader.startsWith('Bearer ')) {
249
+ const token = authHeader.substring(7);
250
+ return this.authManager.validateToken(token, 'pypi');
251
+ }
252
+ return null;
253
+ }
254
+ /**
255
+ * Handle package upload (multipart/form-data)
256
+ * POST / with :action=file_upload
257
+ */
258
+ async handleUpload(context, token) {
259
+ if (!token) {
260
+ return {
261
+ status: 401,
262
+ headers: {
263
+ 'Content-Type': 'application/json',
264
+ 'WWW-Authenticate': 'Basic realm="PyPI"'
265
+ },
266
+ body: Buffer.from(JSON.stringify({ message: 'Authentication required' })),
267
+ };
268
+ }
269
+ try {
270
+ // Parse multipart form data (context.body should be parsed by server)
271
+ const formData = context.body; // Assuming parsed multipart data
272
+ if (!formData || formData[':action'] !== 'file_upload') {
273
+ return this.errorResponse(400, 'Invalid upload request');
274
+ }
275
+ // Extract required fields
276
+ const packageName = formData.name;
277
+ const version = formData.version;
278
+ const filename = formData.content?.filename;
279
+ const fileData = formData.content?.data;
280
+ const filetype = formData.filetype; // 'bdist_wheel' or 'sdist'
281
+ const pyversion = formData.pyversion;
282
+ if (!packageName || !version || !filename || !fileData) {
283
+ return this.errorResponse(400, 'Missing required fields');
284
+ }
285
+ // Validate package name
286
+ if (!helpers.isValidPackageName(packageName)) {
287
+ return this.errorResponse(400, 'Invalid package name');
288
+ }
289
+ const normalized = helpers.normalizePypiPackageName(packageName);
290
+ // Check permission
291
+ if (!(await this.checkPermission(token, normalized, 'write'))) {
292
+ return this.errorResponse(403, 'Insufficient permissions');
293
+ }
294
+ // Calculate hashes
295
+ const hashes = {};
296
+ if (formData.sha256_digest) {
297
+ hashes.sha256 = formData.sha256_digest;
298
+ }
299
+ else {
300
+ hashes.sha256 = await helpers.calculateHash(fileData, 'sha256');
301
+ }
302
+ if (formData.md5_digest) {
303
+ // MD5 digest in PyPI is urlsafe base64, convert to hex
304
+ hashes.md5 = await helpers.calculateHash(fileData, 'md5');
305
+ }
306
+ if (formData.blake2_256_digest) {
307
+ hashes.blake2b = formData.blake2_256_digest;
308
+ }
309
+ // Store file
310
+ await this.storage.putPypiPackageFile(normalized, filename, fileData);
311
+ // Update metadata
312
+ let metadata = await this.storage.getPypiPackageMetadata(normalized);
313
+ if (!metadata) {
314
+ metadata = {
315
+ name: normalized,
316
+ versions: {},
317
+ };
318
+ }
319
+ if (!metadata.versions[version]) {
320
+ metadata.versions[version] = {
321
+ version,
322
+ files: [],
323
+ };
324
+ }
325
+ // Add file to version
326
+ metadata.versions[version].files.push({
327
+ filename,
328
+ path: `pypi/packages/${normalized}/${filename}`,
329
+ filetype,
330
+ python_version: pyversion,
331
+ hashes,
332
+ size: fileData.length,
333
+ 'requires-python': formData.requires_python,
334
+ 'upload-time': new Date().toISOString(),
335
+ 'uploaded-by': token.userId,
336
+ });
337
+ // Store core metadata if provided
338
+ if (formData.summary || formData.description) {
339
+ metadata.versions[version].metadata = helpers.extractCoreMetadata(formData);
340
+ }
341
+ metadata['last-modified'] = new Date().toISOString();
342
+ await this.storage.putPypiPackageMetadata(normalized, metadata);
343
+ this.logger.log('info', `Package uploaded: ${normalized} ${version}`, {
344
+ filename,
345
+ size: fileData.length
346
+ });
347
+ return {
348
+ status: 200,
349
+ headers: { 'Content-Type': 'application/json' },
350
+ body: Buffer.from(JSON.stringify({
351
+ message: 'Package uploaded successfully',
352
+ url: `${this.registryUrl}/pypi/packages/${normalized}/${filename}`
353
+ })),
354
+ };
355
+ }
356
+ catch (error) {
357
+ this.logger.log('error', 'Upload failed', { error: error.message });
358
+ return this.errorResponse(500, 'Upload failed: ' + error.message);
359
+ }
360
+ }
361
+ /**
362
+ * Handle package download
363
+ */
364
+ async handleDownload(packageName, filename) {
365
+ const normalized = helpers.normalizePypiPackageName(packageName);
366
+ const fileData = await this.storage.getPypiPackageFile(normalized, filename);
367
+ if (!fileData) {
368
+ return {
369
+ status: 404,
370
+ headers: { 'Content-Type': 'application/json' },
371
+ body: Buffer.from(JSON.stringify({ message: 'File not found' })),
372
+ };
373
+ }
374
+ return {
375
+ status: 200,
376
+ headers: {
377
+ 'Content-Type': 'application/octet-stream',
378
+ 'Content-Disposition': `attachment; filename="${filename}"`,
379
+ 'Content-Length': fileData.length.toString()
380
+ },
381
+ body: fileData,
382
+ };
383
+ }
384
+ /**
385
+ * Handle package JSON API (all versions)
386
+ */
387
+ async handlePackageJson(packageName) {
388
+ const normalized = helpers.normalizePypiPackageName(packageName);
389
+ const metadata = await this.storage.getPypiPackageMetadata(normalized);
390
+ if (!metadata) {
391
+ return this.errorResponse(404, 'Package not found');
392
+ }
393
+ return {
394
+ status: 200,
395
+ headers: {
396
+ 'Content-Type': 'application/json',
397
+ 'Cache-Control': 'public, max-age=300'
398
+ },
399
+ body: Buffer.from(JSON.stringify(metadata)),
400
+ };
401
+ }
402
+ /**
403
+ * Handle version-specific JSON API
404
+ */
405
+ async handleVersionJson(packageName, version) {
406
+ const normalized = helpers.normalizePypiPackageName(packageName);
407
+ const metadata = await this.storage.getPypiPackageMetadata(normalized);
408
+ if (!metadata || !metadata.versions[version]) {
409
+ return this.errorResponse(404, 'Version not found');
410
+ }
411
+ return {
412
+ status: 200,
413
+ headers: {
414
+ 'Content-Type': 'application/json',
415
+ 'Cache-Control': 'public, max-age=300'
416
+ },
417
+ body: Buffer.from(JSON.stringify(metadata.versions[version])),
418
+ };
419
+ }
420
+ /**
421
+ * Handle package deletion
422
+ */
423
+ async handleDeletePackage(packageName, token) {
424
+ if (!token) {
425
+ return this.errorResponse(401, 'Authentication required');
426
+ }
427
+ const normalized = helpers.normalizePypiPackageName(packageName);
428
+ if (!(await this.checkPermission(token, normalized, 'delete'))) {
429
+ return this.errorResponse(403, 'Insufficient permissions');
430
+ }
431
+ await this.storage.deletePypiPackage(normalized);
432
+ this.logger.log('info', `Package deleted: ${normalized}`);
433
+ return {
434
+ status: 204,
435
+ headers: {},
436
+ body: Buffer.from(''),
437
+ };
438
+ }
439
+ /**
440
+ * Handle version deletion
441
+ */
442
+ async handleDeleteVersion(packageName, version, token) {
443
+ if (!token) {
444
+ return this.errorResponse(401, 'Authentication required');
445
+ }
446
+ const normalized = helpers.normalizePypiPackageName(packageName);
447
+ if (!(await this.checkPermission(token, normalized, 'delete'))) {
448
+ return this.errorResponse(403, 'Insufficient permissions');
449
+ }
450
+ await this.storage.deletePypiPackageVersion(normalized, version);
451
+ this.logger.log('info', `Version deleted: ${normalized} ${version}`);
452
+ return {
453
+ status: 204,
454
+ headers: {},
455
+ body: Buffer.from(''),
456
+ };
457
+ }
458
+ /**
459
+ * Helper: Create error response
460
+ */
461
+ errorResponse(status, message) {
462
+ const error = { message, status };
463
+ return {
464
+ status,
465
+ headers: { 'Content-Type': 'application/json' },
466
+ body: Buffer.from(JSON.stringify(error)),
467
+ };
468
+ }
469
+ }
470
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5weXBpcmVnaXN0cnkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9weXBpL2NsYXNzZXMucHlwaXJlZ2lzdHJ5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDL0QsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQ3JFLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQVE3RCxPQUFPLEtBQUssT0FBTyxNQUFNLG1CQUFtQixDQUFDO0FBRTdDOzs7R0FHRztBQUNILE1BQU0sT0FBTyxZQUFhLFNBQVEsWUFBWTtJQUNwQyxPQUFPLENBQWtCO0lBQ3pCLFdBQVcsQ0FBYztJQUN6QixRQUFRLEdBQVcsT0FBTyxDQUFDO0lBQzNCLFdBQVcsQ0FBUztJQUNwQixNQUFNLENBQVc7SUFFekIsWUFDRSxPQUF3QixFQUN4QixXQUF3QixFQUN4QixXQUFtQixPQUFPLEVBQzFCLGNBQXNCLHVCQUF1QjtRQUU3QyxLQUFLLEVBQUUsQ0FBQztRQUNSLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBQy9CLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO1FBQ3pCLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBRS9CLG9CQUFvQjtRQUNwQixJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksUUFBUSxDQUFDO1lBQ3pCLFVBQVUsRUFBRTtnQkFDVixPQUFPLEVBQUUsWUFBWTtnQkFDckIsV0FBVyxFQUFFLGVBQWU7Z0JBQzVCLGFBQWEsRUFBRSxlQUFlO2dCQUM5QixXQUFXLEVBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFnQixJQUFJLGFBQWE7Z0JB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
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Helper functions for PyPI registry
3
+ * Package name normalization, HTML generation, etc.
4
+ */
5
+ import type { IPypiFile } from './interfaces.pypi.js';
6
+ /**
7
+ * Normalize package name according to PEP 503
8
+ * Lowercase and replace runs of [._-] with a single dash
9
+ * @param name - Package name
10
+ * @returns Normalized name
11
+ */
12
+ export declare function normalizePypiPackageName(name: string): string;
13
+ /**
14
+ * Escape HTML special characters to prevent XSS
15
+ * @param str - String to escape
16
+ * @returns Escaped string
17
+ */
18
+ export declare function escapeHtml(str: string): string;
19
+ /**
20
+ * Generate PEP 503 compliant HTML for root index (all packages)
21
+ * @param packages - List of package names
22
+ * @returns HTML string
23
+ */
24
+ export declare function generateSimpleRootHtml(packages: string[]): string;
25
+ /**
26
+ * Generate PEP 503 compliant HTML for package index (file list)
27
+ * @param packageName - Package name (normalized)
28
+ * @param files - List of files
29
+ * @param baseUrl - Base URL for downloads
30
+ * @returns HTML string
31
+ */
32
+ export declare function generateSimplePackageHtml(packageName: string, files: IPypiFile[], baseUrl: string): string;
33
+ /**
34
+ * Parse filename to extract package info
35
+ * Supports wheel and sdist formats
36
+ * @param filename - Package filename
37
+ * @returns Parsed info or null
38
+ */
39
+ export declare function parsePackageFilename(filename: string): {
40
+ name: string;
41
+ version: string;
42
+ filetype: 'bdist_wheel' | 'sdist';
43
+ pythonVersion?: string;
44
+ } | null;
45
+ /**
46
+ * Calculate hash digest for a buffer
47
+ * @param data - Data to hash
48
+ * @param algorithm - Hash algorithm (sha256, md5, blake2b)
49
+ * @returns Hex-encoded hash
50
+ */
51
+ export declare function calculateHash(data: Buffer, algorithm: 'sha256' | 'md5' | 'blake2b'): Promise<string>;
52
+ /**
53
+ * Validate package name
54
+ * Must contain only ASCII letters, numbers, ., -, and _
55
+ * @param name - Package name
56
+ * @returns true if valid
57
+ */
58
+ export declare function isValidPackageName(name: string): boolean;
59
+ /**
60
+ * Validate version string (basic check)
61
+ * @param version - Version string
62
+ * @returns true if valid
63
+ */
64
+ export declare function isValidVersion(version: string): boolean;
65
+ /**
66
+ * Extract metadata from package metadata
67
+ * Filters and normalizes metadata fields
68
+ * @param metadata - Raw metadata object
69
+ * @returns Filtered metadata
70
+ */
71
+ export declare function extractCoreMetadata(metadata: Record<string, any>): Record<string, any>;
72
+ /**
73
+ * Generate JSON API response for package list (PEP 691)
74
+ * @param packages - List of package names
75
+ * @returns JSON object
76
+ */
77
+ export declare function generateJsonRootResponse(packages: string[]): any;
78
+ /**
79
+ * Generate JSON API response for package files (PEP 691)
80
+ * @param packageName - Package name (normalized)
81
+ * @param files - List of files
82
+ * @returns JSON object
83
+ */
84
+ export declare function generateJsonPackageResponse(packageName: string, files: IPypiFile[]): any;