@push.rocks/smartregistry 1.1.1 → 1.4.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/cargo/classes.cargoregistry.d.ts +79 -0
- package/dist_ts/cargo/classes.cargoregistry.js +490 -0
- package/dist_ts/cargo/index.d.ts +5 -0
- package/dist_ts/cargo/index.js +6 -0
- package/dist_ts/cargo/interfaces.cargo.d.ts +160 -0
- package/dist_ts/cargo/interfaces.cargo.js +6 -0
- package/dist_ts/classes.smartregistry.d.ts +2 -2
- package/dist_ts/classes.smartregistry.js +50 -2
- package/dist_ts/composer/classes.composerregistry.d.ts +26 -0
- package/dist_ts/composer/classes.composerregistry.js +366 -0
- package/dist_ts/composer/helpers.composer.d.ts +35 -0
- package/dist_ts/composer/helpers.composer.js +120 -0
- package/dist_ts/composer/index.d.ts +7 -0
- package/dist_ts/composer/index.js +8 -0
- package/dist_ts/composer/interfaces.composer.d.ts +102 -0
- package/dist_ts/composer/interfaces.composer.js +6 -0
- package/dist_ts/core/classes.authmanager.d.ts +46 -1
- package/dist_ts/core/classes.authmanager.js +121 -12
- package/dist_ts/core/classes.registrystorage.d.ts +103 -0
- package/dist_ts/core/classes.registrystorage.js +253 -1
- package/dist_ts/core/interfaces.core.d.ts +4 -1
- package/dist_ts/index.d.ts +4 -1
- package/dist_ts/index.js +8 -2
- package/dist_ts/maven/classes.mavenregistry.d.ts +35 -0
- package/dist_ts/maven/classes.mavenregistry.js +407 -0
- package/dist_ts/maven/helpers.maven.d.ts +68 -0
- package/dist_ts/maven/helpers.maven.js +286 -0
- package/dist_ts/maven/index.d.ts +6 -0
- package/dist_ts/maven/index.js +7 -0
- package/dist_ts/maven/interfaces.maven.d.ts +116 -0
- package/dist_ts/maven/interfaces.maven.js +6 -0
- package/package.json +3 -2
- package/readme.md +288 -14
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/cargo/classes.cargoregistry.ts +604 -0
- package/ts/cargo/index.ts +6 -0
- package/ts/cargo/interfaces.cargo.ts +169 -0
- package/ts/classes.smartregistry.ts +56 -2
- package/ts/composer/classes.composerregistry.ts +475 -0
- package/ts/composer/helpers.composer.ts +139 -0
- package/ts/composer/index.ts +8 -0
- package/ts/composer/interfaces.composer.ts +111 -0
- package/ts/core/classes.authmanager.ts +145 -12
- package/ts/core/classes.registrystorage.ts +334 -0
- package/ts/core/interfaces.core.ts +4 -1
- package/ts/index.ts +10 -1
- package/ts/maven/classes.mavenregistry.ts +580 -0
- package/ts/maven/helpers.maven.ts +346 -0
- package/ts/maven/index.ts +7 -0
- package/ts/maven/interfaces.maven.ts +127 -0
|
@@ -0,0 +1,604 @@
|
|
|
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
|
+
ICargoIndexEntry,
|
|
8
|
+
ICargoPublishMetadata,
|
|
9
|
+
ICargoConfig,
|
|
10
|
+
ICargoError,
|
|
11
|
+
ICargoPublishResponse,
|
|
12
|
+
ICargoYankResponse,
|
|
13
|
+
ICargoSearchResponse,
|
|
14
|
+
ICargoSearchResult,
|
|
15
|
+
} from './interfaces.cargo.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cargo/crates.io registry implementation
|
|
19
|
+
* Implements the sparse HTTP-based protocol
|
|
20
|
+
* Spec: https://doc.rust-lang.org/cargo/reference/registry-index.html
|
|
21
|
+
*/
|
|
22
|
+
export class CargoRegistry extends BaseRegistry {
|
|
23
|
+
private storage: RegistryStorage;
|
|
24
|
+
private authManager: AuthManager;
|
|
25
|
+
private basePath: string = '/cargo';
|
|
26
|
+
private registryUrl: string;
|
|
27
|
+
private logger: Smartlog;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
storage: RegistryStorage,
|
|
31
|
+
authManager: AuthManager,
|
|
32
|
+
basePath: string = '/cargo',
|
|
33
|
+
registryUrl: string = 'http://localhost:5000/cargo'
|
|
34
|
+
) {
|
|
35
|
+
super();
|
|
36
|
+
this.storage = storage;
|
|
37
|
+
this.authManager = authManager;
|
|
38
|
+
this.basePath = basePath;
|
|
39
|
+
this.registryUrl = registryUrl;
|
|
40
|
+
|
|
41
|
+
// Initialize logger
|
|
42
|
+
this.logger = new Smartlog({
|
|
43
|
+
logContext: {
|
|
44
|
+
company: 'push.rocks',
|
|
45
|
+
companyunit: 'smartregistry',
|
|
46
|
+
containerName: 'cargo-registry',
|
|
47
|
+
environment: (process.env.NODE_ENV as any) || 'development',
|
|
48
|
+
runtime: 'node',
|
|
49
|
+
zone: 'cargo'
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.logger.enableConsole();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async init(): Promise<void> {
|
|
56
|
+
// Initialize config.json if not exists
|
|
57
|
+
const existingConfig = await this.storage.getCargoConfig();
|
|
58
|
+
if (!existingConfig) {
|
|
59
|
+
const config: ICargoConfig = {
|
|
60
|
+
dl: `${this.registryUrl}/api/v1/crates/{crate}/{version}/download`,
|
|
61
|
+
api: this.registryUrl,
|
|
62
|
+
};
|
|
63
|
+
await this.storage.putCargoConfig(config);
|
|
64
|
+
this.logger.log('info', 'Initialized Cargo registry config', { config });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public getBasePath(): string {
|
|
69
|
+
return this.basePath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async handleRequest(context: IRequestContext): Promise<IResponse> {
|
|
73
|
+
const path = context.path.replace(this.basePath, '');
|
|
74
|
+
|
|
75
|
+
// Extract token (Cargo uses Authorization header WITHOUT "Bearer" prefix)
|
|
76
|
+
const authHeader = context.headers['authorization'] || context.headers['Authorization'];
|
|
77
|
+
const token = authHeader ? await this.authManager.validateToken(authHeader, 'cargo') : null;
|
|
78
|
+
|
|
79
|
+
this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
|
|
80
|
+
method: context.method,
|
|
81
|
+
path,
|
|
82
|
+
hasAuth: !!token
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Config endpoint (required for sparse protocol)
|
|
86
|
+
if (path === '/config.json') {
|
|
87
|
+
return this.handleConfigJson();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// API endpoints
|
|
91
|
+
if (path.startsWith('/api/v1/')) {
|
|
92
|
+
return this.handleApiRequest(path, context, token);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Index files (sparse protocol)
|
|
96
|
+
return this.handleIndexRequest(path);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if token has permission for resource
|
|
101
|
+
*/
|
|
102
|
+
protected async checkPermission(
|
|
103
|
+
token: IAuthToken | null,
|
|
104
|
+
resource: string,
|
|
105
|
+
action: string
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
if (!token) return false;
|
|
108
|
+
return this.authManager.authorize(token, `cargo:crate:${resource}`, action);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handle API requests (/api/v1/*)
|
|
113
|
+
*/
|
|
114
|
+
private async handleApiRequest(
|
|
115
|
+
path: string,
|
|
116
|
+
context: IRequestContext,
|
|
117
|
+
token: IAuthToken | null
|
|
118
|
+
): Promise<IResponse> {
|
|
119
|
+
// Publish: PUT /api/v1/crates/new
|
|
120
|
+
if (path === '/api/v1/crates/new' && context.method === 'PUT') {
|
|
121
|
+
return this.handlePublish(context.body as Buffer, token);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Download: GET /api/v1/crates/{crate}/{version}/download
|
|
125
|
+
const downloadMatch = path.match(/^\/api\/v1\/crates\/([^\/]+)\/([^\/]+)\/download$/);
|
|
126
|
+
if (downloadMatch && context.method === 'GET') {
|
|
127
|
+
return this.handleDownload(downloadMatch[1], downloadMatch[2]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Yank: DELETE /api/v1/crates/{crate}/{version}/yank
|
|
131
|
+
const yankMatch = path.match(/^\/api\/v1\/crates\/([^\/]+)\/([^\/]+)\/yank$/);
|
|
132
|
+
if (yankMatch && context.method === 'DELETE') {
|
|
133
|
+
return this.handleYank(yankMatch[1], yankMatch[2], token);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Unyank: PUT /api/v1/crates/{crate}/{version}/unyank
|
|
137
|
+
const unyankMatch = path.match(/^\/api\/v1\/crates\/([^\/]+)\/([^\/]+)\/unyank$/);
|
|
138
|
+
if (unyankMatch && context.method === 'PUT') {
|
|
139
|
+
return this.handleUnyank(unyankMatch[1], unyankMatch[2], token);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Search: GET /api/v1/crates?q={query}
|
|
143
|
+
if (path.startsWith('/api/v1/crates') && context.method === 'GET') {
|
|
144
|
+
const query = context.query?.q || '';
|
|
145
|
+
const perPage = parseInt(context.query?.per_page || '10', 10);
|
|
146
|
+
return this.handleSearch(query, perPage);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
status: 404,
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: this.createError('API endpoint not found'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle index file requests
|
|
158
|
+
* Paths: /1/{name}, /2/{name}, /3/{c}/{name}, /{p1}/{p2}/{name}
|
|
159
|
+
*/
|
|
160
|
+
private async handleIndexRequest(path: string): Promise<IResponse> {
|
|
161
|
+
// Parse index paths to extract crate name
|
|
162
|
+
const pathParts = path.split('/').filter(p => p);
|
|
163
|
+
let crateName: string | null = null;
|
|
164
|
+
|
|
165
|
+
if (pathParts.length === 2 && pathParts[0] === '1') {
|
|
166
|
+
// 1-character names: /1/{name}
|
|
167
|
+
crateName = pathParts[1];
|
|
168
|
+
} else if (pathParts.length === 2 && pathParts[0] === '2') {
|
|
169
|
+
// 2-character names: /2/{name}
|
|
170
|
+
crateName = pathParts[1];
|
|
171
|
+
} else if (pathParts.length === 3 && pathParts[0] === '3') {
|
|
172
|
+
// 3-character names: /3/{c}/{name}
|
|
173
|
+
crateName = pathParts[2];
|
|
174
|
+
} else if (pathParts.length === 3) {
|
|
175
|
+
// 4+ character names: /{p1}/{p2}/{name}
|
|
176
|
+
crateName = pathParts[2];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!crateName) {
|
|
180
|
+
return {
|
|
181
|
+
status: 404,
|
|
182
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
183
|
+
body: Buffer.from(''),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this.handleIndexFile(crateName);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Serve config.json
|
|
192
|
+
*/
|
|
193
|
+
private async handleConfigJson(): Promise<IResponse> {
|
|
194
|
+
const config = await this.storage.getCargoConfig();
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
status: 200,
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: config || {
|
|
200
|
+
dl: `${this.registryUrl}/api/v1/crates/{crate}/{version}/download`,
|
|
201
|
+
api: this.registryUrl,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Serve index file for a crate
|
|
208
|
+
*/
|
|
209
|
+
private async handleIndexFile(crateName: string): Promise<IResponse> {
|
|
210
|
+
const index = await this.storage.getCargoIndex(crateName);
|
|
211
|
+
|
|
212
|
+
if (!index || index.length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
status: 404,
|
|
215
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
216
|
+
body: Buffer.from(''),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Return newline-delimited JSON
|
|
221
|
+
const data = index.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
222
|
+
|
|
223
|
+
// Calculate ETag for caching
|
|
224
|
+
const crypto = await import('crypto');
|
|
225
|
+
const etag = `"${crypto.createHash('sha256').update(data).digest('hex')}"`;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
status: 200,
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'text/plain',
|
|
231
|
+
'ETag': etag,
|
|
232
|
+
},
|
|
233
|
+
body: Buffer.from(data, 'utf-8'),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse binary publish request
|
|
239
|
+
* Format: [4 bytes JSON len][JSON][4 bytes crate len][.crate file]
|
|
240
|
+
*/
|
|
241
|
+
private parsePublishRequest(body: Buffer): {
|
|
242
|
+
metadata: ICargoPublishMetadata;
|
|
243
|
+
crateFile: Buffer;
|
|
244
|
+
} {
|
|
245
|
+
let offset = 0;
|
|
246
|
+
|
|
247
|
+
// Read JSON length (4 bytes, u32 little-endian)
|
|
248
|
+
if (body.length < 4) {
|
|
249
|
+
throw new Error('Invalid publish request: body too short');
|
|
250
|
+
}
|
|
251
|
+
const jsonLength = body.readUInt32LE(offset);
|
|
252
|
+
offset += 4;
|
|
253
|
+
|
|
254
|
+
// Read JSON metadata
|
|
255
|
+
if (body.length < offset + jsonLength) {
|
|
256
|
+
throw new Error('Invalid publish request: JSON data incomplete');
|
|
257
|
+
}
|
|
258
|
+
const jsonBuffer = body.slice(offset, offset + jsonLength);
|
|
259
|
+
const metadata = JSON.parse(jsonBuffer.toString('utf-8'));
|
|
260
|
+
offset += jsonLength;
|
|
261
|
+
|
|
262
|
+
// Read crate file length (4 bytes, u32 little-endian)
|
|
263
|
+
if (body.length < offset + 4) {
|
|
264
|
+
throw new Error('Invalid publish request: crate length missing');
|
|
265
|
+
}
|
|
266
|
+
const crateLength = body.readUInt32LE(offset);
|
|
267
|
+
offset += 4;
|
|
268
|
+
|
|
269
|
+
// Read crate file
|
|
270
|
+
if (body.length < offset + crateLength) {
|
|
271
|
+
throw new Error('Invalid publish request: crate data incomplete');
|
|
272
|
+
}
|
|
273
|
+
const crateFile = body.slice(offset, offset + crateLength);
|
|
274
|
+
|
|
275
|
+
return { metadata, crateFile };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle crate publish
|
|
280
|
+
*/
|
|
281
|
+
private async handlePublish(
|
|
282
|
+
body: Buffer,
|
|
283
|
+
token: IAuthToken | null
|
|
284
|
+
): Promise<IResponse> {
|
|
285
|
+
this.logger.log('info', 'handlePublish: received publish request', {
|
|
286
|
+
bodyLength: body?.length || 0,
|
|
287
|
+
hasAuth: !!token
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Check authorization
|
|
291
|
+
if (!token) {
|
|
292
|
+
return {
|
|
293
|
+
status: 403,
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: this.createError('Authentication required'),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Parse binary request
|
|
300
|
+
let metadata: ICargoPublishMetadata;
|
|
301
|
+
let crateFile: Buffer;
|
|
302
|
+
try {
|
|
303
|
+
const parsed = this.parsePublishRequest(body);
|
|
304
|
+
metadata = parsed.metadata;
|
|
305
|
+
crateFile = parsed.crateFile;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.logger.log('error', 'handlePublish: parse error', { error: error.message });
|
|
308
|
+
return {
|
|
309
|
+
status: 400,
|
|
310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
311
|
+
body: this.createError(`Invalid request format: ${error.message}`),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Validate crate name
|
|
316
|
+
if (!this.validateCrateName(metadata.name)) {
|
|
317
|
+
return {
|
|
318
|
+
status: 400,
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: this.createError('Invalid crate name'),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check permission
|
|
325
|
+
const hasPermission = await this.checkPermission(token, metadata.name, 'write');
|
|
326
|
+
if (!hasPermission) {
|
|
327
|
+
this.logger.log('warn', 'handlePublish: unauthorized', {
|
|
328
|
+
crateName: metadata.name,
|
|
329
|
+
userId: token.userId
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
status: 403,
|
|
333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
334
|
+
body: this.createError('Insufficient permissions'),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Calculate SHA256 checksum
|
|
339
|
+
const crypto = await import('crypto');
|
|
340
|
+
const cksum = crypto.createHash('sha256').update(crateFile).digest('hex');
|
|
341
|
+
|
|
342
|
+
// Create index entry
|
|
343
|
+
const indexEntry: ICargoIndexEntry = {
|
|
344
|
+
name: metadata.name,
|
|
345
|
+
vers: metadata.vers,
|
|
346
|
+
deps: metadata.deps,
|
|
347
|
+
cksum,
|
|
348
|
+
features: metadata.features,
|
|
349
|
+
yanked: false,
|
|
350
|
+
links: metadata.links || null,
|
|
351
|
+
v: 2,
|
|
352
|
+
rust_version: metadata.rust_version,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Check for duplicate version
|
|
356
|
+
const existingIndex = await this.storage.getCargoIndex(metadata.name) || [];
|
|
357
|
+
if (existingIndex.some(e => e.vers === metadata.vers)) {
|
|
358
|
+
return {
|
|
359
|
+
status: 400,
|
|
360
|
+
headers: { 'Content-Type': 'application/json' },
|
|
361
|
+
body: this.createError(`Version ${metadata.vers} already exists`),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Store crate file
|
|
366
|
+
await this.storage.putCargoCrate(metadata.name, metadata.vers, crateFile);
|
|
367
|
+
|
|
368
|
+
// Update index (append new version)
|
|
369
|
+
existingIndex.push(indexEntry);
|
|
370
|
+
await this.storage.putCargoIndex(metadata.name, existingIndex);
|
|
371
|
+
|
|
372
|
+
this.logger.log('success', 'handlePublish: published crate', {
|
|
373
|
+
name: metadata.name,
|
|
374
|
+
version: metadata.vers,
|
|
375
|
+
checksum: cksum
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const response: ICargoPublishResponse = {
|
|
379
|
+
warnings: {
|
|
380
|
+
invalid_categories: [],
|
|
381
|
+
invalid_badges: [],
|
|
382
|
+
other: [],
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
status: 200,
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
body: response,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Handle crate download
|
|
395
|
+
*/
|
|
396
|
+
private async handleDownload(
|
|
397
|
+
crateName: string,
|
|
398
|
+
version: string
|
|
399
|
+
): Promise<IResponse> {
|
|
400
|
+
this.logger.log('debug', 'handleDownload', { crate: crateName, version });
|
|
401
|
+
|
|
402
|
+
const crateFile = await this.storage.getCargoCrate(crateName, version);
|
|
403
|
+
|
|
404
|
+
if (!crateFile) {
|
|
405
|
+
return {
|
|
406
|
+
status: 404,
|
|
407
|
+
headers: { 'Content-Type': 'application/json' },
|
|
408
|
+
body: this.createError('Crate not found'),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
status: 200,
|
|
414
|
+
headers: {
|
|
415
|
+
'Content-Type': 'application/gzip',
|
|
416
|
+
'Content-Length': crateFile.length.toString(),
|
|
417
|
+
'Content-Disposition': `attachment; filename="${crateName}-${version}.crate"`,
|
|
418
|
+
},
|
|
419
|
+
body: crateFile,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handle yank operation
|
|
425
|
+
*/
|
|
426
|
+
private async handleYank(
|
|
427
|
+
crateName: string,
|
|
428
|
+
version: string,
|
|
429
|
+
token: IAuthToken | null
|
|
430
|
+
): Promise<IResponse> {
|
|
431
|
+
return this.handleYankOperation(crateName, version, token, true);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Handle unyank operation
|
|
436
|
+
*/
|
|
437
|
+
private async handleUnyank(
|
|
438
|
+
crateName: string,
|
|
439
|
+
version: string,
|
|
440
|
+
token: IAuthToken | null
|
|
441
|
+
): Promise<IResponse> {
|
|
442
|
+
return this.handleYankOperation(crateName, version, token, false);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Handle yank/unyank operation
|
|
447
|
+
*/
|
|
448
|
+
private async handleYankOperation(
|
|
449
|
+
crateName: string,
|
|
450
|
+
version: string,
|
|
451
|
+
token: IAuthToken | null,
|
|
452
|
+
yank: boolean
|
|
453
|
+
): Promise<IResponse> {
|
|
454
|
+
this.logger.log('info', `handle${yank ? 'Yank' : 'Unyank'}`, {
|
|
455
|
+
crate: crateName,
|
|
456
|
+
version,
|
|
457
|
+
hasAuth: !!token
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Check authorization
|
|
461
|
+
if (!token) {
|
|
462
|
+
return {
|
|
463
|
+
status: 403,
|
|
464
|
+
headers: { 'Content-Type': 'application/json' },
|
|
465
|
+
body: this.createError('Authentication required'),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check permission
|
|
470
|
+
const hasPermission = await this.checkPermission(token, crateName, 'write');
|
|
471
|
+
if (!hasPermission) {
|
|
472
|
+
return {
|
|
473
|
+
status: 403,
|
|
474
|
+
headers: { 'Content-Type': 'application/json' },
|
|
475
|
+
body: this.createError('Insufficient permissions'),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Load index
|
|
480
|
+
const index = await this.storage.getCargoIndex(crateName);
|
|
481
|
+
if (!index) {
|
|
482
|
+
return {
|
|
483
|
+
status: 404,
|
|
484
|
+
headers: { 'Content-Type': 'application/json' },
|
|
485
|
+
body: this.createError('Crate not found'),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Find version
|
|
490
|
+
const entry = index.find(e => e.vers === version);
|
|
491
|
+
if (!entry) {
|
|
492
|
+
return {
|
|
493
|
+
status: 404,
|
|
494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
495
|
+
body: this.createError('Version not found'),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Update yank status
|
|
500
|
+
entry.yanked = yank;
|
|
501
|
+
|
|
502
|
+
// Save index (NOTE: do NOT delete .crate file)
|
|
503
|
+
await this.storage.putCargoIndex(crateName, index);
|
|
504
|
+
|
|
505
|
+
this.logger.log('success', `${yank ? 'Yanked' : 'Unyanked'} version`, {
|
|
506
|
+
crate: crateName,
|
|
507
|
+
version
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const response: ICargoYankResponse = { ok: true };
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
status: 200,
|
|
514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
515
|
+
body: response,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Handle search
|
|
521
|
+
*/
|
|
522
|
+
private async handleSearch(query: string, perPage: number): Promise<IResponse> {
|
|
523
|
+
this.logger.log('debug', 'handleSearch', { query, perPage });
|
|
524
|
+
|
|
525
|
+
const results: ICargoSearchResult[] = [];
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
// List all index paths
|
|
529
|
+
const indexPaths = await this.storage.listObjects('cargo/index/');
|
|
530
|
+
|
|
531
|
+
// Extract unique crate names
|
|
532
|
+
const crateNames = new Set<string>();
|
|
533
|
+
for (const path of indexPaths) {
|
|
534
|
+
// Parse path to extract crate name
|
|
535
|
+
const parts = path.split('/');
|
|
536
|
+
if (parts.length >= 3) {
|
|
537
|
+
const name = parts[parts.length - 1];
|
|
538
|
+
if (name && !name.includes('.')) {
|
|
539
|
+
crateNames.add(name);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
this.logger.log('debug', `handleSearch: found ${crateNames.size} crates`, {
|
|
545
|
+
totalCrates: crateNames.size
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Filter and process matching crates
|
|
549
|
+
for (const name of crateNames) {
|
|
550
|
+
if (!query || name.toLowerCase().includes(query.toLowerCase())) {
|
|
551
|
+
const index = await this.storage.getCargoIndex(name);
|
|
552
|
+
if (index && index.length > 0) {
|
|
553
|
+
// Find latest non-yanked version
|
|
554
|
+
const nonYanked = index.filter(e => !e.yanked);
|
|
555
|
+
if (nonYanked.length > 0) {
|
|
556
|
+
// Sort by version (simplified - should use semver)
|
|
557
|
+
const sorted = [...nonYanked].sort((a, b) => b.vers.localeCompare(a.vers));
|
|
558
|
+
|
|
559
|
+
results.push({
|
|
560
|
+
name: sorted[0].name,
|
|
561
|
+
max_version: sorted[0].vers,
|
|
562
|
+
description: '', // Would need to store separately
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (results.length >= perPage) break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
this.logger.log('error', 'handleSearch: error', { error: error.message });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const response: ICargoSearchResponse = {
|
|
575
|
+
crates: results,
|
|
576
|
+
meta: {
|
|
577
|
+
total: results.length,
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
status: 200,
|
|
583
|
+
headers: { 'Content-Type': 'application/json' },
|
|
584
|
+
body: response,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Validate crate name
|
|
590
|
+
* Rules: lowercase alphanumeric + _ and -, length 1-64
|
|
591
|
+
*/
|
|
592
|
+
private validateCrateName(name: string): boolean {
|
|
593
|
+
return /^[a-z0-9_-]+$/.test(name) && name.length >= 1 && name.length <= 64;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Create error response
|
|
598
|
+
*/
|
|
599
|
+
private createError(detail: string): ICargoError {
|
|
600
|
+
return {
|
|
601
|
+
errors: [{ detail }],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|