@push.rocks/smartregistry 1.1.1

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 (52) hide show
  1. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  2. package/dist_ts/00_commitinfo_data.js +9 -0
  3. package/dist_ts/classes.smartregistry.d.ts +45 -0
  4. package/dist_ts/classes.smartregistry.js +113 -0
  5. package/dist_ts/core/classes.authmanager.d.ts +108 -0
  6. package/dist_ts/core/classes.authmanager.js +315 -0
  7. package/dist_ts/core/classes.baseregistry.d.ts +28 -0
  8. package/dist_ts/core/classes.baseregistry.js +6 -0
  9. package/dist_ts/core/classes.registrystorage.d.ts +109 -0
  10. package/dist_ts/core/classes.registrystorage.js +226 -0
  11. package/dist_ts/core/index.d.ts +7 -0
  12. package/dist_ts/core/index.js +10 -0
  13. package/dist_ts/core/interfaces.core.d.ts +142 -0
  14. package/dist_ts/core/interfaces.core.js +5 -0
  15. package/dist_ts/index.d.ts +8 -0
  16. package/dist_ts/index.js +13 -0
  17. package/dist_ts/npm/classes.npmregistry.d.ts +36 -0
  18. package/dist_ts/npm/classes.npmregistry.js +717 -0
  19. package/dist_ts/npm/index.d.ts +5 -0
  20. package/dist_ts/npm/index.js +6 -0
  21. package/dist_ts/npm/interfaces.npm.d.ts +245 -0
  22. package/dist_ts/npm/interfaces.npm.js +6 -0
  23. package/dist_ts/oci/classes.ociregistry.d.ts +43 -0
  24. package/dist_ts/oci/classes.ociregistry.js +565 -0
  25. package/dist_ts/oci/index.d.ts +5 -0
  26. package/dist_ts/oci/index.js +6 -0
  27. package/dist_ts/oci/interfaces.oci.d.ts +103 -0
  28. package/dist_ts/oci/interfaces.oci.js +5 -0
  29. package/dist_ts/paths.d.ts +1 -0
  30. package/dist_ts/paths.js +3 -0
  31. package/dist_ts/plugins.d.ts +6 -0
  32. package/dist_ts/plugins.js +9 -0
  33. package/npmextra.json +18 -0
  34. package/package.json +49 -0
  35. package/readme.hints.md +3 -0
  36. package/readme.md +486 -0
  37. package/ts/00_commitinfo_data.ts +8 -0
  38. package/ts/classes.smartregistry.ts +129 -0
  39. package/ts/core/classes.authmanager.ts +388 -0
  40. package/ts/core/classes.baseregistry.ts +36 -0
  41. package/ts/core/classes.registrystorage.ts +270 -0
  42. package/ts/core/index.ts +11 -0
  43. package/ts/core/interfaces.core.ts +159 -0
  44. package/ts/index.ts +16 -0
  45. package/ts/npm/classes.npmregistry.ts +890 -0
  46. package/ts/npm/index.ts +6 -0
  47. package/ts/npm/interfaces.npm.ts +263 -0
  48. package/ts/oci/classes.ociregistry.ts +734 -0
  49. package/ts/oci/index.ts +6 -0
  50. package/ts/oci/interfaces.oci.ts +101 -0
  51. package/ts/paths.ts +5 -0
  52. package/ts/plugins.ts +11 -0
