@push.rocks/smartregistry 1.5.0 → 1.7.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.
@@ -18,14 +18,8 @@ export class RegistryStorage implements IStorageBackend {
18
18
  * Initialize the storage backend
19
19
  */
20
20
  public async init(): Promise<void> {
21
- this.smartBucket = new plugins.smartbucket.SmartBucket({
22
- accessKey: this.config.accessKey,
23
- accessSecret: this.config.accessSecret,
24
- endpoint: this.config.endpoint,
25
- port: this.config.port || 443,
26
- useSsl: this.config.useSsl !== false,
27
- region: this.config.region || 'us-east-1',
28
- });
21
+ // Pass config as IS3Descriptor to SmartBucket (bucketName is extra, SmartBucket ignores it)
22
+ this.smartBucket = new plugins.smartbucket.SmartBucket(this.config as plugins.tsclass.storage.IS3Descriptor);
29
23
 
30
24
  // Ensure bucket exists
31
25
  await this.smartBucket.createBucket(this.bucketName).catch(() => {
@@ -828,4 +822,240 @@ export class RegistryStorage implements IStorageBackend {
828
822
  private getPypiPackageFilePath(packageName: string, filename: string): string {
829
823
  return `pypi/packages/${packageName}/${filename}`;
830
824
  }
825
+
826
+ // ========================================================================
827
+ // RUBYGEMS STORAGE METHODS
828
+ // ========================================================================
829
+
830
+ /**
831
+ * Get RubyGems versions file (compact index)
832
+ */
833
+ public async getRubyGemsVersions(): Promise<string | null> {
834
+ const path = this.getRubyGemsVersionsPath();
835
+ const data = await this.getObject(path);
836
+ return data ? data.toString('utf-8') : null;
837
+ }
838
+
839
+ /**
840
+ * Store RubyGems versions file (compact index)
841
+ */
842
+ public async putRubyGemsVersions(content: string): Promise<void> {
843
+ const path = this.getRubyGemsVersionsPath();
844
+ const data = Buffer.from(content, 'utf-8');
845
+ return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
846
+ }
847
+
848
+ /**
849
+ * Get RubyGems info file for a gem (compact index)
850
+ */
851
+ public async getRubyGemsInfo(gemName: string): Promise<string | null> {
852
+ const path = this.getRubyGemsInfoPath(gemName);
853
+ const data = await this.getObject(path);
854
+ return data ? data.toString('utf-8') : null;
855
+ }
856
+
857
+ /**
858
+ * Store RubyGems info file for a gem (compact index)
859
+ */
860
+ public async putRubyGemsInfo(gemName: string, content: string): Promise<void> {
861
+ const path = this.getRubyGemsInfoPath(gemName);
862
+ const data = Buffer.from(content, 'utf-8');
863
+ return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
864
+ }
865
+
866
+ /**
867
+ * Get RubyGems names file
868
+ */
869
+ public async getRubyGemsNames(): Promise<string | null> {
870
+ const path = this.getRubyGemsNamesPath();
871
+ const data = await this.getObject(path);
872
+ return data ? data.toString('utf-8') : null;
873
+ }
874
+
875
+ /**
876
+ * Store RubyGems names file
877
+ */
878
+ public async putRubyGemsNames(content: string): Promise<void> {
879
+ const path = this.getRubyGemsNamesPath();
880
+ const data = Buffer.from(content, 'utf-8');
881
+ return this.putObject(path, data, { 'Content-Type': 'text/plain; charset=utf-8' });
882
+ }
883
+
884
+ /**
885
+ * Get RubyGems .gem file
886
+ */
887
+ public async getRubyGemsGem(gemName: string, version: string, platform?: string): Promise<Buffer | null> {
888
+ const path = this.getRubyGemsGemPath(gemName, version, platform);
889
+ return this.getObject(path);
890
+ }
891
+
892
+ /**
893
+ * Store RubyGems .gem file
894
+ */
895
+ public async putRubyGemsGem(
896
+ gemName: string,
897
+ version: string,
898
+ data: Buffer,
899
+ platform?: string
900
+ ): Promise<void> {
901
+ const path = this.getRubyGemsGemPath(gemName, version, platform);
902
+ return this.putObject(path, data, { 'Content-Type': 'application/octet-stream' });
903
+ }
904
+
905
+ /**
906
+ * Check if RubyGems .gem file exists
907
+ */
908
+ public async rubyGemsGemExists(gemName: string, version: string, platform?: string): Promise<boolean> {
909
+ const path = this.getRubyGemsGemPath(gemName, version, platform);
910
+ return this.objectExists(path);
911
+ }
912
+
913
+ /**
914
+ * Delete RubyGems .gem file
915
+ */
916
+ public async deleteRubyGemsGem(gemName: string, version: string, platform?: string): Promise<void> {
917
+ const path = this.getRubyGemsGemPath(gemName, version, platform);
918
+ return this.deleteObject(path);
919
+ }
920
+
921
+ /**
922
+ * Get RubyGems metadata
923
+ */
924
+ public async getRubyGemsMetadata(gemName: string): Promise<any | null> {
925
+ const path = this.getRubyGemsMetadataPath(gemName);
926
+ const data = await this.getObject(path);
927
+ return data ? JSON.parse(data.toString('utf-8')) : null;
928
+ }
929
+
930
+ /**
931
+ * Store RubyGems metadata
932
+ */
933
+ public async putRubyGemsMetadata(gemName: string, metadata: any): Promise<void> {
934
+ const path = this.getRubyGemsMetadataPath(gemName);
935
+ const data = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
936
+ return this.putObject(path, data, { 'Content-Type': 'application/json' });
937
+ }
938
+
939
+ /**
940
+ * Check if RubyGems metadata exists
941
+ */
942
+ public async rubyGemsMetadataExists(gemName: string): Promise<boolean> {
943
+ const path = this.getRubyGemsMetadataPath(gemName);
944
+ return this.objectExists(path);
945
+ }
946
+
947
+ /**
948
+ * Delete RubyGems metadata
949
+ */
950
+ public async deleteRubyGemsMetadata(gemName: string): Promise<void> {
951
+ const path = this.getRubyGemsMetadataPath(gemName);
952
+ return this.deleteObject(path);
953
+ }
954
+
955
+ /**
956
+ * List all RubyGems
957
+ */
958
+ public async listRubyGems(): Promise<string[]> {
959
+ const prefix = 'rubygems/metadata/';
960
+ const objects = await this.listObjects(prefix);
961
+ const gems = new Set<string>();
962
+
963
+ // Extract gem names from paths like: rubygems/metadata/gem-name/metadata.json
964
+ for (const obj of objects) {
965
+ const match = obj.match(/^rubygems\/metadata\/([^\/]+)\/metadata\.json$/);
966
+ if (match) {
967
+ gems.add(match[1]);
968
+ }
969
+ }
970
+
971
+ return Array.from(gems).sort();
972
+ }
973
+
974
+ /**
975
+ * List all versions of a RubyGem
976
+ */
977
+ public async listRubyGemsVersions(gemName: string): Promise<string[]> {
978
+ const prefix = `rubygems/gems/`;
979
+ const objects = await this.listObjects(prefix);
980
+ const versions = new Set<string>();
981
+
982
+ // Extract versions from filenames: gem-name-version[-platform].gem
983
+ const gemPrefix = `${gemName}-`;
984
+ for (const obj of objects) {
985
+ const filename = obj.split('/').pop();
986
+ if (!filename || !filename.startsWith(gemPrefix) || !filename.endsWith('.gem')) continue;
987
+
988
+ // Remove gem name prefix and .gem suffix
989
+ const versionPart = filename.substring(gemPrefix.length, filename.length - 4);
990
+
991
+ // Split on last hyphen to separate version from platform
992
+ const lastHyphen = versionPart.lastIndexOf('-');
993
+ const version = lastHyphen > 0 ? versionPart.substring(0, lastHyphen) : versionPart;
994
+
995
+ versions.add(version);
996
+ }
997
+
998
+ return Array.from(versions).sort();
999
+ }
1000
+
1001
+ /**
1002
+ * Delete entire RubyGem (all versions and files)
1003
+ */
1004
+ public async deleteRubyGem(gemName: string): Promise<void> {
1005
+ // Delete metadata
1006
+ await this.deleteRubyGemsMetadata(gemName);
1007
+
1008
+ // Delete all gem files
1009
+ const prefix = `rubygems/gems/`;
1010
+ const objects = await this.listObjects(prefix);
1011
+ const gemPrefix = `${gemName}-`;
1012
+
1013
+ for (const obj of objects) {
1014
+ const filename = obj.split('/').pop();
1015
+ if (filename && filename.startsWith(gemPrefix) && filename.endsWith('.gem')) {
1016
+ await this.deleteObject(obj);
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ /**
1022
+ * Delete specific version of a RubyGem
1023
+ */
1024
+ public async deleteRubyGemsVersion(gemName: string, version: string, platform?: string): Promise<void> {
1025
+ // Delete gem file
1026
+ await this.deleteRubyGemsGem(gemName, version, platform);
1027
+
1028
+ // Update metadata to remove this version
1029
+ const metadata = await this.getRubyGemsMetadata(gemName);
1030
+ if (metadata && metadata.versions) {
1031
+ const versionKey = platform ? `${version}-${platform}` : version;
1032
+ delete metadata.versions[versionKey];
1033
+ await this.putRubyGemsMetadata(gemName, metadata);
1034
+ }
1035
+ }
1036
+
1037
+ // ========================================================================
1038
+ // RUBYGEMS PATH HELPERS
1039
+ // ========================================================================
1040
+
1041
+ private getRubyGemsVersionsPath(): string {
1042
+ return 'rubygems/versions';
1043
+ }
1044
+
1045
+ private getRubyGemsInfoPath(gemName: string): string {
1046
+ return `rubygems/info/${gemName}`;
1047
+ }
1048
+
1049
+ private getRubyGemsNamesPath(): string {
1050
+ return 'rubygems/names';
1051
+ }
1052
+
1053
+ private getRubyGemsGemPath(gemName: string, version: string, platform?: string): string {
1054
+ const filename = platform ? `${gemName}-${version}-${platform}.gem` : `${gemName}-${version}.gem`;
1055
+ return `rubygems/gems/${filename}`;
1056
+ }
1057
+
1058
+ private getRubyGemsMetadataPath(gemName: string): string {
1059
+ return `rubygems/metadata/${gemName}/metadata.json`;
1060
+ }
831
1061
  }
@@ -2,6 +2,8 @@
2
2
  * Core interfaces for the composable registry system
3
3
  */
4
4
 
5
+ import type * as plugins from '../plugins.js';
6
+
5
7
  /**
6
8
  * Registry protocol types
7
9
  */
@@ -40,14 +42,9 @@ export interface ICredentials {
40
42
 
41
43
  /**
42
44
  * Storage backend configuration
45
+ * Extends IS3Descriptor from @tsclass/tsclass with bucketName
43
46
  */
44
- export interface IStorageConfig {
45
- accessKey: string;
46
- accessSecret: string;
47
- endpoint: string;
48
- port?: number;
49
- useSsl?: boolean;
50
- region?: string;
47
+ export interface IStorageConfig extends plugins.tsclass.storage.IS3Descriptor {
51
48
  bucketName: string;
52
49
  }
53
50
 
package/ts/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @push.rocks/smartregistry
3
- * Composable registry supporting OCI, NPM, Maven, Cargo, and Composer protocols
3
+ * Composable registry supporting OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems protocols
4
4
  */
5
5
 
6
6
  // Main orchestrator
@@ -23,3 +23,9 @@ export * from './cargo/index.js';
23
23
 
24
24
  // Composer Registry
25
25
  export * from './composer/index.js';
26
+
27
+ // PyPI Registry
28
+ export * from './pypi/index.js';
29
+
30
+ // RubyGems Registry
31
+ export * from './rubygems/index.js';
package/ts/plugins.ts CHANGED
@@ -9,3 +9,8 @@ import * as smartlog from '@push.rocks/smartlog';
9
9
  import * as smartpath from '@push.rocks/smartpath';
10
10
 
11
11
  export { smartbucket, smartlog, smartpath };
12
+
13
+ // @tsclass scope
14
+ import * as tsclass from '@tsclass/tsclass';
15
+
16
+ export { tsclass };
@@ -351,22 +351,38 @@ export class PypiRegistry extends BaseRegistry {
351
351
  return this.errorResponse(403, 'Insufficient permissions');
352
352
  }
353
353
 
354
- // Calculate hashes
354
+ // Calculate and verify hashes
355
355
  const hashes: Record<string, string> = {};
356
356
 
357
- if (formData.sha256_digest) {
358
- hashes.sha256 = formData.sha256_digest;
359
- } else {
360
- hashes.sha256 = await helpers.calculateHash(fileData, 'sha256');
357
+ // Always calculate SHA256
358
+ const actualSha256 = await helpers.calculateHash(fileData, 'sha256');
359
+ hashes.sha256 = actualSha256;
360
+
361
+ // Verify client-provided SHA256 if present
362
+ if (formData.sha256_digest && formData.sha256_digest !== actualSha256) {
363
+ return this.errorResponse(400, 'SHA256 hash mismatch');
361
364
  }
362
365
 
366
+ // Calculate MD5 if requested
363
367
  if (formData.md5_digest) {
364
- // MD5 digest in PyPI is urlsafe base64, convert to hex
365
- hashes.md5 = await helpers.calculateHash(fileData, 'md5');
368
+ const actualMd5 = await helpers.calculateHash(fileData, 'md5');
369
+ hashes.md5 = actualMd5;
370
+
371
+ // Verify if client provided MD5
372
+ if (formData.md5_digest !== actualMd5) {
373
+ return this.errorResponse(400, 'MD5 hash mismatch');
374
+ }
366
375
  }
367
376
 
377
+ // Calculate Blake2b if requested
368
378
  if (formData.blake2_256_digest) {
369
- hashes.blake2b = formData.blake2_256_digest;
379
+ const actualBlake2b = await helpers.calculateHash(fileData, 'blake2b');
380
+ hashes.blake2b = actualBlake2b;
381
+
382
+ // Verify if client provided Blake2b
383
+ if (formData.blake2_256_digest !== actualBlake2b) {
384
+ return this.errorResponse(400, 'Blake2b hash mismatch');
385
+ }
370
386
  }
371
387
 
372
388
  // Store file