@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.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/cargo/classes.cargoregistry.d.ts +79 -0
  3. package/dist_ts/cargo/classes.cargoregistry.js +490 -0
  4. package/dist_ts/cargo/index.d.ts +5 -0
  5. package/dist_ts/cargo/index.js +6 -0
  6. package/dist_ts/cargo/interfaces.cargo.d.ts +160 -0
  7. package/dist_ts/cargo/interfaces.cargo.js +6 -0
  8. package/dist_ts/classes.smartregistry.d.ts +2 -2
  9. package/dist_ts/classes.smartregistry.js +50 -2
  10. package/dist_ts/composer/classes.composerregistry.d.ts +26 -0
  11. package/dist_ts/composer/classes.composerregistry.js +366 -0
  12. package/dist_ts/composer/helpers.composer.d.ts +35 -0
  13. package/dist_ts/composer/helpers.composer.js +120 -0
  14. package/dist_ts/composer/index.d.ts +7 -0
  15. package/dist_ts/composer/index.js +8 -0
  16. package/dist_ts/composer/interfaces.composer.d.ts +102 -0
  17. package/dist_ts/composer/interfaces.composer.js +6 -0
  18. package/dist_ts/core/classes.authmanager.d.ts +46 -1
  19. package/dist_ts/core/classes.authmanager.js +121 -12
  20. package/dist_ts/core/classes.registrystorage.d.ts +103 -0
  21. package/dist_ts/core/classes.registrystorage.js +253 -1
  22. package/dist_ts/core/interfaces.core.d.ts +4 -1
  23. package/dist_ts/index.d.ts +4 -1
  24. package/dist_ts/index.js +8 -2
  25. package/dist_ts/maven/classes.mavenregistry.d.ts +35 -0
  26. package/dist_ts/maven/classes.mavenregistry.js +407 -0
  27. package/dist_ts/maven/helpers.maven.d.ts +68 -0
  28. package/dist_ts/maven/helpers.maven.js +286 -0
  29. package/dist_ts/maven/index.d.ts +6 -0
  30. package/dist_ts/maven/index.js +7 -0
  31. package/dist_ts/maven/interfaces.maven.d.ts +116 -0
  32. package/dist_ts/maven/interfaces.maven.js +6 -0
  33. package/package.json +3 -2
  34. package/readme.md +288 -14
  35. package/ts/00_commitinfo_data.ts +1 -1
  36. package/ts/cargo/classes.cargoregistry.ts +604 -0
  37. package/ts/cargo/index.ts +6 -0
  38. package/ts/cargo/interfaces.cargo.ts +169 -0
  39. package/ts/classes.smartregistry.ts +56 -2
  40. package/ts/composer/classes.composerregistry.ts +475 -0
  41. package/ts/composer/helpers.composer.ts +139 -0
  42. package/ts/composer/index.ts +8 -0
  43. package/ts/composer/interfaces.composer.ts +111 -0
  44. package/ts/core/classes.authmanager.ts +145 -12
  45. package/ts/core/classes.registrystorage.ts +334 -0
  46. package/ts/core/interfaces.core.ts +4 -1
  47. package/ts/index.ts +10 -1
  48. package/ts/maven/classes.mavenregistry.ts +580 -0
  49. package/ts/maven/helpers.maven.ts +346 -0
  50. package/ts/maven/index.ts +7 -0
  51. package/ts/maven/interfaces.maven.ts +127 -0