@@ -0,0 +1,129 @@
1
+ import { RegistryStorage } from './core/classes.registrystorage.js';
2
+ import { AuthManager } from './core/classes.authmanager.js';
3
+ import { BaseRegistry } from './core/classes.baseregistry.js';
4
+ import type { IRegistryConfig, IRequestContext, IResponse } from './core/interfaces.core.js';
5
+ import { OciRegistry } from './oci/classes.ociregistry.js';
6
+ import { NpmRegistry } from './npm/classes.npmregistry.js';
7
+
8
+ /**
9
+ * Main registry orchestrator
10
+ * Routes requests to appropriate protocol handlers (OCI or NPM)
11
+ */
12
+ export class SmartRegistry {
13
+ private storage: RegistryStorage;
14
+ private authManager: AuthManager;
15
+ private registries: Map<string, BaseRegistry> = new Map();
16
+ private config: IRegistryConfig;
17
+ private initialized: boolean = false;
18
+
19
+ constructor(config: IRegistryConfig) {
20
+ this.config = config;
21
+ this.storage = new RegistryStorage(config.storage);
22
+ this.authManager = new AuthManager(config.auth);
23
+ }
24
+
25
+ /**
26
+ * Initialize the registry system
27
+ */
28
+ public async init(): Promise<void> {
29
+ if (this.initialized) return;
30
+
31
+ // Initialize storage
32
+ await this.storage.init();
33
+
34
+ // Initialize auth manager
35
+ await this.authManager.init();
36
+
37
+ // Initialize OCI registry if enabled
38
+ if (this.config.oci?.enabled) {
39
+ const ociBasePath = this.config.oci.basePath || '/oci';
40
+ const ociRegistry = new OciRegistry(this.storage, this.authManager, ociBasePath);
41
+ await ociRegistry.init();
42
+ this.registries.set('oci', ociRegistry);
43
+ }
44
+
45
+ // Initialize NPM registry if enabled
46
+ if (this.config.npm?.enabled) {
47
+ const npmBasePath = this.config.npm.basePath || '/npm';
48
+ const registryUrl = `http://localhost:5000${npmBasePath}`; // TODO: Make configurable
49
+ const npmRegistry = new NpmRegistry(this.storage, this.authManager, npmBasePath, registryUrl);
50
+ await npmRegistry.init();
51
+ this.registries.set('npm', npmRegistry);
52
+ }
53
+
54
+ this.initialized = true;
55
+ }
56
+
57
+ /**
58
+ * Handle an incoming HTTP request
59
+ * Routes to the appropriate protocol handler based on path
60
+ */
61
+ public async handleRequest(context: IRequestContext): Promise<IResponse> {
62
+ const path = context.path;
63
+
64
+ // Route to OCI registry
65
+ if (this.config.oci?.enabled && path.startsWith(this.config.oci.basePath)) {
66
+ const ociRegistry = this.registries.get('oci');
67
+ if (ociRegistry) {
68
+ return ociRegistry.handleRequest(context);
69
+ }
70
+ }
71
+
72
+ // Route to NPM registry
73
+ if (this.config.npm?.enabled && path.startsWith(this.config.npm.basePath)) {
74
+ const npmRegistry = this.registries.get('npm');
75
+ if (npmRegistry) {
76
+ return npmRegistry.handleRequest(context);
77
+ }
78
+ }
79
+
80
+ // No matching registry
81
+ return {
82
+ status: 404,
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: {
85
+ error: 'NOT_FOUND',
86
+ message: 'No registry handler for this path',
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get the storage instance (for testing/advanced use)
93
+ */
94
+ public getStorage(): RegistryStorage {
95
+ return this.storage;
96
+ }
97
+
98
+ /**
99
+ * Get the auth manager instance (for testing/advanced use)
100
+ */
101
+ public getAuthManager(): AuthManager {
102
+ return this.authManager;
103
+ }
104
+
105
+ /**
106
+ * Get a specific registry handler
107
+ */
108
+ public getRegistry(protocol: 'oci' | 'npm'): BaseRegistry | undefined {
109
+ return this.registries.get(protocol);
110
+ }
111
+
112
+ /**
113
+ * Check if the registry is initialized
114
+ */
115
+ public isInitialized(): boolean {
116
+ return this.initialized;
117
+ }
118
+
119
+ /**
120
+ * Clean up resources (timers, connections, etc.)
121
+ */
122
+ public destroy(): void {
123
+ for (const registry of this.registries.values()) {
124
+ if (typeof (registry as any).destroy === 'function') {
125
+ (registry as any).destroy();
126
+ }
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,388 @@
1
+ import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js';
2
+
3
+ /**
4
+ * Unified authentication manager for all registry protocols
5
+ * Handles both NPM UUID tokens and OCI JWT tokens
6
+ */
7
+ export class AuthManager {
8
+ private tokenStore: Map<string, IAuthToken> = new Map();
9
+ private userCredentials: Map<string, string> = new Map(); // username -> password hash (mock)
10
+
11
+ constructor(private config: IAuthConfig) {}
12
+
13
+ /**
14
+ * Initialize the auth manager
15
+ */
16
+ public async init(): Promise<void> {
17
+ // Initialize token store (in-memory for now)
18
+ // In production, this could be Redis or a database
19
+ }
20
+
21
+ // ========================================================================
22
+ // NPM AUTHENTICATION
23
+ // ========================================================================
24
+
25
+ /**
26
+ * Create an NPM token
27
+ * @param userId - User ID
28
+ * @param readonly - Whether the token is readonly
29
+ * @returns NPM UUID token
30
+ */
31
+ public async createNpmToken(userId: string, readonly: boolean = false): Promise<string> {
32
+ if (!this.config.npmTokens.enabled) {
33
+ throw new Error('NPM tokens are not enabled');
34
+ }
35
+
36
+ const token = this.generateUuid();
37
+ const authToken: IAuthToken = {
38
+ type: 'npm',
39
+ userId,
40
+ scopes: readonly ? ['npm:*:*:read'] : ['npm:*:*:*'],
41
+ readonly,
42
+ metadata: {
43
+ created: new Date().toISOString(),
44
+ },
45
+ };
46
+
47
+ this.tokenStore.set(token, authToken);
48
+ return token;
49
+ }
50
+
51
+ /**
52
+ * Validate an NPM token
53
+ * @param token - NPM UUID token
54
+ * @returns Auth token object or null
55
+ */
56
+ public async validateNpmToken(token: string): Promise<IAuthToken | null> {
57
+ if (!this.isValidUuid(token)) {
58
+ return null;
59
+ }
60
+
61
+ const authToken = this.tokenStore.get(token);
62
+ if (!authToken || authToken.type !== 'npm') {
63
+ return null;
64
+ }
65
+
66
+ // Check expiration if set
67
+ if (authToken.expiresAt && authToken.expiresAt < new Date()) {
68
+ this.tokenStore.delete(token);
69
+ return null;
70
+ }
71
+
72
+ return authToken;
73
+ }
74
+
75
+ /**
76
+ * Revoke an NPM token
77
+ * @param token - NPM UUID token
78
+ */
79
+ public async revokeNpmToken(token: string): Promise<void> {
80
+ this.tokenStore.delete(token);
81
+ }
82
+
83
+ /**
84
+ * List all tokens for a user
85
+ * @param userId - User ID
86
+ * @returns List of token info (without actual token values)
87
+ */
88
+ public async listUserTokens(userId: string): Promise<Array<{
89
+ key: string;
90
+ readonly: boolean;
91
+ created: string;
92
+ }>> {
93
+ const tokens: Array<{key: string; readonly: boolean; created: string}> = [];
94
+
95
+ for (const [token, authToken] of this.tokenStore.entries()) {
96
+ if (authToken.userId === userId) {
97
+ tokens.push({
98
+ key: this.hashToken(token),
99
+ readonly: authToken.readonly || false,
100
+ created: authToken.metadata?.created || 'unknown',
101
+ });
102
+ }
103
+ }
104
+
105
+ return tokens;
106
+ }
107
+
108
+ // ========================================================================
109
+ // OCI AUTHENTICATION (JWT)
110
+ // ========================================================================
111
+
112
+ /**
113
+ * Create an OCI JWT token
114
+ * @param userId - User ID
115
+ * @param scopes - Permission scopes
116
+ * @param expiresIn - Expiration time in seconds
117
+ * @returns JWT token string
118
+ */
119
+ public async createOciToken(
120
+ userId: string,
121
+ scopes: string[],
122
+ expiresIn: number = 3600
123
+ ): Promise<string> {
124
+ if (!this.config.ociTokens.enabled) {
125
+ throw new Error('OCI tokens are not enabled');
126
+ }
127
+
128
+ const now = Math.floor(Date.now() / 1000);
129
+ const payload = {
130
+ iss: this.config.ociTokens.realm,
131
+ sub: userId,
132
+ aud: this.config.ociTokens.service,
133
+ exp: now + expiresIn,
134
+ nbf: now,
135
+ iat: now,
136
+ access: this.scopesToOciAccess(scopes),
137
+ };
138
+
139
+ // In production, use proper JWT library with signing
140
+ // For now, return JSON string (mock JWT)
141
+ return JSON.stringify(payload);
142
+ }
143
+
144
+ /**
145
+ * Validate an OCI JWT token
146
+ * @param jwt - JWT token string
147
+ * @returns Auth token object or null
148
+ */
149
+ public async validateOciToken(jwt: string): Promise<IAuthToken | null> {
150
+ try {
151
+ // In production, verify JWT signature
152
+ const payload = JSON.parse(jwt);
153
+
154
+ // Check expiration
155
+ const now = Math.floor(Date.now() / 1000);
156
+ if (payload.exp && payload.exp < now) {
157
+ return null;
158
+ }
159
+
160
+ // Convert to unified token format
161
+ const scopes = this.ociAccessToScopes(payload.access || []);
162
+
163
+ return {
164
+ type: 'oci',
165
+ userId: payload.sub,
166
+ scopes,
167
+ expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined,
168
+ metadata: {
169
+ iss: payload.iss,
170
+ aud: payload.aud,
171
+ },
172
+ };
173
+ } catch (error) {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ // ========================================================================
179
+ // UNIFIED AUTHENTICATION
180
+ // ========================================================================
181
+
182
+ /**
183
+ * Authenticate user credentials
184
+ * @param credentials - Username and password
185
+ * @returns User ID or null
186
+ */
187
+ public async authenticate(credentials: ICredentials): Promise<string | null> {
188
+ // Mock authentication - in production, verify against database
189
+ const storedPassword = this.userCredentials.get(credentials.username);
190
+
191
+ if (!storedPassword) {
192
+ // Auto-register for testing (remove in production)
193
+ this.userCredentials.set(credentials.username, credentials.password);
194
+ return credentials.username;
195
+ }
196
+
197
+ if (storedPassword === credentials.password) {
198
+ return credentials.username;
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Validate any token (NPM or OCI)
206
+ * @param tokenString - Token string (UUID or JWT)
207
+ * @param protocol - Expected protocol type
208
+ * @returns Auth token object or null
209
+ */
210
+ public async validateToken(
211
+ tokenString: string,
212
+ protocol?: TRegistryProtocol
213
+ ): Promise<IAuthToken | null> {
214
+ // Try NPM token first (UUID format)
215
+ if (this.isValidUuid(tokenString)) {
216
+ const npmToken = await this.validateNpmToken(tokenString);
217
+ if (npmToken && (!protocol || protocol === 'npm')) {
218
+ return npmToken;
219
+ }
220
+ }
221
+
222
+ // Try OCI JWT
223
+ const ociToken = await this.validateOciToken(tokenString);
224
+ if (ociToken && (!protocol || protocol === 'oci')) {
225
+ return ociToken;
226
+ }
227
+
228
+ return null;
229
+ }
230
+
231
+ /**
232
+ * Check if token has permission for an action
233
+ * @param token - Auth token
234
+ * @param resource - Resource being accessed (e.g., "package:foo" or "repository:bar")
235
+ * @param action - Action being performed (read, write, push, pull, delete)
236
+ * @returns true if authorized
237
+ */
238
+ public async authorize(
239
+ token: IAuthToken | null,
240
+ resource: string,
241
+ action: string
242
+ ): Promise<boolean> {
243
+ if (!token) {
244
+ return false;
245
+ }
246
+
247
+ // Check readonly flag
248
+ if (token.readonly && ['write', 'push', 'delete'].includes(action)) {
249
+ return false;
250
+ }
251
+
252
+ // Check scopes
253
+ for (const scope of token.scopes) {
254
+ if (this.matchesScope(scope, resource, action)) {
255
+ return true;
256
+ }
257
+ }
258
+
259
+ return false;
260
+ }
261
+
262
+ // ========================================================================
263
+ // HELPER METHODS
264
+ // ========================================================================
265
+
266
+ /**
267
+ * Check if a scope matches a resource and action
268
+ * Scope format: "{protocol}:{type}:{name}:{action}"
269
+ * Examples:
270
+ * - "npm:*:*" - All NPM access
271
+ * - "npm:package:foo:*" - All actions on package foo
272
+ * - "npm:package:foo:read" - Read-only on package foo
273
+ * - "oci:repository:*:pull" - Pull from any OCI repo
274
+ */
275
+ private matchesScope(scope: string, resource: string, action: string): boolean {
276
+ const scopeParts = scope.split(':');
277
+ const resourceParts = resource.split(':');
278
+
279
+ // Scope must have at least protocol:type:name:action
280
+ if (scopeParts.length < 4) {
281
+ return false;
282
+ }
283
+
284
+ const [scopeProtocol, scopeType, scopeName, scopeAction] = scopeParts;
285
+ const [resourceProtocol, resourceType, resourceName] = resourceParts;
286
+
287
+ // Check protocol
288
+ if (scopeProtocol !== '*' && scopeProtocol !== resourceProtocol) {
289
+ return false;
290
+ }
291
+
292
+ // Check type
293
+ if (scopeType !== '*' && scopeType !== resourceType) {
294
+ return false;
295
+ }
296
+
297
+ // Check name
298
+ if (scopeName !== '*' && scopeName !== resourceName) {
299
+ return false;
300
+ }
301
+
302
+ // Check action
303
+ if (scopeAction !== '*' && scopeAction !== action) {
304
+ // Map action aliases
305
+ const actionAliases: Record<string, string[]> = {
306
+ read: ['pull', 'get'],
307
+ write: ['push', 'put', 'post'],
308
+ };
309
+
310
+ const aliases = actionAliases[scopeAction] || [];
311
+ if (!aliases.includes(action)) {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ return true;
317
+ }
318
+
319
+ /**
320
+ * Convert unified scopes to OCI access array
321
+ */
322
+ private scopesToOciAccess(scopes: string[]): Array<{
323
+ type: string;
324
+ name: string;
325
+ actions: string[];
326
+ }> {
327
+ const access: Array<{type: string; name: string; actions: string[]}> = [];
328
+
329
+ for (const scope of scopes) {
330
+ const parts = scope.split(':');
331
+ if (parts.length >= 4 && parts[0] === 'oci') {
332
+ access.push({
333
+ type: parts[1],
334
+ name: parts[2],
335
+ actions: [parts[3]],
336
+ });
337
+ }
338
+ }
339
+
340
+ return access;
341
+ }
342
+
343
+ /**
344
+ * Convert OCI access array to unified scopes
345
+ */
346
+ private ociAccessToScopes(access: Array<{
347
+ type: string;
348
+ name: string;
349
+ actions: string[];
350
+ }>): string[] {
351
+ const scopes: string[] = [];
352
+
353
+ for (const item of access) {
354
+ for (const action of item.actions) {
355
+ scopes.push(`oci:${item.type}:${item.name}:${action}`);
356
+ }
357
+ }
358
+
359
+ return scopes;
360
+ }
361
+
362
+ /**
363
+ * Generate UUID for NPM tokens
364
+ */
365
+ private generateUuid(): string {
366
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
367
+ const r = (Math.random() * 16) | 0;
368
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
369
+ return v.toString(16);
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Check if string is a valid UUID
375
+ */
376
+ private isValidUuid(str: string): boolean {
377
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
378
+ return uuidRegex.test(str);
379
+ }
380
+
381
+ /**
382
+ * Hash a token for identification (SHA-512 mock)
383
+ */
384
+ private hashToken(token: string): string {
385
+ // In production, use actual SHA-512
386
+ return `sha512-${token.substring(0, 16)}...`;
387
+ }
388
+ }
@@ -0,0 +1,36 @@
1
+ import type { IRequestContext, IResponse, IAuthToken } from './interfaces.core.js';
2
+
3
+ /**
4
+ * Abstract base class for all registry protocol implementations
5
+ */
6
+ export abstract class BaseRegistry {
7
+ /**
8
+ * Initialize the registry
9
+ */
10
+ abstract init(): Promise<void>;
11
+
12
+ /**
13
+ * Handle an incoming HTTP request
14
+ * @param context - Request context
15
+ * @returns Response object
16
+ */
17
+ abstract handleRequest(context: IRequestContext): Promise<IResponse>;
18
+
19
+ /**
20
+ * Get the base path for this registry protocol
21
+ */
22
+ abstract getBasePath(): string;
23
+
24
+ /**
25
+ * Validate that a token has the required permissions
26
+ * @param token - Authentication token
27
+ * @param resource - Resource being accessed
28
+ * @param action - Action being performed
29
+ * @returns true if authorized
30
+ */
31
+ protected abstract checkPermission(
32
+ token: IAuthToken | null,
33
+ resource: string,
34
+ action: string
35
+ ): Promise<boolean>;
36
+ }