@push.rocks/smartregistry 1.4.0 → 1.5.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/composer/classes.composerregistry.js +3 -17
- 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 +68 -0
- package/dist_ts/core/classes.registrystorage.js +195 -1
- package/dist_ts/core/interfaces.core.d.ts +13 -1
- package/dist_ts/pypi/classes.pypiregistry.d.ts +70 -0
- package/dist_ts/pypi/classes.pypiregistry.js +470 -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/package.json +1 -1
- package/readme.hints.md +333 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/composer/classes.composerregistry.ts +2 -18
- package/ts/core/classes.authmanager.ts +161 -2
- package/ts/core/classes.registrystorage.ts +227 -0
- package/ts/core/interfaces.core.ts +13 -1
- package/ts/pypi/classes.pypiregistry.ts +564 -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/readme.hints.md
CHANGED
|
@@ -1,3 +1,335 @@
|
|
|
1
1
|
# Project Readme Hints
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Python (PyPI) Protocol Implementation Notes
|
|
4
|
+
|
|
5
|
+
### PEP 503: Simple Repository API (HTML-based)
|
|
6
|
+
|
|
7
|
+
**URL Structure:**
|
|
8
|
+
- Root: `/<base>/` - Lists all projects
|
|
9
|
+
- Project: `/<base>/<project>/` - Lists all files for a project
|
|
10
|
+
- All URLs MUST end with `/` (redirect if missing)
|
|
11
|
+
|
|
12
|
+
**Package Name Normalization:**
|
|
13
|
+
- Lowercase all characters
|
|
14
|
+
- Replace runs of `.`, `-`, `_` with single `-`
|
|
15
|
+
- Implementation: `re.sub(r"[-_.]+", "-", name).lower()`
|
|
16
|
+
|
|
17
|
+
**HTML Format:**
|
|
18
|
+
- Root: One anchor per project
|
|
19
|
+
- Project: One anchor per file
|
|
20
|
+
- Anchor text must match final filename
|
|
21
|
+
- Anchor href links to download URL
|
|
22
|
+
|
|
23
|
+
**Hash Fragments:**
|
|
24
|
+
Format: `#<hashname>=<hashvalue>`
|
|
25
|
+
- hashname: lowercase hash function name (recommend `sha256`)
|
|
26
|
+
- hashvalue: hex-encoded digest
|
|
27
|
+
|
|
28
|
+
**Data Attributes:**
|
|
29
|
+
- `data-gpg-sig`: `true`/`false` for GPG signature presence
|
|
30
|
+
- `data-requires-python`: PEP 345 requirement string (HTML-encode `<` as `<`, `>` as `>`)
|
|
31
|
+
|
|
32
|
+
### PEP 691: JSON-based Simple API
|
|
33
|
+
|
|
34
|
+
**Content Types:**
|
|
35
|
+
- `application/vnd.pypi.simple.v1+json` - JSON format
|
|
36
|
+
- `application/vnd.pypi.simple.v1+html` - HTML format
|
|
37
|
+
- `text/html` - Alias for HTML (backwards compat)
|
|
38
|
+
|
|
39
|
+
**Root Endpoint JSON:**
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"meta": {"api-version": "1.0"},
|
|
43
|
+
"projects": [{"name": "ProjectName"}]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Project Endpoint JSON:**
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "normalized-name",
|
|
51
|
+
"meta": {"api-version": "1.0"},
|
|
52
|
+
"files": [
|
|
53
|
+
{
|
|
54
|
+
"filename": "package-1.0-py3-none-any.whl",
|
|
55
|
+
"url": "https://example.com/path/to/file",
|
|
56
|
+
"hashes": {"sha256": "..."},
|
|
57
|
+
"requires-python": ">=3.7",
|
|
58
|
+
"dist-info-metadata": true | {"sha256": "..."},
|
|
59
|
+
"gpg-sig": true,
|
|
60
|
+
"yanked": false | "reason string"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Content Negotiation:**
|
|
67
|
+
- Use `Accept` header for format selection
|
|
68
|
+
- Server responds with `Content-Type` header
|
|
69
|
+
- Support both JSON and HTML formats
|
|
70
|
+
|
|
71
|
+
### PyPI Upload API (Legacy /legacy/)
|
|
72
|
+
|
|
73
|
+
**Endpoint:**
|
|
74
|
+
- URL: `https://upload.pypi.org/legacy/`
|
|
75
|
+
- Method: `POST`
|
|
76
|
+
- Content-Type: `multipart/form-data`
|
|
77
|
+
|
|
78
|
+
**Required Form Fields:**
|
|
79
|
+
- `:action` = `file_upload`
|
|
80
|
+
- `protocol_version` = `1`
|
|
81
|
+
- `content` = Binary file data with filename
|
|
82
|
+
- `filetype` = `bdist_wheel` | `sdist`
|
|
83
|
+
- `pyversion` = Python tag (e.g., `py3`, `py2.py3`) or `source` for sdist
|
|
84
|
+
- `metadata_version` = Metadata standard version
|
|
85
|
+
- `name` = Package name
|
|
86
|
+
- `version` = Version string
|
|
87
|
+
|
|
88
|
+
**Hash Digest (one required):**
|
|
89
|
+
- `md5_digest`: urlsafe base64 without padding
|
|
90
|
+
- `sha256_digest`: hexadecimal
|
|
91
|
+
- `blake2_256_digest`: hexadecimal
|
|
92
|
+
|
|
93
|
+
**Optional Fields:**
|
|
94
|
+
- `attestations`: JSON array of attestation objects
|
|
95
|
+
- Any Core Metadata fields (lowercase, hyphens → underscores)
|
|
96
|
+
- Example: `Description-Content-Type` → `description_content_type`
|
|
97
|
+
|
|
98
|
+
**Authentication:**
|
|
99
|
+
- Username/password or API token in HTTP Basic Auth
|
|
100
|
+
- API tokens: username = `__token__`, password = token value
|
|
101
|
+
|
|
102
|
+
**Behavior:**
|
|
103
|
+
- First file uploaded creates the release
|
|
104
|
+
- Multiple files uploaded sequentially for same version
|
|
105
|
+
|
|
106
|
+
### PEP 694: Upload 2.0 API
|
|
107
|
+
|
|
108
|
+
**Status:** Draft (not yet required, legacy API still supported)
|
|
109
|
+
- Multi-step workflow with sessions
|
|
110
|
+
- Async upload support with resumption
|
|
111
|
+
- JSON-based API
|
|
112
|
+
- Standard HTTP auth (RFC 7235)
|
|
113
|
+
- Not implementing initially (legacy API sufficient)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Ruby (RubyGems) Protocol Implementation Notes
|
|
118
|
+
|
|
119
|
+
### Compact Index Format
|
|
120
|
+
|
|
121
|
+
**Endpoints:**
|
|
122
|
+
- `/versions` - Master list of all gems and versions
|
|
123
|
+
- `/info/<RUBYGEM>` - Detailed info for specific gem
|
|
124
|
+
- `/names` - Simple list of gem names
|
|
125
|
+
|
|
126
|
+
**Authentication:**
|
|
127
|
+
- UUID tokens similar to NPM pattern
|
|
128
|
+
- API key in `Authorization` header
|
|
129
|
+
- Scope format: `rubygems:gem:{name}:{read|write|yank}`
|
|
130
|
+
|
|
131
|
+
### `/versions` File Format
|
|
132
|
+
|
|
133
|
+
**Structure:**
|
|
134
|
+
```
|
|
135
|
+
created_at: 2024-04-01T00:00:05Z
|
|
136
|
+
---
|
|
137
|
+
RUBYGEM [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Details:**
|
|
141
|
+
- Metadata lines before `---` delimiter
|
|
142
|
+
- One line per gem with comma-separated versions
|
|
143
|
+
- `[-]` prefix indicates yanked version
|
|
144
|
+
- `MD5`: Checksum of corresponding `/info/<RUBYGEM>` file
|
|
145
|
+
- Append-only during month, recalculated monthly
|
|
146
|
+
|
|
147
|
+
### `/info/<RUBYGEM>` File Format
|
|
148
|
+
|
|
149
|
+
**Structure:**
|
|
150
|
+
```
|
|
151
|
+
---
|
|
152
|
+
VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Dependency Format:**
|
|
156
|
+
```
|
|
157
|
+
GEM:CONSTRAINT[&CONSTRAINT]
|
|
158
|
+
```
|
|
159
|
+
- Examples: `actionmailer:= 2.2.2`, `parser:>= 3.2.2.3`
|
|
160
|
+
- Operators: `=`, `>`, `<`, `>=`, `<=`, `~>`, `!=`
|
|
161
|
+
- Multiple constraints: `unicode-display_width:< 3.0&>= 2.4.0`
|
|
162
|
+
|
|
163
|
+
**Requirement Format:**
|
|
164
|
+
```
|
|
165
|
+
checksum:SHA256_HEX
|
|
166
|
+
ruby:CONSTRAINT
|
|
167
|
+
rubygems:CONSTRAINT
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Platform:**
|
|
171
|
+
- Default platform is `ruby`
|
|
172
|
+
- Non-default platforms: `VERSION-PLATFORM` (e.g., `3.2.1-arm64-darwin`)
|
|
173
|
+
|
|
174
|
+
**Yanked Gems:**
|
|
175
|
+
- Listed with `-` prefix in `/versions`
|
|
176
|
+
- Excluded entirely from `/info/<RUBYGEM>` file
|
|
177
|
+
|
|
178
|
+
### `/names` File Format
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
---
|
|
182
|
+
gemname1
|
|
183
|
+
gemname2
|
|
184
|
+
gemname3
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### HTTP Range Support
|
|
188
|
+
|
|
189
|
+
**Headers:**
|
|
190
|
+
- `Range: bytes=#{start}-`: Request from byte position
|
|
191
|
+
- `If-None-Match`: ETag conditional request
|
|
192
|
+
- `Repr-Digest`: SHA256 checksum in response
|
|
193
|
+
|
|
194
|
+
**Caching Strategy:**
|
|
195
|
+
1. Store file with last byte position
|
|
196
|
+
2. Request range from last position
|
|
197
|
+
3. Append response to existing file
|
|
198
|
+
4. Verify SHA256 against `Repr-Digest`
|
|
199
|
+
|
|
200
|
+
### RubyGems Upload/Management API
|
|
201
|
+
|
|
202
|
+
**Upload Gem:**
|
|
203
|
+
- `POST /api/v1/gems`
|
|
204
|
+
- Binary `.gem` file in request body
|
|
205
|
+
- `Authorization` header with API key
|
|
206
|
+
|
|
207
|
+
**Yank Version:**
|
|
208
|
+
- `DELETE /api/v1/gems/yank`
|
|
209
|
+
- Parameters: `gem_name`, `version`
|
|
210
|
+
|
|
211
|
+
**Unyank Version:**
|
|
212
|
+
- `PUT /api/v1/gems/unyank`
|
|
213
|
+
- Parameters: `gem_name`, `version`
|
|
214
|
+
|
|
215
|
+
**Version Metadata:**
|
|
216
|
+
- `GET /api/v1/versions/<gem>.json`
|
|
217
|
+
- Returns JSON array of versions
|
|
218
|
+
|
|
219
|
+
**Dependencies:**
|
|
220
|
+
- `GET /api/v1/dependencies?gems=<comma-list>`
|
|
221
|
+
- Returns dependency information for resolution
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Implementation Strategy
|
|
226
|
+
|
|
227
|
+
### Storage Paths
|
|
228
|
+
|
|
229
|
+
**PyPI:**
|
|
230
|
+
```
|
|
231
|
+
pypi/
|
|
232
|
+
├── simple/ # PEP 503 HTML files
|
|
233
|
+
│ ├── index.html # All packages list
|
|
234
|
+
│ └── {package}/index.html # Package versions list
|
|
235
|
+
├── packages/
|
|
236
|
+
│ └── {package}/{filename} # .whl and .tar.gz files
|
|
237
|
+
└── metadata/
|
|
238
|
+
└── {package}/metadata.json # Package metadata
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**RubyGems:**
|
|
242
|
+
```
|
|
243
|
+
rubygems/
|
|
244
|
+
├── versions # Master versions file
|
|
245
|
+
├── info/{gemname} # Per-gem info files
|
|
246
|
+
├── names # All gem names
|
|
247
|
+
└── gems/{gemname}-{version}.gem # .gem files
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Authentication Pattern
|
|
251
|
+
|
|
252
|
+
Both protocols should follow the existing UUID token pattern used by NPM, Maven, Cargo, Composer:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// AuthManager additions
|
|
256
|
+
createPypiToken(userId: string, readonly: boolean): string
|
|
257
|
+
validatePypiToken(token: string): ITokenInfo | null
|
|
258
|
+
revokePypiToken(token: string): boolean
|
|
259
|
+
|
|
260
|
+
createRubyGemsToken(userId: string, readonly: boolean): string
|
|
261
|
+
validateRubyGemsToken(token: string): ITokenInfo | null
|
|
262
|
+
revokeRubyGemsToken(token: string): boolean
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Scope Format
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
pypi:package:{name}:{read|write}
|
|
269
|
+
rubygems:gem:{name}:{read|write|yank}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Common Patterns
|
|
273
|
+
|
|
274
|
+
1. **Package name normalization** - Critical for PyPI
|
|
275
|
+
2. **Checksum calculation** - SHA256 for both protocols
|
|
276
|
+
3. **Append-only files** - RubyGems compact index
|
|
277
|
+
4. **Content negotiation** - PyPI JSON vs HTML
|
|
278
|
+
5. **Multipart upload parsing** - PyPI file uploads
|
|
279
|
+
6. **Binary file handling** - Both protocols (.whl, .tar.gz, .gem)
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Key Differences from Existing Protocols
|
|
284
|
+
|
|
285
|
+
**PyPI vs NPM:**
|
|
286
|
+
- PyPI uses Simple API (HTML) + JSON API
|
|
287
|
+
- PyPI requires package name normalization
|
|
288
|
+
- PyPI uses multipart form data for uploads (not JSON)
|
|
289
|
+
- PyPI supports multiple file types per release (wheel + sdist)
|
|
290
|
+
|
|
291
|
+
**RubyGems vs Cargo:**
|
|
292
|
+
- RubyGems uses compact index (append-only text files)
|
|
293
|
+
- RubyGems uses checksums in index files (not just filenames)
|
|
294
|
+
- RubyGems has HTTP Range support for incremental updates
|
|
295
|
+
- RubyGems uses MD5 for index checksums, SHA256 for .gem files
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Testing Requirements
|
|
300
|
+
|
|
301
|
+
### PyPI Tests Must Cover:
|
|
302
|
+
- Package upload (wheel and sdist)
|
|
303
|
+
- Package name normalization
|
|
304
|
+
- Simple API HTML generation (PEP 503)
|
|
305
|
+
- JSON API responses (PEP 691)
|
|
306
|
+
- Content negotiation
|
|
307
|
+
- Hash calculation and verification
|
|
308
|
+
- Authentication (tokens)
|
|
309
|
+
- Multi-file releases
|
|
310
|
+
- Yanked packages
|
|
311
|
+
|
|
312
|
+
### RubyGems Tests Must Cover:
|
|
313
|
+
- Gem upload
|
|
314
|
+
- Compact index generation
|
|
315
|
+
- `/versions` file updates (append-only)
|
|
316
|
+
- `/info/<gem>` file generation
|
|
317
|
+
- `/names` file generation
|
|
318
|
+
- Checksum calculations (MD5 and SHA256)
|
|
319
|
+
- Platform-specific gems
|
|
320
|
+
- Yanking/unyanking
|
|
321
|
+
- HTTP Range requests
|
|
322
|
+
- Authentication (API keys)
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Security Considerations
|
|
327
|
+
|
|
328
|
+
1. **Package name validation** - Prevent path traversal
|
|
329
|
+
2. **File size limits** - Prevent DoS via large uploads
|
|
330
|
+
3. **Content-Type validation** - Verify file types
|
|
331
|
+
4. **Checksum verification** - Ensure file integrity
|
|
332
|
+
5. **Token scope enforcement** - Read vs write permissions
|
|
333
|
+
6. **HTML escaping** - Prevent XSS in generated HTML
|
|
334
|
+
7. **Metadata sanitization** - Clean user-provided strings
|
|
335
|
+
8. **Rate limiting** - Consider upload frequency limits
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -159,15 +159,7 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
159
159
|
includeDev: boolean,
|
|
160
160
|
token: IAuthToken | null
|
|
161
161
|
): Promise<IResponse> {
|
|
162
|
-
//
|
|
163
|
-
if (!await this.checkPermission(token, vendorPackage, 'read')) {
|
|
164
|
-
return {
|
|
165
|
-
status: 401,
|
|
166
|
-
headers: { 'WWW-Authenticate': 'Bearer realm="composer"' },
|
|
167
|
-
body: { status: 'error', message: 'Authentication required' },
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
162
|
+
// Read operations are public, no authentication required
|
|
171
163
|
const metadata = await this.storage.getComposerPackageMetadata(vendorPackage);
|
|
172
164
|
|
|
173
165
|
if (!metadata) {
|
|
@@ -227,15 +219,7 @@ export class ComposerRegistry extends BaseRegistry {
|
|
|
227
219
|
reference: string,
|
|
228
220
|
token: IAuthToken | null
|
|
229
221
|
): Promise<IResponse> {
|
|
230
|
-
//
|
|
231
|
-
if (!await this.checkPermission(token, vendorPackage, 'read')) {
|
|
232
|
-
return {
|
|
233
|
-
status: 401,
|
|
234
|
-
headers: { 'WWW-Authenticate': 'Bearer realm="composer"' },
|
|
235
|
-
body: { status: 'error', message: 'Authentication required' },
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
222
|
+
// Read operations are public, no authentication required
|
|
239
223
|
const zipData = await this.storage.getComposerPackageZip(vendorPackage, reference);
|
|
240
224
|
|
|
241
225
|
if (!zipData) {
|
|
@@ -317,12 +317,153 @@ export class AuthManager {
|
|
|
317
317
|
this.tokenStore.delete(token);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// ========================================================================
|
|
321
|
+
// CARGO TOKEN MANAGEMENT
|
|
322
|
+
// ========================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a Cargo token
|
|
326
|
+
* @param userId - User ID
|
|
327
|
+
* @param readonly - Whether the token is readonly
|
|
328
|
+
* @returns Cargo UUID token
|
|
329
|
+
*/
|
|
330
|
+
public async createCargoToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
331
|
+
const scopes = readonly ? ['cargo:*:*:read'] : ['cargo:*:*:*'];
|
|
332
|
+
return this.createUuidToken(userId, 'cargo', scopes, readonly);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Validate a Cargo token
|
|
337
|
+
* @param token - Cargo UUID token
|
|
338
|
+
* @returns Auth token object or null
|
|
339
|
+
*/
|
|
340
|
+
public async validateCargoToken(token: string): Promise<IAuthToken | null> {
|
|
341
|
+
if (!this.isValidUuid(token)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const authToken = this.tokenStore.get(token);
|
|
346
|
+
if (!authToken || authToken.type !== 'cargo') {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check expiration if set
|
|
351
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
352
|
+
this.tokenStore.delete(token);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return authToken;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Revoke a Cargo token
|
|
361
|
+
* @param token - Cargo UUID token
|
|
362
|
+
*/
|
|
363
|
+
public async revokeCargoToken(token: string): Promise<void> {
|
|
364
|
+
this.tokenStore.delete(token);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ========================================================================
|
|
368
|
+
// PYPI AUTHENTICATION
|
|
369
|
+
// ========================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create a PyPI token
|
|
373
|
+
* @param userId - User ID
|
|
374
|
+
* @param readonly - Whether the token is readonly
|
|
375
|
+
* @returns PyPI UUID token
|
|
376
|
+
*/
|
|
377
|
+
public async createPypiToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
378
|
+
const scopes = readonly ? ['pypi:*:*:read'] : ['pypi:*:*:*'];
|
|
379
|
+
return this.createUuidToken(userId, 'pypi', scopes, readonly);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Validate a PyPI token
|
|
384
|
+
* @param token - PyPI UUID token
|
|
385
|
+
* @returns Auth token object or null
|
|
386
|
+
*/
|
|
387
|
+
public async validatePypiToken(token: string): Promise<IAuthToken | null> {
|
|
388
|
+
if (!this.isValidUuid(token)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const authToken = this.tokenStore.get(token);
|
|
393
|
+
if (!authToken || authToken.type !== 'pypi') {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check expiration if set
|
|
398
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
399
|
+
this.tokenStore.delete(token);
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return authToken;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Revoke a PyPI token
|
|
408
|
+
* @param token - PyPI UUID token
|
|
409
|
+
*/
|
|
410
|
+
public async revokePypiToken(token: string): Promise<void> {
|
|
411
|
+
this.tokenStore.delete(token);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ========================================================================
|
|
415
|
+
// RUBYGEMS AUTHENTICATION
|
|
416
|
+
// ========================================================================
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a RubyGems token
|
|
420
|
+
* @param userId - User ID
|
|
421
|
+
* @param readonly - Whether the token is readonly
|
|
422
|
+
* @returns RubyGems UUID token
|
|
423
|
+
*/
|
|
424
|
+
public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise<string> {
|
|
425
|
+
const scopes = readonly ? ['rubygems:*:*:read'] : ['rubygems:*:*:*'];
|
|
426
|
+
return this.createUuidToken(userId, 'rubygems', scopes, readonly);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Validate a RubyGems token
|
|
431
|
+
* @param token - RubyGems UUID token
|
|
432
|
+
* @returns Auth token object or null
|
|
433
|
+
*/
|
|
434
|
+
public async validateRubyGemsToken(token: string): Promise<IAuthToken | null> {
|
|
435
|
+
if (!this.isValidUuid(token)) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const authToken = this.tokenStore.get(token);
|
|
440
|
+
if (!authToken || authToken.type !== 'rubygems') {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check expiration if set
|
|
445
|
+
if (authToken.expiresAt && authToken.expiresAt < new Date()) {
|
|
446
|
+
this.tokenStore.delete(token);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return authToken;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Revoke a RubyGems token
|
|
455
|
+
* @param token - RubyGems UUID token
|
|
456
|
+
*/
|
|
457
|
+
public async revokeRubyGemsToken(token: string): Promise<void> {
|
|
458
|
+
this.tokenStore.delete(token);
|
|
459
|
+
}
|
|
460
|
+
|
|
320
461
|
// ========================================================================
|
|
321
462
|
// UNIFIED AUTHENTICATION
|
|
322
463
|
// ========================================================================
|
|
323
464
|
|
|
324
465
|
/**
|
|
325
|
-
* Validate any token (NPM, Maven,
|
|
466
|
+
* Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo)
|
|
326
467
|
* @param tokenString - Token string (UUID or JWT)
|
|
327
468
|
* @param protocol - Expected protocol type
|
|
328
469
|
* @returns Auth token object or null
|
|
@@ -331,7 +472,7 @@ export class AuthManager {
|
|
|
331
472
|
tokenString: string,
|
|
332
473
|
protocol?: TRegistryProtocol
|
|
333
474
|
): Promise<IAuthToken | null> {
|
|
334
|
-
// Try UUID-based tokens (NPM, Maven, Composer)
|
|
475
|
+
// Try UUID-based tokens (NPM, Maven, Composer, Cargo, PyPI, RubyGems)
|
|
335
476
|
if (this.isValidUuid(tokenString)) {
|
|
336
477
|
// Try NPM token
|
|
337
478
|
const npmToken = await this.validateNpmToken(tokenString);
|
|
@@ -350,6 +491,24 @@ export class AuthManager {
|
|
|
350
491
|
if (composerToken && (!protocol || protocol === 'composer')) {
|
|
351
492
|
return composerToken;
|
|
352
493
|
}
|
|
494
|
+
|
|
495
|
+
// Try Cargo token
|
|
496
|
+
const cargoToken = await this.validateCargoToken(tokenString);
|
|
497
|
+
if (cargoToken && (!protocol || protocol === 'cargo')) {
|
|
498
|
+
return cargoToken;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Try PyPI token
|
|
502
|
+
const pypiToken = await this.validatePypiToken(tokenString);
|
|
503
|
+
if (pypiToken && (!protocol || protocol === 'pypi')) {
|
|
504
|
+
return pypiToken;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Try RubyGems token
|
|
508
|
+
const rubygemsToken = await this.validateRubyGemsToken(tokenString);
|
|
509
|
+
if (rubygemsToken && (!protocol || protocol === 'rubygems')) {
|
|
510
|
+
return rubygemsToken;
|
|
511
|
+
}
|
|
353
512
|
}
|
|
354
513
|
|
|
355
514
|
// Try OCI JWT
|