@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.
Files changed (43) hide show
  1. package/dist_ts/00_commitinfo_data.js +3 -3
  2. package/dist_ts/classes.smartregistry.d.ts +2 -2
  3. package/dist_ts/classes.smartregistry.js +37 -2
  4. package/dist_ts/core/classes.authmanager.d.ts +55 -1
  5. package/dist_ts/core/classes.authmanager.js +138 -3
  6. package/dist_ts/core/classes.registrystorage.d.ts +145 -0
  7. package/dist_ts/core/classes.registrystorage.js +392 -1
  8. package/dist_ts/core/interfaces.core.d.ts +13 -1
  9. package/dist_ts/index.d.ts +3 -1
  10. package/dist_ts/index.js +6 -2
  11. package/dist_ts/pypi/classes.pypiregistry.d.ts +70 -0
  12. package/dist_ts/pypi/classes.pypiregistry.js +482 -0
  13. package/dist_ts/pypi/helpers.pypi.d.ts +84 -0
  14. package/dist_ts/pypi/helpers.pypi.js +263 -0
  15. package/dist_ts/pypi/index.d.ts +7 -0
  16. package/dist_ts/pypi/index.js +8 -0
  17. package/dist_ts/pypi/interfaces.pypi.d.ts +301 -0
  18. package/dist_ts/pypi/interfaces.pypi.js +6 -0
  19. package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +86 -0
  20. package/dist_ts/rubygems/classes.rubygemsregistry.js +475 -0
  21. package/dist_ts/rubygems/helpers.rubygems.d.ts +143 -0
  22. package/dist_ts/rubygems/helpers.rubygems.js +312 -0
  23. package/dist_ts/rubygems/index.d.ts +7 -0
  24. package/dist_ts/rubygems/index.js +8 -0
  25. package/dist_ts/rubygems/interfaces.rubygems.d.ts +236 -0
  26. package/dist_ts/rubygems/interfaces.rubygems.js +6 -0
  27. package/package.json +2 -2
  28. package/readme.hints.md +438 -2
  29. package/readme.md +288 -13
  30. package/ts/00_commitinfo_data.ts +2 -2
  31. package/ts/classes.smartregistry.ts +41 -2
  32. package/ts/core/classes.authmanager.ts +161 -2
  33. package/ts/core/classes.registrystorage.ts +463 -0
  34. package/ts/core/interfaces.core.ts +13 -1
  35. package/ts/index.ts +7 -1
  36. package/ts/pypi/classes.pypiregistry.ts +580 -0
  37. package/ts/pypi/helpers.pypi.ts +299 -0
  38. package/ts/pypi/index.ts +8 -0
  39. package/ts/pypi/interfaces.pypi.ts +316 -0
  40. package/ts/rubygems/classes.rubygemsregistry.ts +598 -0
  41. package/ts/rubygems/helpers.rubygems.ts +398 -0
  42. package/ts/rubygems/index.ts +8 -0
  43. package/ts/rubygems/interfaces.rubygems.ts +251 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Helper functions for PyPI registry
