@push.rocks/smartregistry 1.4.1 → 1.6.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 +3 -3
- package/dist_ts/classes.smartregistry.d.ts +2 -2
- package/dist_ts/classes.smartregistry.js +37 -2
- package/dist_ts/core/classes.authmanager.d.ts +55 -1
- package/dist_ts/core/classes.authmanager.js +138 -3
- package/dist_ts/core/classes.registrystorage.d.ts +145 -0
- package/dist_ts/core/classes.registrystorage.js +392 -1
- package/dist_ts/core/interfaces.core.d.ts +13 -1
- package/dist_ts/index.d.ts +3 -1
- package/dist_ts/index.js +6 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +70 -0
- package/dist_ts/pypi/classes.pypiregistry.js +482 -0
- package/dist_ts/pypi/helpers.pypi.d.ts +84 -0
- package/dist_ts/pypi/helpers.pypi.js +263 -0
- package/dist_ts/pypi/index.d.ts +7 -0
- package/dist_ts/pypi/index.js +8 -0
- package/dist_ts/pypi/interfaces.pypi.d.ts +301 -0
- package/dist_ts/pypi/interfaces.pypi.js +6 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +86 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.js +475 -0
- package/dist_ts/rubygems/helpers.rubygems.d.ts +143 -0
- package/dist_ts/rubygems/helpers.rubygems.js +312 -0
- package/dist_ts/rubygems/index.d.ts +7 -0
- package/dist_ts/rubygems/index.js +8 -0
- package/dist_ts/rubygems/interfaces.rubygems.d.ts +236 -0
- package/dist_ts/rubygems/interfaces.rubygems.js +6 -0
- package/package.json +2 -2
- package/readme.hints.md +438 -2
- package/readme.md +288 -13
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.smartregistry.ts +41 -2
- package/ts/core/classes.authmanager.ts +161 -2
- package/ts/core/classes.registrystorage.ts +463 -0
- package/ts/core/interfaces.core.ts +13 -1
- package/ts/index.ts +7 -1
- package/ts/pypi/classes.pypiregistry.ts +580 -0
- package/ts/pypi/helpers.pypi.ts +299 -0
- package/ts/pypi/index.ts +8 -0
- package/ts/pypi/interfaces.pypi.ts +316 -0
- package/ts/rubygems/classes.rubygemsregistry.ts +598 -0
- package/ts/rubygems/helpers.rubygems.ts +398 -0
- package/ts/rubygems/index.ts +8 -0
- package/ts/rubygems/interfaces.rubygems.ts +251 -0
|
@@ -0,0 +1,598 @@
|
|
|
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 type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
|
|
6
|
+
import type {
|
|
7
|
+
IRubyGemsMetadata,
|
|
8
|
+
IRubyGemsVersionMetadata,
|
|
9
|
+
IRubyGemsUploadResponse,
|
|
10
|
+
IRubyGemsYankResponse,
|
|
11
|
+
IRubyGemsError,
|
|
12
|
+
ICompactIndexInfoEntry,
|
|
13
|
+
} from './interfaces.rubygems.js';
|
|
14
|
+
import * as helpers from './helpers.rubygems.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* RubyGems registry implementation
|
|
18
|
+
* Implements Compact Index API and RubyGems protocol
|
|
19
|
+
*/
|
|
20
|
+
export class RubyGemsRegistry extends BaseRegistry {
|
|
21
|
+
private storage: RegistryStorage;
|
|
22
|
+
private authManager: AuthManager;
|
|
23
|
+
private basePath: string = '/rubygems';
|
|
24
|
+
private registryUrl: string;
|
|
25
|
+
private logger: Smartlog;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
storage: RegistryStorage,
|
|
29
|
+
authManager: AuthManager,
|
|
30
|
+
basePath: string = '/rubygems',
|
|
31
|
+
registryUrl: string = 'http://localhost:5000/rubygems'
|
|
32
|
+
) {
|
|
33
|
+
super();
|
|
34
|
+
this.storage = storage;
|
|
35
|
+
this.authManager = authManager;
|
|
36
|
+
this.basePath = basePath;
|
|
37
|
+
this.registryUrl = registryUrl;
|
|
38
|
+
|
|
39
|
+
// Initialize logger
|
|
40
|
+
this.logger = new Smartlog({
|
|
41
|
+
logContext: {
|
|
42
|
+
company: 'push.rocks',
|
|
43
|
+
companyunit: 'smartregistry',
|
|
44
|
+
containerName: 'rubygems-registry',
|
|
45
|
+
environment: (process.env.NODE_ENV as any) || 'development',
|
|
46
|
+
runtime: 'node',
|
|
47
|
+
zone: 'rubygems'
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.logger.enableConsole();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async init(): Promise<void> {
|
|
54
|
+
// Initialize Compact Index files if not exist
|
|
55
|
+
const existingVersions = await this.storage.getRubyGemsVersions();
|
|
56
|
+
if (!existingVersions) {
|
|
57
|
+
const versions = helpers.generateCompactIndexVersions([]);
|
|
58
|
+
await this.storage.putRubyGemsVersions(versions);
|
|
59
|
+
this.logger.log('info', 'Initialized RubyGems Compact Index');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const existingNames = await this.storage.getRubyGemsNames();
|
|
63
|
+
if (!existingNames) {
|
|
64
|
+
const names = helpers.generateNamesFile([]);
|
|
65
|
+
await this.storage.putRubyGemsNames(names);
|
|
66
|
+
this.logger.log('info', 'Initialized RubyGems names file');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getBasePath(): string {
|
|
71
|
+
return this.basePath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
|
75
|
+
let path = context.path.replace(this.basePath, '');
|
|
76
|
+
|
|
77
|
+
// Extract token (Authorization header)
|
|
78
|
+
const token = await this.extractToken(context);
|
|
79
|
+
|
|
80
|
+
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
|
81
|
+
method: context.method,
|
|
82
|
+
path,
|
|
83
|
+
hasAuth: !!token
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Compact Index endpoints
|
|
87
|
+
if (path === '/versions' && context.method === 'GET') {
|
|
88
|
+
return this.handleVersionsFile();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (path === '/names' && context.method === 'GET') {
|
|
92
|
+
return this.handleNamesFile();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Info file: GET /info/{gem}
|
|
96
|
+
const infoMatch = path.match(/^\/info\/([^\/]+)$/);
|
|
97
|
+
if (infoMatch && context.method === 'GET') {
|
|
98
|
+
return this.handleInfoFile(infoMatch[1]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Gem download: GET /gems/{gem}-{version}[-{platform}].gem
|
|
102
|
+
const downloadMatch = path.match(/^\/gems\/(.+\.gem)$/);
|
|
103
|
+
if (downloadMatch && context.method === 'GET') {
|
|
104
|
+
return this.handleDownload(downloadMatch[1]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// API v1 endpoints
|
|
108
|
+
if (path.startsWith('/api/v1/')) {
|
|
109
|
+
return this.handleApiRequest(path.substring(8), context, token);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
status: 404,
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: Buffer.from(JSON.stringify({ message: 'Not Found' })),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if token has permission for resource
|
|
121
|
+
*/
|
|
122
|
+
protected async checkPermission(
|
|
123
|
+
token: IAuthToken | null,
|
|
124
|
+
resource: string,
|
|
125
|
+
action: string
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
if (!token) return false;
|
|
128
|
+
return this.authManager.authorize(token, `rubygems:gem:${resource}`, action);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract authentication token from request
|
|
133
|
+
*/
|
|
134
|
+
private async extractToken(context: IRequestContext): Promise<IAuthToken | null> {
|
|
135
|
+
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
|
|
136
|
+
if (!authHeader) return null;
|
|
137
|
+
|
|
138
|
+
// RubyGems typically uses plain API key in Authorization header
|
|
139
|
+
return this.authManager.validateToken(authHeader, 'rubygems');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handle /versions endpoint (Compact Index)
|
|
144
|
+
*/
|
|
145
|
+
private async handleVersionsFile(): Promise<IResponse> {
|
|
146
|
+
const content = await this.storage.getRubyGemsVersions();
|
|
147
|
+
|
|
148
|
+
if (!content) {
|
|
149
|
+
return this.errorResponse(500, 'Versions file not initialized');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
156
|
+
'Cache-Control': 'public, max-age=60',
|
|
157
|
+
'ETag': `"${await helpers.calculateMD5(content)}"`
|
|
158
|
+
},
|
|
159
|
+
body: Buffer.from(content),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle /names endpoint (Compact Index)
|
|
165
|
+
*/
|
|
166
|
+
private async handleNamesFile(): Promise<IResponse> {
|
|
167
|
+
const content = await this.storage.getRubyGemsNames();
|
|
168
|
+
|
|
169
|
+
if (!content) {
|
|
170
|
+
return this.errorResponse(500, 'Names file not initialized');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
status: 200,
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
177
|
+
'Cache-Control': 'public, max-age=300'
|
|
178
|
+
},
|
|
179
|
+
body: Buffer.from(content),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle /info/{gem} endpoint (Compact Index)
|
|
185
|
+
*/
|
|
186
|
+
private async handleInfoFile(gemName: string): Promise<IResponse> {
|
|
187
|
+
const content = await this.storage.getRubyGemsInfo(gemName);
|
|
188
|
+
|
|
189
|
+
if (!content) {
|
|
190
|
+
return {
|
|
191
|
+
status: 404,
|
|
192
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
193
|
+
body: Buffer.from('Not Found'),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
status: 200,
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
201
|
+
'Cache-Control': 'public, max-age=300',
|
|
202
|
+
'ETag': `"${await helpers.calculateMD5(content)}"`
|
|
203
|
+
},
|
|
204
|
+
body: Buffer.from(content),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle gem file download
|
|
210
|
+
*/
|
|
211
|
+
private async handleDownload(filename: string): Promise<IResponse> {
|
|
212
|
+
const parsed = helpers.parseGemFilename(filename);
|
|
213
|
+
if (!parsed) {
|
|
214
|
+
return this.errorResponse(400, 'Invalid gem filename');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const gemData = await this.storage.getRubyGemsGem(
|
|
218
|
+
parsed.name,
|
|
219
|
+
parsed.version,
|
|
220
|
+
parsed.platform
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!gemData) {
|
|
224
|
+
return this.errorResponse(404, 'Gem not found');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
status: 200,
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/octet-stream',
|
|
231
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
232
|
+
'Content-Length': gemData.length.toString()
|
|
233
|
+
},
|
|
234
|
+
body: gemData,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handle API v1 requests
|
|
240
|
+
*/
|
|
241
|
+
private async handleApiRequest(
|
|
242
|
+
path: string,
|
|
243
|
+
context: IRequestContext,
|
|
244
|
+
token: IAuthToken | null
|
|
245
|
+
): Promise<IResponse> {
|
|
246
|
+
// Upload gem: POST /gems
|
|
247
|
+
if (path === '/gems' && context.method === 'POST') {
|
|
248
|
+
return this.handleUpload(context, token);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Yank gem: DELETE /gems/yank
|
|
252
|
+
if (path === '/gems/yank' && context.method === 'DELETE') {
|
|
253
|
+
return this.handleYank(context, token);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Unyank gem: PUT /gems/unyank
|
|
257
|
+
if (path === '/gems/unyank' && context.method === 'PUT') {
|
|
258
|
+
return this.handleUnyank(context, token);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Version list: GET /versions/{gem}.json
|
|
262
|
+
const versionsMatch = path.match(/^\/versions\/([^\/]+)\.json$/);
|
|
263
|
+
if (versionsMatch && context.method === 'GET') {
|
|
264
|
+
return this.handleVersionsJson(versionsMatch[1]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Dependencies: GET /dependencies?gems={list}
|
|
268
|
+
if (path.startsWith('/dependencies') && context.method === 'GET') {
|
|
269
|
+
const gemsParam = context.query?.gems || '';
|
|
270
|
+
return this.handleDependencies(gemsParam);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return this.errorResponse(404, 'API endpoint not found');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handle gem upload
|
|
278
|
+
* POST /api/v1/gems
|
|
279
|
+
*/
|
|
280
|
+
private async handleUpload(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
|
|
281
|
+
if (!token) {
|
|
282
|
+
return this.errorResponse(401, 'Authentication required');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Extract gem data from request body
|
|
287
|
+
const gemData = context.body as Buffer;
|
|
288
|
+
if (!gemData || gemData.length === 0) {
|
|
289
|
+
return this.errorResponse(400, 'No gem file provided');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// For now, we expect metadata in query params or headers
|
|
293
|
+
// Full implementation would parse .gem file (tar + gzip + Marshal)
|
|
294
|
+
const gemName = context.query?.name || context.headers['x-gem-name'];
|
|
295
|
+
const version = context.query?.version || context.headers['x-gem-version'];
|
|
296
|
+
const platform = context.query?.platform || context.headers['x-gem-platform'];
|
|
297
|
+
|
|
298
|
+
if (!gemName || !version) {
|
|
299
|
+
return this.errorResponse(400, 'Gem name and version required');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate gem name
|
|
303
|
+
if (!helpers.isValidGemName(gemName)) {
|
|
304
|
+
return this.errorResponse(400, 'Invalid gem name');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check permission
|
|
308
|
+
if (!(await this.checkPermission(token, gemName, 'write'))) {
|
|
309
|
+
return this.errorResponse(403, 'Insufficient permissions');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Calculate checksum
|
|
313
|
+
const checksum = await helpers.calculateSHA256(gemData);
|
|
314
|
+
|
|
315
|
+
// Store gem file
|
|
316
|
+
await this.storage.putRubyGemsGem(gemName, version, gemData, platform);
|
|
317
|
+
|
|
318
|
+
// Update metadata
|
|
319
|
+
let metadata: IRubyGemsMetadata = await this.storage.getRubyGemsMetadata(gemName) || {
|
|
320
|
+
name: gemName,
|
|
321
|
+
versions: {},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const versionKey = platform ? `${version}-${platform}` : version;
|
|
325
|
+
metadata.versions[versionKey] = {
|
|
326
|
+
version,
|
|
327
|
+
platform,
|
|
328
|
+
checksum,
|
|
329
|
+
size: gemData.length,
|
|
330
|
+
'upload-time': new Date().toISOString(),
|
|
331
|
+
'uploaded-by': token.userId,
|
|
332
|
+
dependencies: [], // Would extract from gem spec
|
|
333
|
+
requirements: [],
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
metadata['last-modified'] = new Date().toISOString();
|
|
337
|
+
await this.storage.putRubyGemsMetadata(gemName, metadata);
|
|
338
|
+
|
|
339
|
+
// Update Compact Index info file
|
|
340
|
+
await this.updateCompactIndexForGem(gemName, metadata);
|
|
341
|
+
|
|
342
|
+
// Update versions file
|
|
343
|
+
await this.updateVersionsFile(gemName, version, platform || 'ruby', false);
|
|
344
|
+
|
|
345
|
+
// Update names file
|
|
346
|
+
await this.updateNamesFile(gemName);
|
|
347
|
+
|
|
348
|
+
this.logger.log('info', `Gem uploaded: ${gemName} ${version}`, {
|
|
349
|
+
platform,
|
|
350
|
+
size: gemData.length
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
status: 200,
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: Buffer.from(JSON.stringify({
|
|
357
|
+
message: 'Gem uploaded successfully',
|
|
358
|
+
name: gemName,
|
|
359
|
+
version,
|
|
360
|
+
})),
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this.logger.log('error', 'Upload failed', { error: (error as Error).message });
|
|
364
|
+
return this.errorResponse(500, 'Upload failed: ' + (error as Error).message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Handle gem yanking
|
|
370
|
+
* DELETE /api/v1/gems/yank
|
|
371
|
+
*/
|
|
372
|
+
private async handleYank(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
|
|
373
|
+
if (!token) {
|
|
374
|
+
return this.errorResponse(401, 'Authentication required');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const gemName = context.query?.gem_name;
|
|
378
|
+
const version = context.query?.version;
|
|
379
|
+
const platform = context.query?.platform;
|
|
380
|
+
|
|
381
|
+
if (!gemName || !version) {
|
|
382
|
+
return this.errorResponse(400, 'Gem name and version required');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!(await this.checkPermission(token, gemName, 'yank'))) {
|
|
386
|
+
return this.errorResponse(403, 'Insufficient permissions');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Update metadata to mark as yanked
|
|
390
|
+
const metadata = await this.storage.getRubyGemsMetadata(gemName);
|
|
391
|
+
if (!metadata) {
|
|
392
|
+
return this.errorResponse(404, 'Gem not found');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const versionKey = platform ? `${version}-${platform}` : version;
|
|
396
|
+
if (!metadata.versions[versionKey]) {
|
|
397
|
+
return this.errorResponse(404, 'Version not found');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
metadata.versions[versionKey].yanked = true;
|
|
401
|
+
await this.storage.putRubyGemsMetadata(gemName, metadata);
|
|
402
|
+
|
|
403
|
+
// Update Compact Index
|
|
404
|
+
await this.updateCompactIndexForGem(gemName, metadata);
|
|
405
|
+
await this.updateVersionsFile(gemName, version, platform || 'ruby', true);
|
|
406
|
+
|
|
407
|
+
this.logger.log('info', `Gem yanked: ${gemName} ${version}`);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
status: 200,
|
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
412
|
+
body: Buffer.from(JSON.stringify({
|
|
413
|
+
success: true,
|
|
414
|
+
message: 'Gem yanked successfully'
|
|
415
|
+
})),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Handle gem unyanking
|
|
421
|
+
* PUT /api/v1/gems/unyank
|
|
422
|
+
*/
|
|
423
|
+
private async handleUnyank(context: IRequestContext, token: IAuthToken | null): Promise<IResponse> {
|
|
424
|
+
if (!token) {
|
|
425
|
+
return this.errorResponse(401, 'Authentication required');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const gemName = context.query?.gem_name;
|
|
429
|
+
const version = context.query?.version;
|
|
430
|
+
const platform = context.query?.platform;
|
|
431
|
+
|
|
432
|
+
if (!gemName || !version) {
|
|
433
|
+
return this.errorResponse(400, 'Gem name and version required');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!(await this.checkPermission(token, gemName, 'write'))) {
|
|
437
|
+
return this.errorResponse(403, 'Insufficient permissions');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const metadata = await this.storage.getRubyGemsMetadata(gemName);
|
|
441
|
+
if (!metadata) {
|
|
442
|
+
return this.errorResponse(404, 'Gem not found');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const versionKey = platform ? `${version}-${platform}` : version;
|
|
446
|
+
if (!metadata.versions[versionKey]) {
|
|
447
|
+
return this.errorResponse(404, 'Version not found');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
metadata.versions[versionKey].yanked = false;
|
|
451
|
+
await this.storage.putRubyGemsMetadata(gemName, metadata);
|
|
452
|
+
|
|
453
|
+
// Update Compact Index
|
|
454
|
+
await this.updateCompactIndexForGem(gemName, metadata);
|
|
455
|
+
await this.updateVersionsFile(gemName, version, platform || 'ruby', false);
|
|
456
|
+
|
|
457
|
+
this.logger.log('info', `Gem unyanked: ${gemName} ${version}`);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
status: 200,
|
|
461
|
+
headers: { 'Content-Type': 'application/json' },
|
|
462
|
+
body: Buffer.from(JSON.stringify({
|
|
463
|
+
success: true,
|
|
464
|
+
message: 'Gem unyanked successfully'
|
|
465
|
+
})),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Handle versions JSON API
|
|
471
|
+
*/
|
|
472
|
+
private async handleVersionsJson(gemName: string): Promise<IResponse> {
|
|
473
|
+
const metadata = await this.storage.getRubyGemsMetadata(gemName);
|
|
474
|
+
if (!metadata) {
|
|
475
|
+
return this.errorResponse(404, 'Gem not found');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const versions = Object.values(metadata.versions).map((v: any) => ({
|
|
479
|
+
version: v.version,
|
|
480
|
+
platform: v.platform,
|
|
481
|
+
uploadTime: v['upload-time'],
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
const response = helpers.generateVersionsJson(gemName, versions);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
status: 200,
|
|
488
|
+
headers: {
|
|
489
|
+
'Content-Type': 'application/json',
|
|
490
|
+
'Cache-Control': 'public, max-age=300'
|
|
491
|
+
},
|
|
492
|
+
body: Buffer.from(JSON.stringify(response)),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Handle dependencies query
|
|
498
|
+
*/
|
|
499
|
+
private async handleDependencies(gemsParam: string): Promise<IResponse> {
|
|
500
|
+
const gemNames = gemsParam.split(',').filter(n => n.trim());
|
|
501
|
+
const result = new Map();
|
|
502
|
+
|
|
503
|
+
for (const gemName of gemNames) {
|
|
504
|
+
const metadata = await this.storage.getRubyGemsMetadata(gemName);
|
|
505
|
+
if (metadata) {
|
|
506
|
+
const versions = Object.values(metadata.versions).map((v: any) => ({
|
|
507
|
+
version: v.version,
|
|
508
|
+
platform: v.platform,
|
|
509
|
+
dependencies: v.dependencies || [],
|
|
510
|
+
}));
|
|
511
|
+
result.set(gemName, versions);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const response = helpers.generateDependenciesJson(result);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
status: 200,
|
|
519
|
+
headers: { 'Content-Type': 'application/json' },
|
|
520
|
+
body: Buffer.from(JSON.stringify(response)),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Update Compact Index info file for a gem
|
|
526
|
+
*/
|
|
527
|
+
private async updateCompactIndexForGem(
|
|
528
|
+
gemName: string,
|
|
529
|
+
metadata: IRubyGemsMetadata
|
|
530
|
+
): Promise<void> {
|
|
531
|
+
const entries: ICompactIndexInfoEntry[] = Object.values(metadata.versions)
|
|
532
|
+
.filter(v => !v.yanked) // Exclude yanked from info file
|
|
533
|
+
.map(v => ({
|
|
534
|
+
version: v.version,
|
|
535
|
+
platform: v.platform,
|
|
536
|
+
dependencies: v.dependencies || [],
|
|
537
|
+
requirements: v.requirements || [],
|
|
538
|
+
checksum: v.checksum,
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
const content = helpers.generateCompactIndexInfo(entries);
|
|
542
|
+
await this.storage.putRubyGemsInfo(gemName, content);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Update versions file with new/updated gem
|
|
547
|
+
*/
|
|
548
|
+
private async updateVersionsFile(
|
|
549
|
+
gemName: string,
|
|
550
|
+
version: string,
|
|
551
|
+
platform: string,
|
|
552
|
+
yanked: boolean
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
const existingVersions = await this.storage.getRubyGemsVersions();
|
|
555
|
+
if (!existingVersions) return;
|
|
556
|
+
|
|
557
|
+
// Calculate info file checksum
|
|
558
|
+
const infoContent = await this.storage.getRubyGemsInfo(gemName) || '';
|
|
559
|
+
const infoChecksum = await helpers.calculateMD5(infoContent);
|
|
560
|
+
|
|
561
|
+
const updated = helpers.updateCompactIndexVersions(
|
|
562
|
+
existingVersions,
|
|
563
|
+
gemName,
|
|
564
|
+
{ version, platform: platform !== 'ruby' ? platform : undefined, yanked },
|
|
565
|
+
infoChecksum
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
await this.storage.putRubyGemsVersions(updated);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Update names file with new gem
|
|
573
|
+
*/
|
|
574
|
+
private async updateNamesFile(gemName: string): Promise<void> {
|
|
575
|
+
const existingNames = await this.storage.getRubyGemsNames();
|
|
576
|
+
if (!existingNames) return;
|
|
577
|
+
|
|
578
|
+
const lines = existingNames.split('\n').filter(l => l !== '---');
|
|
579
|
+
if (!lines.includes(gemName)) {
|
|
580
|
+
lines.push(gemName);
|
|
581
|
+
lines.sort();
|
|
582
|
+
const updated = helpers.generateNamesFile(lines);
|
|
583
|
+
await this.storage.putRubyGemsNames(updated);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Helper: Create error response
|
|
589
|
+
*/
|
|
590
|
+
private errorResponse(status: number, message: string): IResponse {
|
|
591
|
+
const error: IRubyGemsError = { message, status };
|
|
592
|
+
return {
|
|
593
|
+
status,
|
|
594
|
+
headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
body: Buffer.from(JSON.stringify(error)),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|