@push.rocks/smartregistry 1.8.0 → 2.1.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 +14 -10
- 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 +14 -1
- package/dist_ts/oci/classes.ociregistry.js +44 -57
- 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 +13 -9
- package/ts/core/classes.authmanager.ts +37 -6
- package/ts/oci/classes.ociregistry.ts +51 -57
- 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: '1.
|
|
6
|
+
version: '2.1.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,15 +41,19 @@ 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
|
|
45
|
-
const
|
|
44
|
+
const ociBasePath = this.config.oci.basePath ?? '/oci';
|
|
45
|
+
const ociTokens = this.config.auth.ociTokens?.enabled ? {
|
|
46
|
+
realm: this.config.auth.ociTokens.realm,
|
|
47
|
+
service: this.config.auth.ociTokens.service,
|
|
48
|
+
} : undefined;
|
|
49
|
+
const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath, ociTokens);
|
|
46
50
|
await ociRegistry.init();
|
|
47
51
|
this.registries.set('oci', ociRegistry);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// Initialize NPM registry if enabled
|
|
51
55
|
if (this.config.npm?.enabled) {
|
|
52
|
-
const npmBasePath = this.config.npm.basePath
|
|
56
|
+
const npmBasePath = this.config.npm.basePath ?? '/npm';
|
|
53
57
|
const registryUrl = `http://localhost:5000${npmBasePath}`; // TODO: Make configurable
|
|
54
58
|
const npmRegistry = new NpmRegistry(this.storage, this.authManager, npmBasePath, registryUrl);
|
|
55
59
|
await npmRegistry.init();
|
|
@@ -58,7 +62,7 @@ export class SmartRegistry {
|
|
|
58
62
|
|
|
59
63
|
// Initialize Maven registry if enabled
|
|
60
64
|
if (this.config.maven?.enabled) {
|
|
61
|
-
const mavenBasePath = this.config.maven.basePath
|
|
65
|
+
const mavenBasePath = this.config.maven.basePath ?? '/maven';
|
|
62
66
|
const registryUrl = `http://localhost:5000${mavenBasePath}`; // TODO: Make configurable
|
|
63
67
|
const mavenRegistry = new MavenRegistry(this.storage, this.authManager, mavenBasePath, registryUrl);
|
|
64
68
|
await mavenRegistry.init();
|
|
@@ -67,7 +71,7 @@ export class SmartRegistry {
|
|
|
67
71
|
|
|
68
72
|
// Initialize Cargo registry if enabled
|
|
69
73
|
if (this.config.cargo?.enabled) {
|
|
70
|
-
const cargoBasePath = this.config.cargo.basePath
|
|
74
|
+
const cargoBasePath = this.config.cargo.basePath ?? '/cargo';
|
|
71
75
|
const registryUrl = `http://localhost:5000${cargoBasePath}`; // TODO: Make configurable
|
|
72
76
|
const cargoRegistry = new CargoRegistry(this.storage, this.authManager, cargoBasePath, registryUrl);
|
|
73
77
|
await cargoRegistry.init();
|
|
@@ -76,7 +80,7 @@ export class SmartRegistry {
|
|
|
76
80
|
|
|
77
81
|
// Initialize Composer registry if enabled
|
|
78
82
|
if (this.config.composer?.enabled) {
|
|
79
|
-
const composerBasePath = this.config.composer.basePath
|
|
83
|
+
const composerBasePath = this.config.composer.basePath ?? '/composer';
|
|
80
84
|
const registryUrl = `http://localhost:5000${composerBasePath}`; // TODO: Make configurable
|
|
81
85
|
const composerRegistry = new ComposerRegistry(this.storage, this.authManager, composerBasePath, registryUrl);
|
|
82
86
|
await composerRegistry.init();
|
|
@@ -85,7 +89,7 @@ export class SmartRegistry {
|
|
|
85
89
|
|
|
86
90
|
// Initialize PyPI registry if enabled
|
|
87
91
|
if (this.config.pypi?.enabled) {
|
|
88
|
-
const pypiBasePath = this.config.pypi.basePath
|
|
92
|
+
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
89
93
|
const registryUrl = `http://localhost:5000`; // TODO: Make configurable
|
|
90
94
|
const pypiRegistry = new PypiRegistry(this.storage, this.authManager, pypiBasePath, registryUrl);
|
|
91
95
|
await pypiRegistry.init();
|
|
@@ -94,7 +98,7 @@ export class SmartRegistry {
|
|
|
94
98
|
|
|
95
99
|
// Initialize RubyGems registry if enabled
|
|
96
100
|
if (this.config.rubygems?.enabled) {
|
|
97
|
-
const rubygemsBasePath = this.config.rubygems.basePath
|
|
101
|
+
const rubygemsBasePath = this.config.rubygems.basePath ?? '/rubygems';
|
|
98
102
|
const registryUrl = `http://localhost:5000${rubygemsBasePath}`; // TODO: Make configurable
|
|
99
103
|
const rubygemsRegistry = new RubyGemsRegistry(this.storage, this.authManager, rubygemsBasePath, registryUrl);
|
|
100
104
|
await rubygemsRegistry.init();
|
|
@@ -153,7 +157,7 @@ export class SmartRegistry {
|
|
|
153
157
|
|
|
154
158
|
// Route to PyPI registry (also handles /simple prefix)
|
|
155
159
|
if (this.config.pypi?.enabled) {
|
|
156
|
-
const pypiBasePath = this.config.pypi.basePath
|
|
160
|
+
const pypiBasePath = this.config.pypi.basePath ?? '/pypi';
|
|
157
161
|
if (path.startsWith(pypiBasePath) || path.startsWith('/simple')) {
|
|
158
162
|
const pypiRegistry = this.registries.get('pypi');
|
|
159
163
|
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
|
|
|
@@ -20,12 +20,19 @@ export class OciRegistry extends BaseRegistry {
|
|
|
20
20
|
private uploadSessions: Map<string, IUploadSession> = new Map();
|
|
21
21
|
private basePath: string = '/oci';
|
|
22
22
|
private cleanupInterval?: NodeJS.Timeout;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
private ociTokens?: { realm: string; service: string };
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
storage: RegistryStorage,
|
|
27
|
+
authManager: AuthManager,
|
|
28
|
+
basePath: string = '/oci',
|
|
29
|
+
ociTokens?: { realm: string; service: string }
|
|
30
|
+
) {
|
|
25
31
|
super();
|
|
26
32
|
this.storage = storage;
|
|
27
33
|
this.authManager = authManager;
|
|
28
34
|
this.basePath = basePath;
|
|
35
|
+
this.ociTokens = ociTokens;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
public async init(): Promise<void> {
|
|
@@ -180,11 +187,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
180
187
|
body?: Buffer | any
|
|
181
188
|
): Promise<IResponse> {
|
|
182
189
|
if (!await this.checkPermission(token, repository, 'push')) {
|
|
183
|
-
return
|
|
184
|
-
status: 401,
|
|
185
|
-
headers: {},
|
|
186
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
187
|
-
};
|
|
190
|
+
return this.createUnauthorizedResponse(repository, 'push');
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
// Check for monolithic upload (digest + body provided)
|
|
@@ -255,11 +258,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
if (!await this.checkPermission(token, session.repository, 'push')) {
|
|
258
|
-
return
|
|
259
|
-
status: 401,
|
|
260
|
-
headers: {},
|
|
261
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
262
|
-
};
|
|
261
|
+
return this.createUnauthorizedResponse(session.repository, 'push');
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
switch (method) {
|
|
@@ -288,13 +287,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
288
287
|
headers?: Record<string, string>
|
|
289
288
|
): Promise<IResponse> {
|
|
290
289
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
291
|
-
return
|
|
292
|
-
status: 401,
|
|
293
|
-
headers: {
|
|
294
|
-
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:pull"`,
|
|
295
|
-
},
|
|
296
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
297
|
-
};
|
|
290
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
298
291
|
}
|
|
299
292
|
|
|
300
293
|
// Resolve tag to digest if needed
|
|
@@ -336,11 +329,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
336
329
|
token: IAuthToken | null
|
|
337
330
|
): Promise<IResponse> {
|
|
338
331
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
339
|
-
return
|
|
340
|
-
status: 401,
|
|
341
|
-
headers: {},
|
|
342
|
-
body: null,
|
|
343
|
-
};
|
|
332
|
+
return this.createUnauthorizedHeadResponse(repository, 'pull');
|
|
344
333
|
}
|
|
345
334
|
|
|
346
335
|
// Similar logic as getManifest but return headers only
|
|
@@ -379,13 +368,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
379
368
|
headers?: Record<string, string>
|
|
380
369
|
): Promise<IResponse> {
|
|
381
370
|
if (!await this.checkPermission(token, repository, 'push')) {
|
|
382
|
-
return
|
|
383
|
-
status: 401,
|
|
384
|
-
headers: {
|
|
385
|
-
'WWW-Authenticate': `Bearer realm="${this.basePath}/v2/token",service="registry",scope="repository:${repository}:push"`,
|
|
386
|
-
},
|
|
387
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
388
|
-
};
|
|
371
|
+
return this.createUnauthorizedResponse(repository, 'push');
|
|
389
372
|
}
|
|
390
373
|
|
|
391
374
|
if (!body) {
|
|
@@ -437,11 +420,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
437
420
|
}
|
|
438
421
|
|
|
439
422
|
if (!await this.checkPermission(token, repository, 'delete')) {
|
|
440
|
-
return
|
|
441
|
-
status: 401,
|
|
442
|
-
headers: {},
|
|
443
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
444
|
-
};
|
|
423
|
+
return this.createUnauthorizedResponse(repository, 'delete');
|
|
445
424
|
}
|
|
446
425
|
|
|
447
426
|
await this.storage.deleteOciManifest(repository, digest);
|
|
@@ -460,11 +439,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
460
439
|
range?: string
|
|
461
440
|
): Promise<IResponse> {
|
|
462
441
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
463
|
-
return
|
|
464
|
-
status: 401,
|
|
465
|
-
headers: {},
|
|
466
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
467
|
-
};
|
|
442
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
468
443
|
}
|
|
469
444
|
|
|
470
445
|
const data = await this.storage.getOciBlob(digest);
|
|
@@ -492,7 +467,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
492
467
|
token: IAuthToken | null
|
|
493
468
|
): Promise<IResponse> {
|
|
494
469
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
495
|
-
return
|
|
470
|
+
return this.createUnauthorizedHeadResponse(repository, 'pull');
|
|
496
471
|
}
|
|
497
472
|
|
|
498
473
|
const exists = await this.storage.ociBlobExists(digest);
|
|
@@ -518,11 +493,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
518
493
|
token: IAuthToken | null
|
|
519
494
|
): Promise<IResponse> {
|
|
520
495
|
if (!await this.checkPermission(token, repository, 'delete')) {
|
|
521
|
-
return
|
|
522
|
-
status: 401,
|
|
523
|
-
headers: {},
|
|
524
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
525
|
-
};
|
|
496
|
+
return this.createUnauthorizedResponse(repository, 'delete');
|
|
526
497
|
}
|
|
527
498
|
|
|
528
499
|
await this.storage.deleteOciBlob(digest);
|
|
@@ -631,11 +602,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
631
602
|
query: Record<string, string>
|
|
632
603
|
): Promise<IResponse> {
|
|
633
604
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
634
|
-
return
|
|
635
|
-
status: 401,
|
|
636
|
-
headers: {},
|
|
637
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
638
|
-
};
|
|
605
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
639
606
|
}
|
|
640
607
|
|
|
641
608
|
const tags = await this.getTagsData(repository);
|
|
@@ -660,11 +627,7 @@ export class OciRegistry extends BaseRegistry {
|
|
|
660
627
|
query: Record<string, string>
|
|
661
628
|
): Promise<IResponse> {
|
|
662
629
|
if (!await this.checkPermission(token, repository, 'pull')) {
|
|
663
|
-
return
|
|
664
|
-
status: 401,
|
|
665
|
-
headers: {},
|
|
666
|
-
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
667
|
-
};
|
|
630
|
+
return this.createUnauthorizedResponse(repository, 'pull');
|
|
668
631
|
}
|
|
669
632
|
|
|
670
633
|
const response: IReferrersResponse = {
|
|
@@ -712,6 +675,37 @@ export class OciRegistry extends BaseRegistry {
|
|
|
712
675
|
};
|
|
713
676
|
}
|
|
714
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Create an unauthorized response with proper WWW-Authenticate header.
|
|
680
|
+
* Per OCI Distribution Spec, 401 responses MUST include WWW-Authenticate header.
|
|
681
|
+
*/
|
|
682
|
+
private createUnauthorizedResponse(repository: string, action: string): IResponse {
|
|
683
|
+
const realm = this.ociTokens?.realm || `${this.basePath}/v2/token`;
|
|
684
|
+
const service = this.ociTokens?.service || 'registry';
|
|
685
|
+
return {
|
|
686
|
+
status: 401,
|
|
687
|
+
headers: {
|
|
688
|
+
'WWW-Authenticate': `Bearer realm="${realm}",service="${service}",scope="repository:${repository}:${action}"`,
|
|
689
|
+
},
|
|
690
|
+
body: this.createError('DENIED', 'Insufficient permissions'),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Create an unauthorized HEAD response (no body per HTTP spec).
|
|
696
|
+
*/
|
|
697
|
+
private createUnauthorizedHeadResponse(repository: string, action: string): IResponse {
|
|
698
|
+
const realm = this.ociTokens?.realm || `${this.basePath}/v2/token`;
|
|
699
|
+
const service = this.ociTokens?.service || 'registry';
|
|
700
|
+
return {
|
|
701
|
+
status: 401,
|
|
702
|
+
headers: {
|
|
703
|
+
'WWW-Authenticate': `Bearer realm="${realm}",service="${service}",scope="repository:${repository}:${action}"`,
|
|
704
|
+
},
|
|
705
|
+
body: null,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
715
709
|
private startUploadSessionCleanup(): void {
|
|
716
710
|
this.cleanupInterval = setInterval(() => {
|
|
717
711
|
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
|
}
|