3
+ * Package name normalization, HTML generation, etc.
4
+ */
5
+
6
+ import type { IPypiFile, IPypiPackageMetadata } from './interfaces.pypi.js';
7
+
8
+ /**
9
+ * Normalize package name according to PEP 503
10
+ * Lowercase and replace runs of [._-] with a single dash
11
+ * @param name - Package name
12
+ * @returns Normalized name
13
+ */
14
+ export function normalizePypiPackageName(name: string): string {
15
+ return name
16
+ .toLowerCase()
17
+ .replace(/[-_.]+/g, '-');
18
+ }
19
+
20
+ /**
21
+ * Escape HTML special characters to prevent XSS
22
+ * @param str - String to escape
23
+ * @returns Escaped string
24
+ */
25
+ export function escapeHtml(str: string): string {
26
+ return str
27
+ .replace(/&/g, '&')
28
+ .replace(/</g, '&lt;')
29
+ .replace(/>/g, '&gt;')
30
+ .replace(/"/g, '&quot;')
31
+ .replace(/'/g, '&#039;');
32
+ }
33
+
34
+ /**
35
+ * Generate PEP 503 compliant HTML for root index (all packages)
36
+ * @param packages - List of package names
37
+ * @returns HTML string
38
+ */
39
+ export function generateSimpleRootHtml(packages: string[]): string {
40
+ const links = packages
41
+ .map(pkg => {
42
+ const normalized = normalizePypiPackageName(pkg);
43
+ return ` <a href="${escapeHtml(normalized)}/">${escapeHtml(pkg)}</a>`;
44
+ })
45
+ .join('\n');
46
+
47
+ return `<!DOCTYPE html>
48
+ <html>
49
+ <head>
50
+ <meta name="pypi:repository-version" content="1.0">
51
+ <title>Simple Index</title>
52
+ </head>
53
+ <body>
54
+ <h1>Simple Index</h1>
55
+ ${links}
56
+ </body>
57
+ </html>`;
58
+ }
59
+
60
+ /**
61
+ * Generate PEP 503 compliant HTML for package index (file list)
62
+ * @param packageName - Package name (normalized)
63
+ * @param files - List of files
64
+ * @param baseUrl - Base URL for downloads
65
+ * @returns HTML string
66
+ */
67
+ export function generateSimplePackageHtml(
68
+ packageName: string,
69
+ files: IPypiFile[],
70
+ baseUrl: string
71
+ ): string {
72
+ const links = files
73
+ .map(file => {
74
+ // Build URL
75
+ let url = file.url;
76
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
77
+ // Relative URL - make it absolute
78
+ url = `${baseUrl}/packages/${packageName}/${file.filename}`;
79
+ }
80
+
81
+ // Add hash fragment
82
+ const hashName = Object.keys(file.hashes)[0];
83
+ const hashValue = file.hashes[hashName];
84
+ const fragment = hashName && hashValue ? `#${hashName}=${hashValue}` : '';
85
+
86
+ // Build data attributes
87
+ const dataAttrs: string[] = [];
88
+
89
+ if (file['requires-python']) {
90
+ const escaped = escapeHtml(file['requires-python']);
91
+ dataAttrs.push(`data-requires-python="${escaped}"`);
92
+ }
93
+
94
+ if (file['gpg-sig'] !== undefined) {
95
+ dataAttrs.push(`data-gpg-sig="${file['gpg-sig'] ? 'true' : 'false'}"`);
96
+ }
97
+
98
+ if (file.yanked) {
99
+ const reason = typeof file.yanked === 'string' ? file.yanked : '';
100
+ if (reason) {
101
+ dataAttrs.push(`data-yanked="${escapeHtml(reason)}"`);
102
+ } else {
103
+ dataAttrs.push(`data-yanked=""`);
104
+ }
105
+ }
106
+
107
+ const dataAttrStr = dataAttrs.length > 0 ? ' ' + dataAttrs.join(' ') : '';
108
+
109
+ return ` <a href="${escapeHtml(url)}${fragment}"${dataAttrStr}>${escapeHtml(file.filename)}</a>`;
110
+ })
111
+ .join('\n');
112
+
113
+ return `<!DOCTYPE html>
114
+ <html>
115
+ <head>
116
+ <meta name="pypi:repository-version" content="1.0">
117
+ <title>Links for ${escapeHtml(packageName)}</title>
118
+ </head>
119
+ <body>
120
+ <h1>Links for ${escapeHtml(packageName)}</h1>
121
+ ${links}
122
+ </body>
123
+ </html>`;
124
+ }
125
+
126
+ /**
127
+ * Parse filename to extract package info
128
+ * Supports wheel and sdist formats
129
+ * @param filename - Package filename
130
+ * @returns Parsed info or null
131
+ */
132
+ export function parsePackageFilename(filename: string): {
133
+ name: string;
134
+ version: string;
135
+ filetype: 'bdist_wheel' | 'sdist';
136
+ pythonVersion?: string;
137
+ } | null {
138
+ // Wheel format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
139
+ const wheelMatch = filename.match(/^([a-zA-Z0-9_.-]+?)-([a-zA-Z0-9_.]+?)(?:-(\d+))?-([^-]+)-([^-]+)-([^-]+)\.whl$/);
140
+ if (wheelMatch) {
141
+ return {
142
+ name: wheelMatch[1],
143
+ version: wheelMatch[2],
144
+ filetype: 'bdist_wheel',
145
+ pythonVersion: wheelMatch[4],
146
+ };
147
+ }
148
+
149
+ // Sdist tar.gz format: {name}-{version}.tar.gz
150
+ const sdistTarMatch = filename.match(/^([a-zA-Z0-9_.-]+?)-([a-zA-Z0-9_.]+)\.tar\.gz$/);
151
+ if (sdistTarMatch) {
152
+ return {
153
+ name: sdistTarMatch[1],
154
+ version: sdistTarMatch[2],
155
+ filetype: 'sdist',
156
+ pythonVersion: 'source',
157
+ };
158
+ }
159
+
160
+ // Sdist zip format: {name}-{version}.zip
161
+ const sdistZipMatch = filename.match(/^([a-zA-Z0-9_.-]+?)-([a-zA-Z0-9_.]+)\.zip$/);
162
+ if (sdistZipMatch) {
163
+ return {
164
+ name: sdistZipMatch[1],
165
+ version: sdistZipMatch[2],
166
+ filetype: 'sdist',
167
+ pythonVersion: 'source',
168
+ };
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Calculate hash digest for a buffer
176
+ * @param data - Data to hash
177
+ * @param algorithm - Hash algorithm (sha256, md5, blake2b)
178
+ * @returns Hex-encoded hash
179
+ */
180
+ export async function calculateHash(data: Buffer, algorithm: 'sha256' | 'md5' | 'blake2b'): Promise<string> {
181
+ const crypto = await import('crypto');
182
+
183
+ let hash: any;
184
+ if (algorithm === 'blake2b') {
185
+ // Node.js uses 'blake2b512' for blake2b
186
+ hash = crypto.createHash('blake2b512');
187
+ } else {
188
+ hash = crypto.createHash(algorithm);
189
+ }
190
+
191
+ hash.update(data);
192
+ return hash.digest('hex');
193
+ }
194
+
195
+ /**
196
+ * Validate package name
197
+ * Must contain only ASCII letters, numbers, ., -, and _
198
+ * @param name - Package name
199
+ * @returns true if valid
200
+ */
201
+ export function isValidPackageName(name: string): boolean {
202
+ return /^[a-zA-Z0-9._-]+$/.test(name);
203
+ }
204
+
205
+ /**
206
+ * Validate version string (basic check)
207
+ * @param version - Version string
208
+ * @returns true if valid
209
+ */
210
+ export function isValidVersion(version: string): boolean {
211
+ // Basic check - allows numbers, letters, dots, hyphens, underscores
212
+ // More strict validation would follow PEP 440
213
+ return /^[a-zA-Z0-9._-]+$/.test(version);
214
+ }
215
+
216
+ /**
217
+ * Extract metadata from package metadata
218
+ * Filters and normalizes metadata fields
219
+ * @param metadata - Raw metadata object
220
+ * @returns Filtered metadata
221
+ */
222
+ export function extractCoreMetadata(metadata: Record<string, any>): Record<string, any> {
223
+ const coreFields = [
224
+ 'metadata-version',
225
+ 'name',
226
+ 'version',
227
+ 'platform',
228
+ 'supported-platform',
229
+ 'summary',
230
+ 'description',
231
+ 'description-content-type',
232
+ 'keywords',
233
+ 'home-page',
234
+ 'download-url',
235
+ 'author',
236
+ 'author-email',
237
+ 'maintainer',
238
+ 'maintainer-email',
239
+ 'license',
240
+ 'classifier',
241
+ 'requires-python',
242
+ 'requires-dist',
243
+ 'requires-external',
244
+ 'provides-dist',
245
+ 'project-url',
246
+ 'provides-extra',
247
+ ];
248
+
249
+ const result: Record<string, any> = {};
250
+
251
+ for (const [key, value] of Object.entries(metadata)) {
252
+ const normalizedKey = key.toLowerCase().replace(/_/g, '-');
253
+ if (coreFields.includes(normalizedKey)) {
254
+ result[normalizedKey] = value;
255
+ }
256
+ }
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Generate JSON API response for package list (PEP 691)
263
+ * @param packages - List of package names
264
+ * @returns JSON object
265
+ */
266
+ export function generateJsonRootResponse(packages: string[]): any {
267
+ return {
268
+ meta: {
269
+ 'api-version': '1.0',
270
+ },
271
+ projects: packages.map(name => ({ name })),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Generate JSON API response for package files (PEP 691)
277
+ * @param packageName - Package name (normalized)
278
+ * @param files - List of files
279
+ * @returns JSON object
280
+ */
281
+ export function generateJsonPackageResponse(packageName: string, files: IPypiFile[]): any {
282
+ return {
283
+ meta: {
284
+ 'api-version': '1.0',
285
+ },
286
+ name: packageName,
287
+ files: files.map(file => ({
288
+ filename: file.filename,
289
+ url: file.url,
290
+ hashes: file.hashes,
291
+ 'requires-python': file['requires-python'],
292
+ 'dist-info-metadata': file['dist-info-metadata'],
293
+ 'gpg-sig': file['gpg-sig'],
294
+ yanked: file.yanked,
295
+ size: file.size,
296
+ 'upload-time': file['upload-time'],
297
+ })),
298
+ };
299
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * PyPI Registry Module
3
+ * Python Package Index implementation
4
+ */
5
+
6
+ export * from './interfaces.pypi.js';
7
+ export * from './classes.pypiregistry.js';
8
+ export * as pypiHelpers from './helpers.pypi.js';
@@ -0,0 +1,316 @@
1
+ /**
2
+ * PyPI Registry Type Definitions
3
+ * Compliant with PEP 503 (Simple API), PEP 691 (JSON API), and PyPI upload API
4
+ */
5
+
6
+ /**
7
+ * File information for a package distribution
8
+ * Used in both PEP 503 HTML and PEP 691 JSON responses
9
+ */
10
+ export interface IPypiFile {
11
+ /** Filename (e.g., "package-1.0.0-py3-none-any.whl") */
12
+ filename: string;
13
+ /** Download URL (absolute or relative) */
14
+ url: string;
15
+ /** Hash digests (multiple algorithms supported in JSON) */
16
+ hashes: Record<string, string>;
17
+ /** Python version requirement (PEP 345 format) */
18
+ 'requires-python'?: string;
19
+ /** Whether distribution info metadata is available (PEP 658) */
20
+ 'dist-info-metadata'?: boolean | { sha256: string };
21
+ /** Whether GPG signature is available */
22
+ 'gpg-sig'?: boolean;
23
+ /** Yank status: false or reason string */
24
+ yanked?: boolean | string;
25
+ /** File size in bytes */
26
+ size?: number;
27
+ /** Upload timestamp */
28
+ 'upload-time'?: string;
29
+ }
30
+
31
+ /**
32
+ * Package metadata stored internally
33
+ * Consolidated from multiple file uploads
34
+ */
35
+ export interface IPypiPackageMetadata {
36
+ /** Normalized package name */
37
+ name: string;
38
+ /** Map of version to file list */
39
+ versions: Record<string, IPypiVersionMetadata>;
40
+ /** Timestamp of last update */
41
+ 'last-modified'?: string;
42
+ }
43
+
44
+ /**
45
+ * Metadata for a specific version
46
+ */
47
+ export interface IPypiVersionMetadata {
48
+ /** Version string */
49
+ version: string;
50
+ /** Files for this version (wheels, sdists) */
51
+ files: IPypiFileMetadata[];
52
+ /** Core metadata fields */
53
+ metadata?: IPypiCoreMetadata;
54
+ /** Whether entire version is yanked */
55
+ yanked?: boolean | string;
56
+ /** Upload timestamp */
57
+ 'upload-time'?: string;
58
+ }
59
+
60
+ /**
61
+ * Internal file metadata
62
+ */
63
+ export interface IPypiFileMetadata {
64
+ filename: string;
65
+ /** Storage key/path */
66
+ path: string;
67
+ /** File type: bdist_wheel or sdist */
68
+ filetype: 'bdist_wheel' | 'sdist';
69
+ /** Python version tag */
70
+ python_version: string;
71
+ /** Hash digests */
72
+ hashes: Record<string, string>;
73
+ /** File size in bytes */
74
+ size: number;
75
+ /** Python version requirement */
76
+ 'requires-python'?: string;
77
+ /** Whether this file is yanked */
78
+ yanked?: boolean | string;
79
+ /** Upload timestamp */
80
+ 'upload-time': string;
81
+ /** Uploader user ID */
82
+ 'uploaded-by': string;
83
+ }
84
+
85
+ /**
86
+ * Core metadata fields (subset of PEP 566)
87
+ * These are extracted from package uploads
88
+ */
89
+ export interface IPypiCoreMetadata {
90
+ /** Metadata version */
91
+ 'metadata-version': string;
92
+ /** Package name */
93
+ name: string;
94
+ /** Version string */
95
+ version: string;
96
+ /** Platform compatibility */
97
+ platform?: string;
98
+ /** Supported platforms */
99
+ 'supported-platform'?: string;
100
+ /** Summary/description */
101
+ summary?: string;
102
+ /** Long description */
103
+ description?: string;
104
+ /** Description content type (text/plain, text/markdown, text/x-rst) */
105
+ 'description-content-type'?: string;
106
+ /** Keywords */
107
+ keywords?: string;
108
+ /** Homepage URL */
109
+ 'home-page'?: string;
110
+ /** Download URL */
111
+ 'download-url'?: string;
112
+ /** Author name */
113
+ author?: string;
114
+ /** Author email */
115
+ 'author-email'?: string;
116
+ /** Maintainer name */
117
+ maintainer?: string;
118
+ /** Maintainer email */
119
+ 'maintainer-email'?: string;
120
+ /** License */
121
+ license?: string;
122
+ /** Classifiers (Trove classifiers) */
123
+ classifier?: string[];
124
+ /** Python version requirement */
125
+ 'requires-python'?: string;
126
+ /** Dist name requirement */
127
+ 'requires-dist'?: string[];
128
+ /** External requirement */
129
+ 'requires-external'?: string[];
130
+ /** Provides dist */
131
+ 'provides-dist'?: string[];
132
+ /** Project URLs */
133
+ 'project-url'?: string[];
134
+ /** Provides extra */
135
+ 'provides-extra'?: string[];
136
+ }
137
+
138
+ /**
139
+ * PEP 503: Simple API root response (project list)
140
+ */
141
+ export interface IPypiSimpleRootHtml {
142
+ /** List of project names */
143
+ projects: string[];
144
+ }
145
+
146
+ /**
147
+ * PEP 503: Simple API project response (file list)
148
+ */
149
+ export interface IPypiSimpleProjectHtml {
150
+ /** Normalized project name */
151
+ name: string;
152
+ /** List of files */
153
+ files: IPypiFile[];
154
+ }
155
+
156
+ /**
157
+ * PEP 691: JSON API root response
158
+ */
159
+ export interface IPypiJsonRoot {
160
+ /** API metadata */
161
+ meta: {
162
+ /** API version (e.g., "1.0") */
163
+ 'api-version': string;
164
+ };
165
+ /** List of projects */
166
+ projects: Array<{
167
+ /** Project name */
168
+ name: string;
169
+ }>;
170
+ }
171
+
172
+ /**
173
+ * PEP 691: JSON API project response
174
+ */
175
+ export interface IPypiJsonProject {
176
+ /** Normalized project name */
177
+ name: string;
178
+ /** API metadata */
179
+ meta: {
180
+ /** API version (e.g., "1.0") */
181
+ 'api-version': string;
182
+ };
183
+ /** List of files */
184
+ files: IPypiFile[];
185
+ }
186
+
187
+ /**
188
+ * Upload form data (multipart/form-data fields)
189
+ * Based on PyPI legacy upload API
190
+ */
191
+ export interface IPypiUploadForm {
192
+ /** Action type (always "file_upload") */
193
+ ':action': 'file_upload';
194
+ /** Protocol version (always "1") */
195
+ protocol_version: '1';
196
+ /** File content (binary) */
197
+ content: Buffer;
198
+ /** File type */
199
+ filetype: 'bdist_wheel' | 'sdist';
200
+ /** Python version tag */
201
+ pyversion: string;
202
+ /** Package name */
203
+ name: string;
204
+ /** Version string */
205
+ version: string;
206
+ /** Metadata version */
207
+ metadata_version: string;
208
+ /** Hash digests (at least one required) */
209
+ md5_digest?: string;
210
+ sha256_digest?: string;
211
+ blake2_256_digest?: string;
212
+ /** Optional attestations */
213
+ attestations?: string; // JSON array
214
+ /** Optional core metadata fields */
215
+ summary?: string;
216
+ description?: string;
217
+ description_content_type?: string;
218
+ author?: string;
219
+ author_email?: string;
220
+ maintainer?: string;
221
+ maintainer_email?: string;
222
+ license?: string;
223
+ keywords?: string;
224
+ home_page?: string;
225
+ download_url?: string;
226
+ requires_python?: string;
227
+ classifiers?: string[];
228
+ platform?: string;
229
+ [key: string]: any; // Allow additional metadata fields
230
+ }
231
+
232
+ /**
233
+ * JSON API upload response
234
+ */
235
+ export interface IPypiUploadResponse {
236
+ /** Success message */
237
+ message?: string;
238
+ /** URL of uploaded file */
239
+ url?: string;
240
+ }
241
+
242
+ /**
243
+ * Error response structure
244
+ */
245
+ export interface IPypiError {
246
+ /** Error message */
247
+ message: string;
248
+ /** HTTP status code */
249
+ status?: number;
250
+ /** Additional error details */
251
+ details?: string[];
252
+ }
253
+
254
+ /**
255
+ * Search query parameters
256
+ */
257
+ export interface IPypiSearchQuery {
258
+ /** Search term */
259
+ q?: string;
260
+ /** Page number */
261
+ page?: number;
262
+ /** Results per page */
263
+ per_page?: number;
264
+ }
265
+
266
+ /**
267
+ * Search result for a single package
268
+ */
269
+ export interface IPypiSearchResult {
270
+ /** Package name */
271
+ name: string;
272
+ /** Latest version */
273
+ version: string;
274
+ /** Summary */
275
+ summary: string;
276
+ /** Description */
277
+ description?: string;
278
+ }
279
+
280
+ /**
281
+ * Search response structure
282
+ */
283
+ export interface IPypiSearchResponse {
284
+ /** Search results */
285
+ results: IPypiSearchResult[];
286
+ /** Result count */
287
+ count: number;
288
+ /** Current page */
289
+ page: number;
290
+ /** Total pages */
291
+ pages: number;
292
+ }
293
+
294
+ /**
295
+ * Yank request
296
+ */
297
+ export interface IPypiYankRequest {
298
+ /** Package name */
299
+ name: string;
300
+ /** Version to yank */
301
+ version: string;
302
+ /** Optional filename (specific file) */
303
+ filename?: string;
304
+ /** Reason for yanking */
305
+ reason?: string;
306
+ }
307
+
308
+ /**
309
+ * Yank response
310
+ */
311
+ export interface IPypiYankResponse {
312
+ /** Success indicator */
313
+ success: boolean;
314
+ /** Message */
315
+ message?: string;
316
+ }