@@ -0,0 +1,580 @@
1
+ /**
2
+ * Maven Registry Implementation
3
+ * Implements Maven repository protocol for Java artifacts
4
+ */
5
+
6
+ import { BaseRegistry } from '../core/classes.baseregistry.js';
7
+ import type { RegistryStorage } from '../core/classes.registrystorage.js';
8
+ import type { AuthManager } from '../core/classes.authmanager.js';
9
+ import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
10
+ import type { IMavenCoordinate, IMavenMetadata, IChecksums } from './interfaces.maven.js';
11
+ import {
12
+ pathToGAV,
13
+ buildFilename,
14
+ calculateChecksums,
15
+ generateMetadataXml,
16
+ parseMetadataXml,
17
+ formatMavenTimestamp,
18
+ isSnapshot,
19
+ validatePom,
20
+ extractGAVFromPom,
21
+ gavToPath,
22
+ } from './helpers.maven.js';
23
+
24
+ /**
25
+ * Maven Registry class
26
+ * Handles Maven repository HTTP protocol
27
+ */
28
+ export class MavenRegistry extends BaseRegistry {
29
+ private storage: RegistryStorage;
30
+ private authManager: AuthManager;
31
+ private basePath: string = '/maven';
32
+ private registryUrl: string;
33
+
34
+ constructor(
35
+ storage: RegistryStorage,
36
+ authManager: AuthManager,
37
+ basePath: string,
38
+ registryUrl: string
39
+ ) {
40
+ super();
41
+ this.storage = storage;
42
+ this.authManager = authManager;
43
+ this.basePath = basePath;
44
+ this.registryUrl = registryUrl;
45
+ }
46
+
47
+ public async init(): Promise<void> {
48
+ // No special initialization needed for Maven
49
+ }
50
+
51
+ public getBasePath(): string {
52
+ return this.basePath;
53
+ }
54
+
55
+ public async handleRequest(context: IRequestContext): Promise<IResponse> {
56
+ // Remove base path from URL
57
+ const path = context.path.replace(this.basePath, '');
58
+
59
+ // Extract token from Authorization header
60
+ const authHeader = context.headers['authorization'] || context.headers['Authorization'];
61
+ let token: IAuthToken | null = null;
62
+
63
+ if (authHeader) {
64
+ const tokenString = authHeader.replace(/^(Bearer|Basic)\s+/i, '');
65
+ // For now, try to validate as Maven token (reuse npm token type)
66
+ token = await this.authManager.validateToken(tokenString, 'maven');
67
+ }
68
+
69
+ // Parse path to determine request type
70
+ const coordinate = pathToGAV(path);
71
+
72
+ if (!coordinate) {
73
+ // Not a valid artifact path, could be metadata or root
74
+ if (path.endsWith('/maven-metadata.xml')) {
75
+ return this.handleMetadataRequest(context.method, path, token);
76
+ }
77
+
78
+ return {
79
+ status: 404,
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: { error: 'NOT_FOUND', message: 'Invalid Maven path' },
82
+ };
83
+ }
84
+
85
+ // Check if it's a checksum file
86
+ if (coordinate.extension === 'md5' || coordinate.extension === 'sha1' ||
87
+ coordinate.extension === 'sha256' || coordinate.extension === 'sha512') {
88
+ return this.handleChecksumRequest(context.method, coordinate, token, path);
89
+ }
90
+
91
+ // Handle artifact requests (JAR, POM, WAR, etc.)
92
+ return this.handleArtifactRequest(context.method, coordinate, token, context.body);
93
+ }
94
+
95
+ protected async checkPermission(
96
+ token: IAuthToken | null,
97
+ resource: string,
98
+ action: string
99
+ ): Promise<boolean> {
100
+ if (!token) return false;
101
+ return this.authManager.authorize(token, `maven:artifact:${resource}`, action);
102
+ }
103
+
104
+ // ========================================================================
105
+ // REQUEST HANDLERS
106
+ // ========================================================================
107
+
108
+ private async handleArtifactRequest(
109
+ method: string,
110
+ coordinate: IMavenCoordinate,
111
+ token: IAuthToken | null,
112
+ body?: Buffer | any
113
+ ): Promise<IResponse> {
114
+ const { groupId, artifactId, version } = coordinate;
115
+ const filename = buildFilename(coordinate);
116
+ const resource = `${groupId}:${artifactId}`;
117
+
118
+ switch (method) {
119
+ case 'GET':
120
+ case 'HEAD':
121
+ // Maven repositories typically allow anonymous reads
122
+ return method === 'GET'
123
+ ? this.getArtifact(groupId, artifactId, version, filename)
124
+ : this.headArtifact(groupId, artifactId, version, filename);
125
+
126
+ case 'PUT':
127
+ // Write permission required
128
+ if (!await this.checkPermission(token, resource, 'write')) {
129
+ return {
130
+ status: 401,
131
+ headers: {
132
+ 'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
133
+ },
134
+ body: { error: 'UNAUTHORIZED', message: 'Write permission required' },
135
+ };
136
+ }
137
+
138
+ if (!body) {
139
+ return {
140
+ status: 400,
141
+ headers: {},
142
+ body: { error: 'BAD_REQUEST', message: 'Request body required' },
143
+ };
144
+ }
145
+
146
+ return this.putArtifact(groupId, artifactId, version, filename, coordinate, body);
147
+
148
+ case 'DELETE':
149
+ // Delete permission required
150
+ if (!await this.checkPermission(token, resource, 'delete')) {
151
+ return {
152
+ status: 401,
153
+ headers: {
154
+ 'WWW-Authenticate': `Bearer realm="${this.basePath}",service="maven-registry"`,
155
+ },
156
+ body: { error: 'UNAUTHORIZED', message: 'Delete permission required' },
157
+ };
158
+ }
159
+
160
+ return this.deleteArtifact(groupId, artifactId, version, filename);
161
+
162
+ default:
163
+ return {
164
+ status: 405,
165
+ headers: { 'Allow': 'GET, HEAD, PUT, DELETE' },
166
+ body: { error: 'METHOD_NOT_ALLOWED', message: 'Method not allowed' },
167
+ };
168
+ }
169
+ }
170
+
171
+ private async handleChecksumRequest(
172
+ method: string,
173
+ coordinate: IMavenCoordinate,
174
+ token: IAuthToken | null,
175
+ path: string
176
+ ): Promise<IResponse> {
177
+ const { groupId, artifactId, version, extension } = coordinate;
178
+ const resource = `${groupId}:${artifactId}`;
179
+
180
+ // Checksums follow the same permissions as their artifacts (public read)
181
+ if (method === 'GET' || method === 'HEAD') {
182
+ return this.getChecksum(groupId, artifactId, version, coordinate, path);
183
+ }
184
+
185
+ return {
186
+ status: 405,
187
+ headers: { 'Allow': 'GET, HEAD' },
188
+ body: { error: 'METHOD_NOT_ALLOWED', message: 'Checksums are auto-generated' },
189
+ };
190
+ }
191
+
192
+ private async handleMetadataRequest(
193
+ method: string,
194
+ path: string,
195
+ token: IAuthToken | null
196
+ ): Promise<IResponse> {
197
+ // Parse path to extract groupId and artifactId
198
+ // Path format: /com/example/my-lib/maven-metadata.xml
199
+ const parts = path.split('/').filter(p => p && p !== 'maven-metadata.xml');
200
+
201
+ if (parts.length < 2) {
202
+ return {
203
+ status: 400,
204
+ headers: {},
205
+ body: { error: 'BAD_REQUEST', message: 'Invalid metadata path' },
206
+ };
207
+ }
208
+
209
+ const artifactId = parts[parts.length - 1];
210
+ const groupId = parts.slice(0, -1).join('.');
211
+ const resource = `${groupId}:${artifactId}`;
212
+
213
+ if (method === 'GET') {
214
+ // Metadata is usually public (read permission optional)
215
+ // Some registries allow anonymous metadata access
216
+ return this.getMetadata(groupId, artifactId);
217
+ }
218
+
219
+ return {
220
+ status: 405,
221
+ headers: { 'Allow': 'GET' },
222
+ body: { error: 'METHOD_NOT_ALLOWED', message: 'Metadata is auto-generated' },
223
+ };
224
+ }
225
+
226
+ // ========================================================================
227
+ // ARTIFACT OPERATIONS
228
+ // ========================================================================
229
+
230
+ private async getArtifact(
231
+ groupId: string,
232
+ artifactId: string,
233
+ version: string,
234
+ filename: string
235
+ ): Promise<IResponse> {
236
+ const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
237
+
238
+ if (!data) {
239
+ return {
240
+ status: 404,
241
+ headers: {},
242
+ body: { error: 'NOT_FOUND', message: 'Artifact not found' },
243
+ };
244
+ }
245
+
246
+ // Determine content type based on extension
247
+ const extension = filename.split('.').pop() || '';
248
+ const contentType = this.getContentType(extension);
249
+
250
+ return {
251
+ status: 200,
252
+ headers: {
253
+ 'Content-Type': contentType,
254
+ 'Content-Length': data.length.toString(),
255
+ },
256
+ body: data,
257
+ };
258
+ }
259
+
260
+ private async headArtifact(
261
+ groupId: string,
262
+ artifactId: string,
263
+ version: string,
264
+ filename: string
265
+ ): Promise<IResponse> {
266
+ const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
267
+
268
+ if (!exists) {
269
+ return {
270
+ status: 404,
271
+ headers: {},
272
+ body: null,
273
+ };
274
+ }
275
+
276
+ // Get file size for Content-Length header
277
+ const data = await this.storage.getMavenArtifact(groupId, artifactId, version, filename);
278
+ const extension = filename.split('.').pop() || '';
279
+ const contentType = this.getContentType(extension);
280
+
281
+ return {
282
+ status: 200,
283
+ headers: {
284
+ 'Content-Type': contentType,
285
+ 'Content-Length': data ? data.length.toString() : '0',
286
+ },
287
+ body: null,
288
+ };
289
+ }
290
+
291
+ private async putArtifact(
292
+ groupId: string,
293
+ artifactId: string,
294
+ version: string,
295
+ filename: string,
296
+ coordinate: IMavenCoordinate,
297
+ body: Buffer | any
298
+ ): Promise<IResponse> {
299
+ const data = Buffer.isBuffer(body) ? body : Buffer.from(JSON.stringify(body));
300
+
301
+ // Validate POM if uploading .pom file
302
+ if (coordinate.extension === 'pom') {
303
+ const pomValid = validatePom(data.toString('utf-8'));
304
+ if (!pomValid) {
305
+ return {
306
+ status: 400,
307
+ headers: {},
308
+ body: { error: 'INVALID_POM', message: 'Invalid POM file' },
309
+ };
310
+ }
311
+
312
+ // Verify GAV matches path
313
+ const pomGAV = extractGAVFromPom(data.toString('utf-8'));
314
+ if (pomGAV && (pomGAV.groupId !== groupId || pomGAV.artifactId !== artifactId || pomGAV.version !== version)) {
315
+ return {
316
+ status: 400,
317
+ headers: {},
318
+ body: { error: 'GAV_MISMATCH', message: 'POM coordinates do not match upload path' },
319
+ };
320
+ }
321
+ }
322
+
323
+ // Store the artifact
324
+ await this.storage.putMavenArtifact(groupId, artifactId, version, filename, data);
325
+
326
+ // Generate and store checksums
327
+ const checksums = await calculateChecksums(data);
328
+ await this.storeChecksums(groupId, artifactId, version, filename, checksums);
329
+
330
+ // Update maven-metadata.xml if this is a primary artifact (jar, pom, war)
331
+ if (['jar', 'pom', 'war', 'ear', 'aar'].includes(coordinate.extension)) {
332
+ await this.updateMetadata(groupId, artifactId, version);
333
+ }
334
+
335
+ return {
336
+ status: 201,
337
+ headers: {
338
+ 'Location': `${this.registryUrl}/${gavToPath(groupId, artifactId, version)}/${filename}`,
339
+ },
340
+ body: { success: true, message: 'Artifact uploaded successfully' },
341
+ };
342
+ }
343
+
344
+ private async deleteArtifact(
345
+ groupId: string,
346
+ artifactId: string,
347
+ version: string,
348
+ filename: string
349
+ ): Promise<IResponse> {
350
+ const exists = await this.storage.mavenArtifactExists(groupId, artifactId, version, filename);
351
+
352
+ if (!exists) {
353
+ return {
354
+ status: 404,
355
+ headers: {},
356
+ body: { error: 'NOT_FOUND', message: 'Artifact not found' },
357
+ };
358
+ }
359
+
360
+ await this.storage.deleteMavenArtifact(groupId, artifactId, version, filename);
361
+
362
+ // Also delete checksums
363
+ for (const ext of ['md5', 'sha1', 'sha256', 'sha512']) {
364
+ const checksumFile = `${filename}.${ext}`;
365
+ const checksumExists = await this.storage.mavenArtifactExists(groupId, artifactId, version, checksumFile);
366
+ if (checksumExists) {
367
+ await this.storage.deleteMavenArtifact(groupId, artifactId, version, checksumFile);
368
+ }
369
+ }
370
+
371
+ return {
372
+ status: 204,
373
+ headers: {},
374
+ body: null,
375
+ };
376
+ }
377
+
378
+ // ========================================================================
379
+ // CHECKSUM OPERATIONS
380
+ // ========================================================================
381
+
382
+ private async getChecksum(
383
+ groupId: string,
384
+ artifactId: string,
385
+ version: string,
386
+ coordinate: IMavenCoordinate,
387
+ fullPath: string
388
+ ): Promise<IResponse> {
389
+ // Extract the filename from the full path (last component)
390
+ // The fullPath might be something like /com/example/test/test-artifact/1.0.0/test-artifact-1.0.0.jar.md5
391
+ const pathParts = fullPath.split('/');
392
+ const checksumFilename = pathParts[pathParts.length - 1];
393
+
394
+ const data = await this.storage.getMavenArtifact(groupId, artifactId, version, checksumFilename);
395
+
396
+ if (!data) {
397
+ return {
398
+ status: 404,
399
+ headers: {},
400
+ body: { error: 'NOT_FOUND', message: 'Checksum not found' },
401
+ };
402
+ }
403
+
404
+ return {
405
+ status: 200,
406
+ headers: {
407
+ 'Content-Type': 'text/plain',
408
+ 'Content-Length': data.length.toString(),
409
+ },
410
+ body: data,
411
+ };
412
+ }
413
+
414
+ private async storeChecksums(
415
+ groupId: string,
416
+ artifactId: string,
417
+ version: string,
418
+ filename: string,
419
+ checksums: IChecksums
420
+ ): Promise<void> {
421
+ // Store each checksum as a separate file
422
+ await this.storage.putMavenArtifact(
423
+ groupId,
424
+ artifactId,
425
+ version,
426
+ `${filename}.md5`,
427
+ Buffer.from(checksums.md5, 'utf-8')
428
+ );
429
+
430
+ await this.storage.putMavenArtifact(
431
+ groupId,
432
+ artifactId,
433
+ version,
434
+ `${filename}.sha1`,
435
+ Buffer.from(checksums.sha1, 'utf-8')
436
+ );
437
+
438
+ if (checksums.sha256) {
439
+ await this.storage.putMavenArtifact(
440
+ groupId,
441
+ artifactId,
442
+ version,
443
+ `${filename}.sha256`,
444
+ Buffer.from(checksums.sha256, 'utf-8')
445
+ );
446
+ }
447
+
448
+ if (checksums.sha512) {
449
+ await this.storage.putMavenArtifact(
450
+ groupId,
451
+ artifactId,
452
+ version,
453
+ `${filename}.sha512`,
454
+ Buffer.from(checksums.sha512, 'utf-8')
455
+ );
456
+ }
457
+ }
458
+
459
+ // ========================================================================
460
+ // METADATA OPERATIONS
461
+ // ========================================================================
462
+
463
+ private async getMetadata(groupId: string, artifactId: string): Promise<IResponse> {
464
+ const metadataBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
465
+
466
+ if (!metadataBuffer) {
467
+ // Generate empty metadata if none exists
468
+ const emptyMetadata: IMavenMetadata = {
469
+ groupId,
470
+ artifactId,
471
+ versioning: {
472
+ versions: [],
473
+ lastUpdated: formatMavenTimestamp(new Date()),
474
+ },
475
+ };
476
+
477
+ const xml = generateMetadataXml(emptyMetadata);
478
+ return {
479
+ status: 200,
480
+ headers: {
481
+ 'Content-Type': 'application/xml',
482
+ 'Content-Length': xml.length.toString(),
483
+ },
484
+ body: Buffer.from(xml, 'utf-8'),
485
+ };
486
+ }
487
+
488
+ return {
489
+ status: 200,
490
+ headers: {
491
+ 'Content-Type': 'application/xml',
492
+ 'Content-Length': metadataBuffer.length.toString(),
493
+ },
494
+ body: metadataBuffer,
495
+ };
496
+ }
497
+
498
+ private async updateMetadata(
499
+ groupId: string,
500
+ artifactId: string,
501
+ newVersion: string
502
+ ): Promise<void> {
503
+ // Get existing metadata or create new
504
+ const existingBuffer = await this.storage.getMavenMetadata(groupId, artifactId);
505
+ let metadata: IMavenMetadata;
506
+
507
+ if (existingBuffer) {
508
+ const parsed = parseMetadataXml(existingBuffer.toString('utf-8'));
509
+ if (parsed) {
510
+ metadata = parsed;
511
+ } else {
512
+ // Create new if parsing failed
513
+ metadata = {
514
+ groupId,
515
+ artifactId,
516
+ versioning: {
517
+ versions: [],
518
+ lastUpdated: formatMavenTimestamp(new Date()),
519
+ },
520
+ };
521
+ }
522
+ } else {
523
+ metadata = {
524
+ groupId,
525
+ artifactId,
526
+ versioning: {
527
+ versions: [],
528
+ lastUpdated: formatMavenTimestamp(new Date()),
529
+ },
530
+ };
531
+ }
532
+
533
+ // Add new version if not already present
534
+ if (!metadata.versioning.versions.includes(newVersion)) {
535
+ metadata.versioning.versions.push(newVersion);
536
+ metadata.versioning.versions.sort(); // Sort versions
537
+ }
538
+
539
+ // Update latest and release
540
+ const versions = metadata.versioning.versions;
541
+ metadata.versioning.latest = versions[versions.length - 1];
542
+
543
+ // Release is the latest non-SNAPSHOT version
544
+ const releaseVersions = versions.filter(v => !isSnapshot(v));
545
+ if (releaseVersions.length > 0) {
546
+ metadata.versioning.release = releaseVersions[releaseVersions.length - 1];
547
+ }
548
+
549
+ // Update timestamp
550
+ metadata.versioning.lastUpdated = formatMavenTimestamp(new Date());
551
+
552
+ // Generate and store XML
553
+ const xml = generateMetadataXml(metadata);
554
+ await this.storage.putMavenMetadata(groupId, artifactId, Buffer.from(xml, 'utf-8'));
555
+
556
+ // Note: Checksums for maven-metadata.xml are optional and not critical
557
+ // They would need special handling since metadata uses a different storage path
558
+ }
559
+
560
+ // ========================================================================
561
+ // UTILITY METHODS
562
+ // ========================================================================
563
+
564
+ private getContentType(extension: string): string {
565
+ const contentTypes: Record<string, string> = {
566
+ 'jar': 'application/java-archive',
567
+ 'war': 'application/java-archive',
568
+ 'ear': 'application/java-archive',
569
+ 'aar': 'application/java-archive',
570
+ 'pom': 'application/xml',
571
+ 'xml': 'application/xml',
572
+ 'md5': 'text/plain',
573
+ 'sha1': 'text/plain',
574
+ 'sha256': 'text/plain',
575
+ 'sha512': 'text/plain',
576
+ };
577
+
578
+ return contentTypes[extension] || 'application/octet-stream';
579
+ }
580
+ }