@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,890 @@
1
+ import { Smartlog } from '@push.rocks/smartlog';
2
+ import { BaseRegistry } from '../core/classes.baseregistry.js';
3
+ import { RegistryStorage } from '../core/classes.registrystorage.js';
4
+ import { AuthManager } from '../core/classes.authmanager.js';
5
+ import type { IRequestContext, IResponse, IAuthToken } from '../core/interfaces.core.js';
6
+ import type {
7
+ IPackument,
8
+ INpmVersion,
9
+ IPublishRequest,
10
+ ISearchResponse,
11
+ ISearchResult,
12
+ ITokenListResponse,
13
+ ITokenCreateRequest,
14
+ IUserAuthRequest,
15
+ INpmError,
16
+ } from './interfaces.npm.js';
17
+
18
+ /**
19
+ * NPM Registry implementation
20
+ * Compliant with npm registry API
21
+ */
22
+ export class NpmRegistry extends BaseRegistry {
23
+ private storage: RegistryStorage;
24
+ private authManager: AuthManager;
25
+ private basePath: string = '/npm';
26
+ private registryUrl: string;
27
+ private logger: Smartlog;
28
+
29
+ constructor(
30
+ storage: RegistryStorage,
31
+ authManager: AuthManager,
32
+ basePath: string = '/npm',
33
+ registryUrl: string = 'http://localhost:5000/npm'
34
+ ) {
35
+ super();
36
+ this.storage = storage;
37
+ this.authManager = authManager;
38
+ this.basePath = basePath;
39
+ this.registryUrl = registryUrl;
40
+
41
+ // Initialize logger
42
+ this.logger = new Smartlog({
43
+ logContext: {
44
+ company: 'push.rocks',
45
+ companyunit: 'smartregistry',
46
+ containerName: 'npm-registry',
47
+ environment: (process.env.NODE_ENV as any) || 'development',
48
+ runtime: 'node',
49
+ zone: 'npm'
50
+ }
51
+ });
52
+ this.logger.enableConsole();
53
+ }
54
+
55
+ public async init(): Promise<void> {
56
+ // NPM registry initialization
57
+ }
58
+
59
+ public getBasePath(): string {
60
+ return this.basePath;
61
+ }
62
+
63
+ public async handleRequest(context: IRequestContext): Promise<IResponse> {
64
+ const path = context.path.replace(this.basePath, '');
65
+
66
+ // Extract token from Authorization header
67
+ const authHeader = context.headers['authorization'] || context.headers['Authorization'];
68
+ const tokenString = authHeader?.replace(/^Bearer\s+/i, '');
69
+ const token = tokenString ? await this.authManager.validateToken(tokenString, 'npm') : null;
70
+
71
+ this.logger.log('debug', `handleRequest: ${context.method} ${path}`, {
72
+ method: context.method,
73
+ path,
74
+ hasAuth: !!token
75
+ });
76
+
77
+ // Registry root
78
+ if (path === '/' || path === '') {
79
+ return this.handleRegistryInfo();
80
+ }
81
+
82
+ // Search: /-/v1/search
83
+ if (path.startsWith('/-/v1/search')) {
84
+ return this.handleSearch(context.query);
85
+ }
86
+
87
+ // User authentication: /-/user/org.couchdb.user:{username}
88
+ const userMatch = path.match(/^\/-\/user\/org\.couchdb\.user:(.+)$/);
89
+ if (userMatch) {
90
+ return this.handleUserAuth(context.method, userMatch[1], context.body, token);
91
+ }
92
+
93
+ // Token operations: /-/npm/v1/tokens
94
+ if (path.startsWith('/-/npm/v1/tokens')) {
95
+ return this.handleTokens(context.method, path, context.body, token);
96
+ }
97
+
98
+ // Dist-tags: /-/package/{package}/dist-tags
99
+ const distTagsMatch = path.match(/^\/-\/package\/(@?[^\/]+(?:\/[^\/]+)?)\/dist-tags(?:\/(.+))?$/);
100
+ if (distTagsMatch) {
101
+ const [, packageName, tag] = distTagsMatch;
102
+ return this.handleDistTags(context.method, packageName, tag, context.body, token);
103
+ }
104
+
105
+ // Tarball download: /{package}/-/{filename}.tgz
106
+ const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/);
107
+ if (tarballMatch) {
108
+ const [, packageName, filename] = tarballMatch;
109
+ return this.handleTarballDownload(packageName, filename, token);
110
+ }
111
+
112
+ // Unpublish specific version: DELETE /{package}/-/{version}
113
+ const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/);
114
+ if (unpublishVersionMatch && context.method === 'DELETE') {
115
+ const [, packageName, version] = unpublishVersionMatch;
116
+ console.log(`[unpublishVersionMatch] packageName=${packageName}, version=${version}`);
117
+ return this.unpublishVersion(packageName, version, token);
118
+ }
119
+
120
+ // Unpublish entire package: DELETE /{package}/-rev/{rev}
121
+ const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/);
122
+ if (unpublishPackageMatch && context.method === 'DELETE') {
123
+ const [, packageName, rev] = unpublishPackageMatch;
124
+ console.log(`[unpublishPackageMatch] packageName=${packageName}, rev=${rev}`);
125
+ return this.unpublishPackage(packageName, token);
126
+ }
127
+
128
+ // Package version: /{package}/{version}
129
+ const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/);
130
+ if (versionMatch) {
131
+ const [, packageName, version] = versionMatch;
132
+ console.log(`[versionMatch] matched! packageName=${packageName}, version=${version}`);
133
+ return this.handlePackageVersion(packageName, version, token);
134
+ }
135
+
136
+ // Package operations: /{package}
137
+ const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/);
138
+ if (packageMatch) {
139
+ const packageName = packageMatch[1];
140
+ console.log(`[packageMatch] matched! packageName=${packageName}`);
141
+ return this.handlePackage(context.method, packageName, context.body, context.query, token);
142
+ }
143
+
144
+ return {
145
+ status: 404,
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: this.createError('E404', 'Not found'),
148
+ };
149
+ }
150
+
151
+ protected async checkPermission(
152
+ token: IAuthToken | null,
153
+ resource: string,
154
+ action: string
155
+ ): Promise<boolean> {
156
+ if (!token) return false;
157
+ return this.authManager.authorize(token, `npm:package:${resource}`, action);
158
+ }
159
+
160
+ // ========================================================================
161
+ // REQUEST HANDLERS
162
+ // ========================================================================
163
+
164
+ private handleRegistryInfo(): IResponse {
165
+ return {
166
+ status: 200,
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: {
169
+ db_name: 'registry',
170
+ doc_count: 0,
171
+ doc_del_count: 0,
172
+ update_seq: 0,
173
+ purge_seq: 0,
174
+ compact_running: false,
175
+ disk_size: 0,
176
+ data_size: 0,
177
+ instance_start_time: Date.now().toString(),
178
+ disk_format_version: 0,
179
+ committed_update_seq: 0,
180
+ },
181
+ };
182
+ }
183
+
184
+ private async handlePackage(
185
+ method: string,
186
+ packageName: string,
187
+ body: any,
188
+ query: Record<string, string>,
189
+ token: IAuthToken | null
190
+ ): Promise<IResponse> {
191
+ switch (method) {
192
+ case 'GET':
193
+ return this.getPackument(packageName, token, query);
194
+ case 'PUT':
195
+ return this.publishPackage(packageName, body, token);
196
+ case 'DELETE':
197
+ return this.unpublishPackage(packageName, token);
198
+ default:
199
+ return {
200
+ status: 405,
201
+ headers: {},
202
+ body: this.createError('EBADREQUEST', 'Method not allowed'),
203
+ };
204
+ }
205
+ }
206
+
207
+ private async getPackument(
208
+ packageName: string,
209
+ token: IAuthToken | null,
210
+ query: Record<string, string>
211
+ ): Promise<IResponse> {
212
+ const packument = await this.storage.getNpmPackument(packageName);
213
+ this.logger.log('debug', `getPackument: ${packageName}`, {
214
+ packageName,
215
+ found: !!packument,
216
+ versions: packument ? Object.keys(packument.versions).length : 0
217
+ });
218
+
219
+ if (!packument) {
220
+ return {
221
+ status: 404,
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: this.createError('E404', `Package '${packageName}' not found`),
224
+ };
225
+ }
226
+
227
+ // Check if abbreviated version requested
228
+ const accept = query['accept'] || '';
229
+ if (accept.includes('application/vnd.npm.install-v1+json')) {
230
+ // Return abbreviated packument
231
+ const abbreviated = {
232
+ name: packument.name,
233
+ modified: packument.time?.modified || new Date().toISOString(),
234
+ 'dist-tags': packument['dist-tags'],
235
+ versions: packument.versions,
236
+ };
237
+
238
+ return {
239
+ status: 200,
240
+ headers: { 'Content-Type': 'application/vnd.npm.install-v1+json' },
241
+ body: abbreviated,
242
+ };
243
+ }
244
+
245
+ return {
246
+ status: 200,
247
+ headers: { 'Content-Type': 'application/json' },
248
+ body: packument,
249
+ };
250
+ }
251
+
252
+ private async handlePackageVersion(
253
+ packageName: string,
254
+ version: string,
255
+ token: IAuthToken | null
256
+ ): Promise<IResponse> {
257
+ console.log(`[handlePackageVersion] packageName=${packageName}, version=${version}`);
258
+ const packument = await this.storage.getNpmPackument(packageName);
259
+ console.log(`[handlePackageVersion] packument found:`, !!packument);
260
+ if (packument) {
261
+ console.log(`[handlePackageVersion] versions:`, Object.keys(packument.versions || {}));
262
+ }
263
+ if (!packument) {
264
+ return {
265
+ status: 404,
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: this.createError('E404', 'Package not found'),
268
+ };
269
+ }
270
+
271
+ // Resolve version (could be "latest" or actual version)
272
+ let actualVersion = version;
273
+ if (version === 'latest') {
274
+ actualVersion = packument['dist-tags']?.latest;
275
+ if (!actualVersion) {
276
+ return {
277
+ status: 404,
278
+ headers: {},
279
+ body: this.createError('E404', 'No latest version'),
280
+ };
281
+ }
282
+ }
283
+
284
+ const versionData = packument.versions[actualVersion];
285
+ if (!versionData) {
286
+ return {
287
+ status: 404,
288
+ headers: {},
289
+ body: this.createError('E404', 'Version not found'),
290
+ };
291
+ }
292
+
293
+ return {
294
+ status: 200,
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: versionData,
297
+ };
298
+ }
299
+
300
+ private async publishPackage(
301
+ packageName: string,
302
+ body: IPublishRequest,
303
+ token: IAuthToken | null
304
+ ): Promise<IResponse> {
305
+ this.logger.log('info', `publishPackage: ${packageName}`, {
306
+ packageName,
307
+ versions: Object.keys(body.versions || {}),
308
+ hasAuth: !!token
309
+ });
310
+
311
+ const hasPermission = await this.checkPermission(token, packageName, 'write');
312
+ if (!hasPermission) {
313
+ this.logger.log('warn', `publishPackage: unauthorized`, { packageName, userId: token?.userId });
314
+ }
315
+ if (!hasPermission) {
316
+ return {
317
+ status: 401,
318
+ headers: {},
319
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
320
+ };
321
+ }
322
+
323
+ if (!body || !body.versions || !body._attachments) {
324
+ return {
325
+ status: 400,
326
+ headers: {},
327
+ body: this.createError('EBADREQUEST', 'Invalid publish request'),
328
+ };
329
+ }
330
+
331
+ // Get existing packument or create new one
332
+ let packument = await this.storage.getNpmPackument(packageName);
333
+ const isNew = !packument;
334
+
335
+ if (isNew) {
336
+ packument = {
337
+ _id: packageName,
338
+ name: packageName,
339
+ description: body.description,
340
+ 'dist-tags': body['dist-tags'] || { latest: Object.keys(body.versions)[0] },
341
+ versions: {},
342
+ time: {
343
+ created: new Date().toISOString(),
344
+ modified: new Date().toISOString(),
345
+ },
346
+ maintainers: body.maintainers || [],
347
+ readme: body.readme,
348
+ };
349
+ }
350
+
351
+ // Process each new version
352
+ for (const [version, versionData] of Object.entries(body.versions)) {
353
+ // Check if version already exists
354
+ if (packument.versions[version]) {
355
+ return {
356
+ status: 403,
357
+ headers: {},
358
+ body: this.createError('EPUBLISHCONFLICT', `Version ${version} already exists`),
359
+ };
360
+ }
361
+
362
+ // Find attachment for this version
363
+ const attachmentKey = Object.keys(body._attachments).find(key =>
364
+ key.includes(version)
365
+ );
366
+
367
+ if (!attachmentKey) {
368
+ return {
369
+ status: 400,
370
+ headers: {},
371
+ body: this.createError('EBADREQUEST', `No tarball for version ${version}`),
372
+ };
373
+ }
374
+
375
+ const attachment = body._attachments[attachmentKey];
376
+
377
+ // Decode base64 tarball
378
+ const tarballBuffer = Buffer.from(attachment.data, 'base64');
379
+
380
+ // Calculate shasum
381
+ const crypto = await import('crypto');
382
+ const shasum = crypto.createHash('sha1').update(tarballBuffer).digest('hex');
383
+ const integrity = `sha512-${crypto.createHash('sha512').update(tarballBuffer).digest('base64')}`;
384
+
385
+ // Store tarball
386
+ await this.storage.putNpmTarball(packageName, version, tarballBuffer);
387
+
388
+ // Update version data with dist info
389
+ const safeName = packageName.replace('@', '').replace('/', '-');
390
+ versionData.dist = {
391
+ tarball: `${this.registryUrl}/${packageName}/-/${safeName}-${version}.tgz`,
392
+ shasum,
393
+ integrity,
394
+ fileCount: 0,
395
+ unpackedSize: tarballBuffer.length,
396
+ };
397
+
398
+ versionData._id = `${packageName}@${version}`;
399
+ versionData._npmUser = token ? { name: token.userId, email: '' } : undefined;
400
+
401
+ // Add version to packument
402
+ packument.versions[version] = versionData;
403
+ if (packument.time) {
404
+ packument.time[version] = new Date().toISOString();
405
+ packument.time.modified = new Date().toISOString();
406
+ }
407
+ }
408
+
409
+ // Update dist-tags
410
+ if (body['dist-tags']) {
411
+ packument['dist-tags'] = { ...packument['dist-tags'], ...body['dist-tags'] };
412
+ }
413
+
414
+ // Save packument
415
+ await this.storage.putNpmPackument(packageName, packument);
416
+ this.logger.log('success', `publishPackage: saved ${packageName}`, {
417
+ packageName,
418
+ versions: Object.keys(packument.versions),
419
+ distTags: packument['dist-tags']
420
+ });
421
+
422
+ return {
423
+ status: 201,
424
+ headers: { 'Content-Type': 'application/json' },
425
+ body: { ok: true, id: packageName, rev: packument._rev || '1-' + Date.now() },
426
+ };
427
+ }
428
+
429
+ private async unpublishVersion(
430
+ packageName: string,
431
+ version: string,
432
+ token: IAuthToken | null
433
+ ): Promise<IResponse> {
434
+ if (!await this.checkPermission(token, packageName, 'delete')) {
435
+ return {
436
+ status: 401,
437
+ headers: {},
438
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
439
+ };
440
+ }
441
+
442
+ const packument = await this.storage.getNpmPackument(packageName);
443
+ if (!packument) {
444
+ return {
445
+ status: 404,
446
+ headers: {},
447
+ body: this.createError('E404', 'Package not found'),
448
+ };
449
+ }
450
+
451
+ // Check if version exists
452
+ if (!packument.versions[version]) {
453
+ return {
454
+ status: 404,
455
+ headers: {},
456
+ body: this.createError('E404', 'Version not found'),
457
+ };
458
+ }
459
+
460
+ // Delete tarball
461
+ await this.storage.deleteNpmTarball(packageName, version);
462
+
463
+ // Remove version from packument
464
+ delete packument.versions[version];
465
+ if (packument.time) {
466
+ delete packument.time[version];
467
+ packument.time.modified = new Date().toISOString();
468
+ }
469
+
470
+ // Update latest tag if this was the latest version
471
+ if (packument['dist-tags']?.latest === version) {
472
+ const remainingVersions = Object.keys(packument.versions);
473
+ if (remainingVersions.length > 0) {
474
+ packument['dist-tags'].latest = remainingVersions[remainingVersions.length - 1];
475
+ } else {
476
+ delete packument['dist-tags'].latest;
477
+ }
478
+ }
479
+
480
+ // Save updated packument
481
+ await this.storage.putNpmPackument(packageName, packument);
482
+
483
+ return {
484
+ status: 200,
485
+ headers: { 'Content-Type': 'application/json' },
486
+ body: { ok: true },
487
+ };
488
+ }
489
+
490
+ private async unpublishPackage(
491
+ packageName: string,
492
+ token: IAuthToken | null
493
+ ): Promise<IResponse> {
494
+ if (!await this.checkPermission(token, packageName, 'delete')) {
495
+ return {
496
+ status: 401,
497
+ headers: {},
498
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
499
+ };
500
+ }
501
+
502
+ const packument = await this.storage.getNpmPackument(packageName);
503
+ if (!packument) {
504
+ return {
505
+ status: 404,
506
+ headers: {},
507
+ body: this.createError('E404', 'Package not found'),
508
+ };
509
+ }
510
+
511
+ // Delete all tarballs
512
+ for (const version of Object.keys(packument.versions)) {
513
+ await this.storage.deleteNpmTarball(packageName, version);
514
+ }
515
+
516
+ // Delete packument
517
+ await this.storage.deleteNpmPackument(packageName);
518
+
519
+ return {
520
+ status: 200,
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: { ok: true },
523
+ };
524
+ }
525
+
526
+ private async handleTarballDownload(
527
+ packageName: string,
528
+ filename: string,
529
+ token: IAuthToken | null
530
+ ): Promise<IResponse> {
531
+ // Extract version from filename: package-name-1.0.0.tgz
532
+ const versionMatch = filename.match(/-([\d.]+(?:-[a-z0-9.]+)?)\.tgz$/);
533
+ if (!versionMatch) {
534
+ return {
535
+ status: 400,
536
+ headers: {},
537
+ body: this.createError('EBADREQUEST', 'Invalid tarball filename'),
538
+ };
539
+ }
540
+
541
+ const version = versionMatch[1];
542
+ const tarball = await this.storage.getNpmTarball(packageName, version);
543
+
544
+ if (!tarball) {
545
+ return {
546
+ status: 404,
547
+ headers: {},
548
+ body: this.createError('E404', 'Tarball not found'),
549
+ };
550
+ }
551
+
552
+ return {
553
+ status: 200,
554
+ headers: {
555
+ 'Content-Type': 'application/octet-stream',
556
+ 'Content-Length': tarball.length.toString(),
557
+ },
558
+ body: tarball,
559
+ };
560
+ }
561
+
562
+ private async handleSearch(query: Record<string, string>): Promise<IResponse> {
563
+ const text = query.text || '';
564
+ const size = parseInt(query.size || '20', 10);
565
+ const from = parseInt(query.from || '0', 10);
566
+
567
+ this.logger.log('debug', `handleSearch: query="${text}"`, { text, size, from });
568
+
569
+ // Simple search implementation
570
+ const results: ISearchResult[] = [];
571
+
572
+ try {
573
+ // List all package paths
574
+ const packagePaths = await this.storage.listObjects('npm/packages/');
575
+
576
+ // Extract unique package names from paths (format: npm/packages/{packageName}/...)
577
+ const packageNames = new Set<string>();
578
+ for (const path of packagePaths) {
579
+ const match = path.match(/^npm\/packages\/([^\/]+)\/index\.json$/);
580
+ if (match) {
581
+ packageNames.add(match[1]);
582
+ }
583
+ }
584
+
585
+ this.logger.log('debug', `handleSearch: found ${packageNames.size} packages`, {
586
+ totalPackages: packageNames.size,
587
+ pathsScanned: packagePaths.length
588
+ });
589
+
590
+ // Load packuments and filter by search text
591
+ for (const packageName of packageNames) {
592
+ if (!text || packageName.toLowerCase().includes(text.toLowerCase())) {
593
+ const packument = await this.storage.getNpmPackument(packageName);
594
+ if (packument) {
595
+ const latestVersion = packument['dist-tags']?.latest;
596
+ const versionData = latestVersion ? packument.versions[latestVersion] : null;
597
+
598
+ results.push({
599
+ package: {
600
+ name: packument.name,
601
+ version: latestVersion || '0.0.0',
602
+ description: packument.description || versionData?.description || '',
603
+ keywords: versionData?.keywords || [],
604
+ date: packument.time?.modified || new Date().toISOString(),
605
+ links: {},
606
+ author: versionData?.author || {},
607
+ publisher: versionData?._npmUser || {},
608
+ maintainers: packument.maintainers || [],
609
+ },
610
+ score: {
611
+ final: 1.0,
612
+ detail: {
613
+ quality: 1.0,
614
+ popularity: 1.0,
615
+ maintenance: 1.0,
616
+ },
617
+ },
618
+ searchScore: 1.0,
619
+ });
620
+ }
621
+ }
622
+ }
623
+ } catch (error) {
624
+ console.error('[handleSearch] Error:', error);
625
+ }
626
+
627
+ // Apply pagination
628
+ const paginatedResults = results.slice(from, from + size);
629
+
630
+ const response: ISearchResponse = {
631
+ objects: paginatedResults,
632
+ total: results.length,
633
+ time: new Date().toISOString(),
634
+ };
635
+
636
+ return {
637
+ status: 200,
638
+ headers: { 'Content-Type': 'application/json' },
639
+ body: response,
640
+ };
641
+ }
642
+
643
+ private async handleUserAuth(
644
+ method: string,
645
+ username: string,
646
+ body: IUserAuthRequest,
647
+ token: IAuthToken | null
648
+ ): Promise<IResponse> {
649
+ if (method !== 'PUT') {
650
+ return {
651
+ status: 405,
652
+ headers: {},
653
+ body: this.createError('EBADREQUEST', 'Method not allowed'),
654
+ };
655
+ }
656
+
657
+ if (!body || !body.name || !body.password) {
658
+ return {
659
+ status: 400,
660
+ headers: {},
661
+ body: this.createError('EBADREQUEST', 'Invalid request'),
662
+ };
663
+ }
664
+
665
+ // Authenticate user
666
+ const userId = await this.authManager.authenticate({
667
+ username: body.name,
668
+ password: body.password,
669
+ });
670
+
671
+ if (!userId) {
672
+ return {
673
+ status: 401,
674
+ headers: {},
675
+ body: this.createError('EUNAUTHORIZED', 'Invalid credentials'),
676
+ };
677
+ }
678
+
679
+ // Create NPM token
680
+ const npmToken = await this.authManager.createNpmToken(userId, false);
681
+
682
+ return {
683
+ status: 201,
684
+ headers: { 'Content-Type': 'application/json' },
685
+ body: {
686
+ ok: true,
687
+ id: `org.couchdb.user:${username}`,
688
+ rev: '1-' + Date.now(),
689
+ token: npmToken,
690
+ },
691
+ };
692
+ }
693
+
694
+ private async handleTokens(
695
+ method: string,
696
+ path: string,
697
+ body: any,
698
+ token: IAuthToken | null
699
+ ): Promise<IResponse> {
700
+ if (!token) {
701
+ return {
702
+ status: 401,
703
+ headers: {},
704
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
705
+ };
706
+ }
707
+
708
+ // List tokens: GET /-/npm/v1/tokens
709
+ if (path === '/-/npm/v1/tokens' && method === 'GET') {
710
+ return this.listTokens(token);
711
+ }
712
+
713
+ // Create token: POST /-/npm/v1/tokens
714
+ if (path === '/-/npm/v1/tokens' && method === 'POST') {
715
+ return this.createToken(body, token);
716
+ }
717
+
718
+ // Delete token: DELETE /-/npm/v1/tokens/token/{key}
719
+ const deleteMatch = path.match(/^\/-\/npm\/v1\/tokens\/token\/(.+)$/);
720
+ if (deleteMatch && method === 'DELETE') {
721
+ return this.deleteToken(deleteMatch[1], token);
722
+ }
723
+
724
+ return {
725
+ status: 404,
726
+ headers: {},
727
+ body: this.createError('E404', 'Not found'),
728
+ };
729
+ }
730
+
731
+ private async listTokens(token: IAuthToken): Promise<IResponse> {
732
+ const tokens = await this.authManager.listUserTokens(token.userId);
733
+
734
+ const response: ITokenListResponse = {
735
+ objects: tokens.map(t => ({
736
+ token: '********',
737
+ key: t.key,
738
+ readonly: t.readonly,
739
+ created: t.created,
740
+ updated: t.created,
741
+ })),
742
+ total: tokens.length,
743
+ urls: {},
744
+ };
745
+
746
+ return {
747
+ status: 200,
748
+ headers: { 'Content-Type': 'application/json' },
749
+ body: response,
750
+ };
751
+ }
752
+
753
+ private async createToken(body: ITokenCreateRequest, token: IAuthToken): Promise<IResponse> {
754
+ if (!body || !body.password) {
755
+ return {
756
+ status: 400,
757
+ headers: {},
758
+ body: this.createError('EBADREQUEST', 'Password required'),
759
+ };
760
+ }
761
+
762
+ // Verify password (simplified - in production, verify against stored password)
763
+ const readonly = body.readonly || false;
764
+ const newToken = await this.authManager.createNpmToken(token.userId, readonly);
765
+
766
+ return {
767
+ status: 200,
768
+ headers: { 'Content-Type': 'application/json' },
769
+ body: {
770
+ token: newToken,
771
+ key: 'sha512-' + newToken.substring(0, 16) + '...',
772
+ cidr_whitelist: body.cidr_whitelist || [],
773
+ readonly,
774
+ created: new Date().toISOString(),
775
+ updated: new Date().toISOString(),
776
+ },
777
+ };
778
+ }
779
+
780
+ private async deleteToken(key: string, token: IAuthToken): Promise<IResponse> {
781
+ // In production, lookup token by key hash and delete
782
+ return {
783
+ status: 200,
784
+ headers: { 'Content-Type': 'application/json' },
785
+ body: { ok: true },
786
+ };
787
+ }
788
+
789
+ private async handleDistTags(
790
+ method: string,
791
+ packageName: string,
792
+ tag: string | undefined,
793
+ body: any,
794
+ token: IAuthToken | null
795
+ ): Promise<IResponse> {
796
+ const packument = await this.storage.getNpmPackument(packageName);
797
+ if (!packument) {
798
+ return {
799
+ status: 404,
800
+ headers: {},
801
+ body: this.createError('E404', 'Package not found'),
802
+ };
803
+ }
804
+
805
+ // GET /-/package/{package}/dist-tags
806
+ if (method === 'GET' && !tag) {
807
+ return {
808
+ status: 200,
809
+ headers: { 'Content-Type': 'application/json' },
810
+ body: packument['dist-tags'] || {},
811
+ };
812
+ }
813
+
814
+ // PUT /-/package/{package}/dist-tags/{tag}
815
+ if (method === 'PUT' && tag) {
816
+ if (!await this.checkPermission(token, packageName, 'write')) {
817
+ return {
818
+ status: 401,
819
+ headers: {},
820
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
821
+ };
822
+ }
823
+
824
+ if (typeof body !== 'string') {
825
+ return {
826
+ status: 400,
827
+ headers: {},
828
+ body: this.createError('EBADREQUEST', 'Version string required'),
829
+ };
830
+ }
831
+
832
+ packument['dist-tags'] = packument['dist-tags'] || {};
833
+ packument['dist-tags'][tag] = body;
834
+ await this.storage.putNpmPackument(packageName, packument);
835
+
836
+ return {
837
+ status: 200,
838
+ headers: { 'Content-Type': 'application/json' },
839
+ body: { ok: true },
840
+ };
841
+ }
842
+
843
+ // DELETE /-/package/{package}/dist-tags/{tag}
844
+ if (method === 'DELETE' && tag) {
845
+ if (!await this.checkPermission(token, packageName, 'write')) {
846
+ return {
847
+ status: 401,
848
+ headers: {},
849
+ body: this.createError('EUNAUTHORIZED', 'Unauthorized'),
850
+ };
851
+ }
852
+
853
+ if (tag === 'latest') {
854
+ return {
855
+ status: 403,
856
+ headers: {},
857
+ body: this.createError('EFORBIDDEN', 'Cannot delete latest tag'),
858
+ };
859
+ }
860
+
861
+ if (packument['dist-tags'] && packument['dist-tags'][tag]) {
862
+ delete packument['dist-tags'][tag];
863
+ await this.storage.putNpmPackument(packageName, packument);
864
+ }
865
+
866
+ return {
867
+ status: 200,
868
+ headers: { 'Content-Type': 'application/json' },
869
+ body: { ok: true },
870
+ };
871
+ }
872
+
873
+ return {
874
+ status: 405,
875
+ headers: {},
876
+ body: this.createError('EBADREQUEST', 'Method not allowed'),
877
+ };
878
+ }
879
+
880
+ // ========================================================================
881
+ // HELPER METHODS
882
+ // ========================================================================
883
+
884
+ private createError(code: string, message: string): INpmError {
885
+ return {
886
+ error: code,
887
+ reason: message,
888
+ };
889
+ }
890
+ }