@push.rocks/smartregistry 1.8.0 → 2.0.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.smartregistry.js +8 -8
- package/dist_ts/core/classes.authmanager.d.ts +1 -1
- package/dist_ts/core/classes.authmanager.js +31 -7
- package/dist_ts/oci/classes.ociregistry.d.ts +9 -0
- package/dist_ts/oci/classes.ociregistry.js +35 -42
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +2 -0
- package/dist_ts/pypi/classes.pypiregistry.js +97 -27
- package/dist_ts/pypi/interfaces.pypi.d.ts +1 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +13 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.js +142 -23
- package/dist_ts/rubygems/helpers.rubygems.d.ts +40 -0
- package/dist_ts/rubygems/helpers.rubygems.js +141 -1
- package/dist_ts/rubygems/interfaces.rubygems.d.ts +1 -1
- package/package.json +2 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.smartregistry.ts +8 -8
- package/ts/core/classes.authmanager.ts +37 -6
- package/ts/oci/classes.ociregistry.ts +36 -41
- package/ts/plugins.ts +2 -1
- package/ts/pypi/classes.pypiregistry.ts +103 -26
- package/ts/pypi/interfaces.pypi.ts +1 -1
- package/ts/rubygems/classes.rubygemsregistry.ts +156 -22
- package/ts/rubygems/helpers.rubygems.ts +175 -0
- package/ts/rubygems/interfaces.rubygems.ts +1 -1
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartregistry',
|
|
6
|
-
version: '
|
|
6
|
+
version: '2.0.0',
|
|
7
7
|
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
|
8
8
|
}
|
|
@@ -41,7 +41,7 @@ export class SmartRegistry {
|
|
|
41
41
|
|
|
42
42
|
// Initialize OCI registry if enabled
|
|
43
43
|
if (this.config.oci?.enabled) {
|
|
44
|
-
const ociBasePath = this.config.oci.basePath
|
|
44
|
+
const ociBasePath = this.config.oci.basePath ?? '/oci';
|
|
45
45
|
const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath);
|
|
46
46
|
await ociRegistry.init();
|
|
47
47
|
this.registries.set('oci', ociRegistry);
|
|
@@ -49,7 +49,7 @@ export class SmartRegistry {
|
|
|
49
49
|
|
|
50
50
|
// Initialize NPM registry if enabled
|
|
51
51
|
if (this.config.npm?.enabled) {
|
|
52
|
-
const npmBasePath = this.config.npm.basePath
|
|
52
|
+
const npmBasePath = this.config.npm.basePath ?? '/npm';
|
|
53
53
|
const registryUrl = `http://localhost:5000${npmBasePath}`; // TODO: Make configurable
|
|
54
54
|
const npmRegistry = new NpmRegistry(this.storage, this.authManager, npmBasePath, registryUrl);
|
|
55
55
|
await npmRegistry.init();
|
|
@@ -58,7 +58,7 @@ export class SmartRegistry {
|
|
|
58
58
|
|
|
59
59
|
// Initialize Maven registry if enabled
|
|
60
60
|
if (this.config.maven?.enabled) {
|
|
61
|
-
const mavenBasePath = this.config.maven.basePath
|
|
61
|
+
const mavenBasePath = this.config.maven.basePath ?? '/maven';
|
|
62
62
|
const registryUrl = `http://localhost:5000${mavenBasePath}`; // TODO: Make configurable
|
|
63
63
|
const mavenRegistry = new MavenRegistry(this.storage, this.authManager, mavenBasePath, registryUrl);
|
|
64
64
|
await mavenRegistry.init();
|
|
@@ -67,7 +67,7 @@ export class SmartRegistry {
|
|
|
67
67
|
|
|
68
68
|
// Initialize Cargo registry if enabled
|
|
69
69
|
if (this.config.cargo?.enabled) {
|
|
70
|
-
const cargoBasePath = this.config.cargo.basePath
|
|
70
|
+
const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
|
|
71
71
|
const registryUrl = `http://localhost:5000${cargoBasePath}`; // TODO: Make configurable
|
|
72
72
|
const cargoRegistry = new CargoRegistry(this.storage, this.authManager, cargoBasePath, registryUrl);
|
|
73
73
|
await cargoRegistry.init();
|
|
@@ -76,7 +76,7 @@ export class SmartRegistry {
|
|
|
76
76
|
|
|
77
77
|
// Initialize Composer registry if enabled
|
|
78
78
|
if (this.config.composer?.enabled) {
|
|
79
|
-
const composerBasePath = this.config.composer.basePath
|
|
79
|
+
const composerBasePath = this.config.composer.basePath ?? '/composer';
|
|
80
80
|
const registryUrl = `http://localhost:5000${composerBasePath}`; // TODO: Make configurable
|
|
81
81
|
const composerRegistry = new ComposerRegistry(this.storage, this.authManager, composerBasePath, registryUrl);
|
|
82
82
|
await composerRegistry.init();
|
|
@@ -85,7 +85,7 @@ export class SmartRegistry {
|
|
|
85
85
|
|
|
86
86
|
// Initialize PyPI registry if enabled
|
|
87
87
|
if (this.config.pypi?.enabled) {
|
|
88
|
-
const pypiBasePath = this.config.pypi.basePath
|
|
88
|
+
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
89
89
|
const registryUrl = `http://localhost:5000`; // TODO: Make configurable
|
|
90
90
|
const pypiRegistry = new PypiRegistry(this.storage, this.authManager, pypiBasePath, registryUrl);
|
|
91
91
|
await pypiRegistry.init();
|
|
@@ -94,7 +94,7 @@ export class SmartRegistry {
|
|
|
94
94
|
|
|
95
95
|
// Initialize RubyGems registry if enabled
|
|
96
96
|
if (this.config.rubygems?.enabled) {
|
|
97
|
-
const rubygemsBasePath = this.config.rubygems.basePath
|
|
97
|
+
const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
|
|
98
98
|
const registryUrl = `http://localhost:5000${rubygemsBasePath}`; // TODO: Make configurable
|
|
99
99
|
const rubygemsRegistry = new RubyGemsRegistry(this.storage, this.authManager, rubygemsBasePath, registryUrl);
|
|
100
100
|
await rubygemsRegistry.init();
|
|
@@ -153,7 +153,7 @@ export class SmartRegistry {
|
|
|
153
153
|
|
|
154
154
|
// Route to PyPI registry (also handles /simple prefix)
|
|
155
155
|
if (this.config.pypi?.enabled) {
|
|
156
|
-
const pypiBasePath = this.config.pypi.basePath
|
|
156
|
+
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
157
157
|
if (path.startsWith(pypiBasePath) || path.startsWith('/simple')) {
|
|
158
158
|
const pypiRegistry = this.registries.get('pypi');
|
|
159
159
|
if (pypiRegistry) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Unified authentication manager for all registry protocols
|
|
@@ -136,7 +137,7 @@ export class AuthManager {
|
|
|
136
137
|
* @param userId - User ID
|
|
137
138
|
* @param scopes - Permission scopes
|
|
138
139
|
* @param expiresIn - Expiration time in seconds
|
|
139
|
-
* @returns JWT token string
|
|
140
|
+
* @returns JWT token string (HMAC-SHA256 signed)
|
|
140
141
|
*/
|
|
141
142
|
public async createOciToken(
|
|
142
143
|
userId: string,
|
|
@@ -158,9 +159,17 @@ export class AuthManager {
|
|
|
158
159
|
access: this.scopesToOciAccess(scopes),
|
|
159
160
|
};
|
|
160
161
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// Create JWT with HMAC-SHA256 signature
|
|
163
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
164
|
+
const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
165
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
166
|
+
|
|
167
|
+
const signature = crypto
|
|
168
|
+
.createHmac('sha256', this.config.jwtSecret)
|
|
169
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
170
|
+
.digest('base64url');
|
|
171
|
+
|
|
172
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
/**
|
|
@@ -170,8 +179,25 @@ export class AuthManager {
|
|
|
170
179
|
*/
|
|
171
180
|
public async validateOciToken(jwt: string): Promise<IAuthToken | null> {
|
|
172
181
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
const parts = jwt.split('.');
|
|
183
|
+
if (parts.length !== 3) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
188
|
+
|
|
189
|
+
// Verify signature
|
|
190
|
+
const expectedSignature = crypto
|
|
191
|
+
.createHmac('sha256', this.config.jwtSecret)
|
|
192
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
193
|
+
.digest('base64url');
|
|
194
|
+
|
|
195
|
+
if (signatureB64 !== expectedSignature) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Decode and parse payload
|
|
200
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8'));
|
|
175
201
|
|
|
176
202
|
// Check expiration
|
|
177
203
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -179,6 +205,11 @@ export class AuthManager {
|
|
|
179
205
|
return null;
|
|
180
206
|
}
|
|
181
207
|
|
|
208
|
+
// Check not-before time
|
|
209
|
+
if (payload.nbf && payload.nbf > now) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
182
213
|
// Convert to unified token format
|
|
183
214
|
const scopes = this.ociAccessToScopes(payload.access || []);
|
|
184
215
|
|
|
@@ -180,11 +180,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
180
180
|
body?: Buffer | any
|
|
181
181
|
): Promise<IResponse> {
|
|
182
182
|
if (!await this.checkPermission(token, repository, 'push')) {
|
|
183
|
-
return
|
|
184
|
-
status: 401,
|
|
185
|
-
headers: {},
|
|
186
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
187
|
-
};
|
|
183
|
+
return this.createUnauthorizedResponse(repository, 'push');
|
|
188
184
|
}
|
|
189
185
|
|
|
190
186
|
// Check for monolithic upload (digest + body provided)
|
|
@@ -255,11 +251,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
255
251
|
}
|
|
256
252
|
|
|
257
253
|
if (!await this.checkPermission(token, session.repository, 'push')) {
|
|
258
|
-
return
|
|
259
|
-
status: 401,
|
|
260
|
-
headers: {},
|
|
261
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
262
|
-
};
|
|
254
|
+
return this.createUnauthorizedResponse(session.repository, 'push');
|
|
263
255
|
}
|
|
264
256
|
|
|
265
257
|
switch (method) {
|
|
@@ -336,11 +328,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
336
328
|
token: IAuthToken | null
|
|
337
329
|
): Promise<IResponse> {
|
|
338
330
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
339
|
-
return
|
|
340
|
-
status: 401,
|
|
341
|
-
headers: {},
|
|
342
|
-
body: null,
|
|
343
|
-
};
|
|
331
|
+
return this.createUnauthorizedHeadResponse(repository, 'pull');
|
|
344
332
|
}
|
|
345
333
|
|
|
346
334
|
// Similar logic as getManifest but return headers only
|
|
@@ -437,11 +425,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
437
425
|
}
|
|
438
426
|
|
|
439
427
|
if (!await this.checkPermission(token, repository, 'delete')) {
|
|
440
|
-
return
|
|
441
|
-
status: 401,
|
|
442
|
-
headers: {},
|
|
443
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
444
|
-
};
|
|
428
|
+
return this.createUnauthorizedResponse(repository, 'delete');
|
|
445
429
|
}
|
|
446
430
|
|
|
447
431
|
await this.storage.deleteOciManifest(repository, digest);
|
|
@@ -460,11 +444,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
460
444
|
range?: string
|
|
461
445
|
): Promise<IResponse> {
|
|
462
446
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
463
|
-
return
|
|
464
|
-
status: 401,
|
|
465
|
-
headers: {},
|
|
466
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
467
|
-
};
|
|
447
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
468
448
|
}
|
|
469
449
|
|
|
470
450
|
const data = await this.storage.getOciBlob(digest);
|
|
@@ -492,7 +472,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
492
472
|
token: IAuthToken | null
|
|
493
473
|
): Promise<IResponse> {
|
|
494
474
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
495
|
-
return
|
|
475
|
+
return this.createUnauthorizedHeadResponse(repository, 'pull');
|
|
496
476
|
}
|
|
497
477
|
|
|
498
478
|
const exists = await this.storage.ociBlobExists(digest);
|
|
@@ -518,11 +498,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
518
498
|
token: IAuthToken | null
|
|
519
499
|
): Promise<IResponse> {
|
|
520
500
|
if (!await this.checkPermission(token, repository, 'delete')) {
|
|
521
|
-
return
|
|
522
|
-
status: 401,
|
|
523
|
-
headers: {},
|
|
524
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
525
|
-
};
|
|
501
|
+
return this.createUnauthorizedResponse(repository, 'delete');
|
|
526
502
|
}
|
|
527
503
|
|
|
528
504
|
await this.storage.deleteOciBlob(digest);
|
|
@@ -631,11 +607,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
631
607
|
query: Record<string, string>
|
|
632
608
|
): Promise<IResponse> {
|
|
633
609
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
634
|
-
return
|
|
635
|
-
status: 401,
|
|
636
|
-
headers: {},
|
|
637
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
638
|
-
};
|
|
610
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
639
611
|
}
|
|
640
612
|
|
|
641
613
|
const tags = await this.getTagsData(repository);
|
|
@@ -660,11 +632,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
660
632
|
query: Record<string, string>
|
|
661
633
|
): Promise<IResponse> {
|
|
662
634
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
663
|
-
return
|
|
664
|
-
status: 401,
|
|
665
|
-
headers: {},
|
|
666
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
667
|
-
};
|
|
635
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
668
636
|
}
|
|
669
637
|
|
|
670
638
|
const response: IReferrersResponse = {
|
|
@@ -712,6 +680,33 @@ export class OciRegistry extends BaseRegistry {
|
|
|
712
680
|
};
|
|
713
681
|
}
|
|
714
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Create an unauthorized response with proper WWW-Authenticate header.
|
|
685
|
+
* Per OCI Distribution Spec, 401 responses MUST include WWW-Authenticate header.
|
|
686
|
+
*/
|
|
687
|
+
private createUnauthorizedResponse(repository: string, action: string): IResponse {
|
|
688
|
+
return {
|
|
689
|
+
status: 401,
|
|
690
|
+
headers: {
|
|
691
|
+
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:${action}"`,
|
|
692
|
+
},
|
|
693
|
+
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Create an unauthorized HEAD response (no body per HTTP spec).
|
|
699
|
+
*/
|
|
700
|
+
private createUnauthorizedHeadResponse(repository: string, action: string): IResponse {
|
|
701
|
+
return {
|
|
702
|
+
status: 401,
|
|
703
|
+
headers: {
|
|
704
|
+
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:${action}"`,
|
|
705
|
+
},
|
|
706
|
+
body: null,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
715
710
|
private startUploadSessionCleanup(): void {
|
|
716
711
|
this.cleanupInterval = setInterval(() => {
|
|
717
712
|
const now = new Date();
|
package/ts/plugins.ts
CHANGED
|
@@ -4,11 +4,12 @@ import * as path from 'path';
|
|
|
4
4
|
export { path };
|
|
5
5
|
|
|
6
6
|
// @push.rocks scope
|
|
7
|
+
import * as smartarchive from '@push.rocks/smartarchive';
|
|
7
8
|
import * as smartbucket from '@push.rocks/smartbucket';
|
|
8
9
|
import * as smartlog from '@push.rocks/smartlog';
|
|
9
10
|
import * as smartpath from '@push.rocks/smartpath';
|
|
10
11
|
|
|
11
|
-
export { smartbucket, smartlog, smartpath };
|
|
12
|
+
export { smartarchive, smartbucket, smartlog, smartpath };
|
|
12
13
|
|
|
13
14
|
// @tsclass scope
|
|
14
15
|
import * as tsclass from '@tsclass/tsclass';
|
|
@@ -85,14 +85,14 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
85
85
|
return this.handleUpload(context, token);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Package metadata JSON API: GET /
|
|
89
|
-
const jsonMatch = path.match(/^\/
|
|
88
|
+
// Package metadata JSON API: GET /{package}/json
|
|
89
|
+
const jsonMatch = path.match(/^\/([^\/]+)\/json$/);
|
|
90
90
|
if (jsonMatch && context.method === 'GET') {
|
|
91
91
|
return this.handlePackageJson(jsonMatch[1]);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// Version-specific JSON API: GET /
|
|
95
|
-
const versionJsonMatch = path.match(/^\/
|
|
94
|
+
// Version-specific JSON API: GET /{package}/{version}/json
|
|
95
|
+
const versionJsonMatch = path.match(/^\/([^\/]+)\/([^\/]+)\/json$/);
|
|
96
96
|
if (versionJsonMatch && context.method === 'GET') {
|
|
97
97
|
return this.handleVersionJson(versionJsonMatch[1], versionJsonMatch[2]);
|
|
98
98
|
}
|
|
@@ -118,7 +118,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
118
118
|
return {
|
|
119
119
|
status: 404,
|
|
120
120
|
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body:
|
|
121
|
+
body: { error: 'Not Found' },
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -185,7 +185,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
185
185
|
'Content-Type': 'application/vnd.pypi.simple.v1+json',
|
|
186
186
|
'Cache-Control': 'public, max-age=600'
|
|
187
187
|
},
|
|
188
|
-
body:
|
|
188
|
+
body: response,
|
|
189
189
|
};
|
|
190
190
|
} else {
|
|
191
191
|
// PEP 503: HTML response
|
|
@@ -200,7 +200,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
200
200
|
'Content-Type': 'text/html; charset=utf-8',
|
|
201
201
|
'Cache-Control': 'public, max-age=600'
|
|
202
202
|
},
|
|
203
|
-
body:
|
|
203
|
+
body: html,
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
}
|
|
@@ -215,11 +215,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
215
215
|
// Get package metadata
|
|
216
216
|
const metadata = await this.storage.getPypiPackageMetadata(normalized);
|
|
217
217
|
if (!metadata) {
|
|
218
|
-
return
|
|
219
|
-
status: 404,
|
|
220
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
221
|
-
body: Buffer.from('<html><body><h1>404 Not Found</h1></body></html>'),
|
|
222
|
-
};
|
|
218
|
+
return this.errorResponse(404, 'Package not found');
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
// Build file list from all versions
|
|
@@ -251,7 +247,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
251
247
|
'Content-Type': 'application/vnd.pypi.simple.v1+json',
|
|
252
248
|
'Cache-Control': 'public, max-age=300'
|
|
253
249
|
},
|
|
254
|
-
body:
|
|
250
|
+
body: response,
|
|
255
251
|
};
|
|
256
252
|
} else {
|
|
257
253
|
// PEP 503: HTML response
|
|
@@ -266,7 +262,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
266
262
|
'Content-Type': 'text/html; charset=utf-8',
|
|
267
263
|
'Cache-Control': 'public, max-age=300'
|
|
268
264
|
},
|
|
269
|
-
body:
|
|
265
|
+
body: html,
|
|
270
266
|
};
|
|
271
267
|
}
|
|
272
268
|
}
|
|
@@ -315,7 +311,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
315
311
|
'Content-Type': 'application/json',
|
|
316
312
|
'WWW-Authenticate': 'Basic realm="PyPI"'
|
|
317
313
|
},
|
|
318
|
-
body:
|
|
314
|
+
body: { error: 'Authentication required' },
|
|
319
315
|
};
|
|
320
316
|
}
|
|
321
317
|
|
|
@@ -327,11 +323,13 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
327
323
|
return this.errorResponse(400, 'Invalid upload request');
|
|
328
324
|
}
|
|
329
325
|
|
|
330
|
-
// Extract required fields
|
|
326
|
+
// Extract required fields - support both nested and flat body formats
|
|
331
327
|
const packageName = formData.name;
|
|
332
328
|
const version = formData.version;
|
|
333
|
-
|
|
334
|
-
const
|
|
329
|
+
// Support both: formData.content.filename (multipart parsed) and formData.filename (flat)
|
|
330
|
+
const filename = formData.content?.filename || formData.filename;
|
|
331
|
+
// Support both: formData.content.data (multipart parsed) and formData.content (Buffer directly)
|
|
332
|
+
const fileData = (formData.content?.data || (Buffer.isBuffer(formData.content) ? formData.content : null)) as Buffer;
|
|
335
333
|
const filetype = formData.filetype; // 'bdist_wheel' or 'sdist'
|
|
336
334
|
const pyversion = formData.pyversion;
|
|
337
335
|
|
|
@@ -431,12 +429,12 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
431
429
|
});
|
|
432
430
|
|
|
433
431
|
return {
|
|
434
|
-
status:
|
|
432
|
+
status: 201,
|
|
435
433
|
headers: { 'Content-Type': 'application/json' },
|
|
436
|
-
body:
|
|
434
|
+
body: {
|
|
437
435
|
message: 'Package uploaded successfully',
|
|
438
436
|
url: `${this.registryUrl}/pypi/packages/${normalized}/${filename}`
|
|
439
|
-
}
|
|
437
|
+
},
|
|
440
438
|
};
|
|
441
439
|
} catch (error) {
|
|
442
440
|
this.logger.log('error', 'Upload failed', { error: (error as Error).message });
|
|
@@ -455,7 +453,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
455
453
|
return {
|
|
456
454
|
status: 404,
|
|
457
455
|
headers: { 'Content-Type': 'application/json' },
|
|
458
|
-
body:
|
|
456
|
+
body: { error: 'File not found' },
|
|
459
457
|
};
|
|
460
458
|
}
|
|
461
459
|
|
|
@@ -472,6 +470,7 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
472
470
|
|
|
473
471
|
/**
|
|
474
472
|
* Handle package JSON API (all versions)
|
|
473
|
+
* Returns format compatible with official PyPI JSON API
|
|
475
474
|
*/
|
|
476
475
|
private async handlePackageJson(packageName: string): Promise<IResponse> {
|
|
477
476
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
@@ -481,18 +480,67 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
481
480
|
return this.errorResponse(404, 'Package not found');
|
|
482
481
|
}
|
|
483
482
|
|
|
483
|
+
// Find latest version for info
|
|
484
|
+
const versions = Object.keys(metadata.versions || {});
|
|
485
|
+
const latestVersion = versions.length > 0 ? versions[versions.length - 1] : null;
|
|
486
|
+
const latestMeta = latestVersion ? metadata.versions[latestVersion] : null;
|
|
487
|
+
|
|
488
|
+
// Build URLs array from latest version files
|
|
489
|
+
const urls = latestMeta?.files?.map((file: any) => ({
|
|
490
|
+
filename: file.filename,
|
|
491
|
+
url: `${this.registryUrl}/pypi/packages/${normalized}/${file.filename}`,
|
|
492
|
+
digests: file.hashes,
|
|
493
|
+
requires_python: file['requires-python'],
|
|
494
|
+
size: file.size,
|
|
495
|
+
upload_time: file['upload-time'],
|
|
496
|
+
packagetype: file.filetype,
|
|
497
|
+
python_version: file.python_version,
|
|
498
|
+
})) || [];
|
|
499
|
+
|
|
500
|
+
// Build releases object
|
|
501
|
+
const releases: Record<string, any[]> = {};
|
|
502
|
+
for (const [ver, verMeta] of Object.entries(metadata.versions || {})) {
|
|
503
|
+
releases[ver] = (verMeta as any).files?.map((file: any) => ({
|
|
504
|
+
filename: file.filename,
|
|
505
|
+
url: `${this.registryUrl}/pypi/packages/${normalized}/${file.filename}`,
|
|
506
|
+
digests: file.hashes,
|
|
507
|
+
requires_python: file['requires-python'],
|
|
508
|
+
size: file.size,
|
|
509
|
+
upload_time: file['upload-time'],
|
|
510
|
+
packagetype: file.filetype,
|
|
511
|
+
python_version: file.python_version,
|
|
512
|
+
})) || [];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const response = {
|
|
516
|
+
info: {
|
|
517
|
+
name: normalized,
|
|
518
|
+
version: latestVersion,
|
|
519
|
+
summary: latestMeta?.metadata?.summary,
|
|
520
|
+
description: latestMeta?.metadata?.description,
|
|
521
|
+
author: latestMeta?.metadata?.author,
|
|
522
|
+
author_email: latestMeta?.metadata?.['author-email'],
|
|
523
|
+
license: latestMeta?.metadata?.license,
|
|
524
|
+
requires_python: latestMeta?.files?.[0]?.['requires-python'],
|
|
525
|
+
...latestMeta?.metadata,
|
|
526
|
+
},
|
|
527
|
+
urls,
|
|
528
|
+
releases,
|
|
529
|
+
};
|
|
530
|
+
|
|
484
531
|
return {
|
|
485
532
|
status: 200,
|
|
486
533
|
headers: {
|
|
487
534
|
'Content-Type': 'application/json',
|
|
488
535
|
'Cache-Control': 'public, max-age=300'
|
|
489
536
|
},
|
|
490
|
-
body:
|
|
537
|
+
body: response,
|
|
491
538
|
};
|
|
492
539
|
}
|
|
493
540
|
|
|
494
541
|
/**
|
|
495
542
|
* Handle version-specific JSON API
|
|
543
|
+
* Returns format compatible with official PyPI JSON API
|
|
496
544
|
*/
|
|
497
545
|
private async handleVersionJson(packageName: string, version: string): Promise<IResponse> {
|
|
498
546
|
const normalized = helpers.normalizePypiPackageName(packageName);
|
|
@@ -502,13 +550,42 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
502
550
|
return this.errorResponse(404, 'Version not found');
|
|
503
551
|
}
|
|
504
552
|
|
|
553
|
+
const verMeta = metadata.versions[version];
|
|
554
|
+
|
|
555
|
+
// Build URLs array from version files
|
|
556
|
+
const urls = verMeta.files?.map((file: any) => ({
|
|
557
|
+
filename: file.filename,
|
|
558
|
+
url: `${this.registryUrl}/pypi/packages/${normalized}/${file.filename}`,
|
|
559
|
+
digests: file.hashes,
|
|
560
|
+
requires_python: file['requires-python'],
|
|
561
|
+
size: file.size,
|
|
562
|
+
upload_time: file['upload-time'],
|
|
563
|
+
packagetype: file.filetype,
|
|
564
|
+
python_version: file.python_version,
|
|
565
|
+
})) || [];
|
|
566
|
+
|
|
567
|
+
const response = {
|
|
568
|
+
info: {
|
|
569
|
+
name: normalized,
|
|
570
|
+
version,
|
|
571
|
+
summary: verMeta.metadata?.summary,
|
|
572
|
+
description: verMeta.metadata?.description,
|
|
573
|
+
author: verMeta.metadata?.author,
|
|
574
|
+
author_email: verMeta.metadata?.['author-email'],
|
|
575
|
+
license: verMeta.metadata?.license,
|
|
576
|
+
requires_python: verMeta.files?.[0]?.['requires-python'],
|
|
577
|
+
...verMeta.metadata,
|
|
578
|
+
},
|
|
579
|
+
urls,
|
|
580
|
+
};
|
|
581
|
+
|
|
505
582
|
return {
|
|
506
583
|
status: 200,
|
|
507
584
|
headers: {
|
|
508
585
|
'Content-Type': 'application/json',
|
|
509
586
|
'Cache-Control': 'public, max-age=300'
|
|
510
587
|
},
|
|
511
|
-
body:
|
|
588
|
+
body: response,
|
|
512
589
|
};
|
|
513
590
|
}
|
|
514
591
|
|
|
@@ -570,11 +647,11 @@ export class PypiRegistry extends BaseRegistry {
|
|
|
570
647
|
* Helper: Create error response
|
|
571
648
|
*/
|
|
572
649
|
private errorResponse(status: number, message: string): IResponse {
|
|
573
|
-
const error: IPypiError = { message, status };
|
|
650
|
+
const error: IPypiError = { error: message, status };
|
|
574
651
|
return {
|
|
575
652
|
status,
|
|
576
653
|
headers: { 'Content-Type': 'application/json' },
|
|
577
|
-
body:
|
|
654
|
+
body: error,
|
|
578
655
|
};
|
|
579
656
|
}
|
|
580
657
|
}
|