@vltpkg/vsr 0.0.0-27 → 0.0.0-28

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 (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +8 -6
  6. package/dist/index.js +3 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +49 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
@@ -0,0 +1,2823 @@
1
+ import * as semver from 'semver'
2
+ import validate from 'validate-npm-package-name'
3
+ import { createRoute, z } from '@hono/zod-openapi'
4
+ import { URL, PROXY, PROXY_URL } from '../../config.ts'
5
+ import {
6
+ getUpstreamConfig,
7
+ buildUpstreamUrl,
8
+ } from '../utils/upstream.ts'
9
+ import { createFile, slimManifest } from '../utils/packages.ts'
10
+ import { getCachedPackageWithRefresh } from '../utils/cache.ts'
11
+ import type {
12
+ HonoContext,
13
+ SlimmedManifest,
14
+ ParsedPackage,
15
+ PackageManifest,
16
+ DatabaseOperations,
17
+ } from '../../types.ts'
18
+
19
+ // Helper function to get typed database from context
20
+ function getDb(c: HonoContext): DatabaseOperations {
21
+ return c.get('db')
22
+ }
23
+
24
+ interface SlimPackumentContext {
25
+ protocol?: string
26
+ host?: string
27
+ upstream?: string
28
+ }
29
+
30
+ interface _TarballRequestParams {
31
+ scope: string
32
+ pkg: string
33
+ }
34
+
35
+ interface _PackageRouteSegments {
36
+ upstream?: string
37
+ packageName: string
38
+ segments: string[]
39
+ }
40
+
41
+ interface _UpstreamData {
42
+ 'dist-tags'?: Record<string, string>
43
+ versions?: Record<string, unknown>
44
+ time?: Record<string, string>
45
+ [key: string]: unknown
46
+ }
47
+
48
+ interface PackageData {
49
+ name: string
50
+ 'dist-tags': Record<string, string>
51
+ versions: Record<string, unknown>
52
+ time: Record<string, string>
53
+ }
54
+
55
+ // Use the existing ParsedVersion interface from types.ts instead
56
+
57
+ interface _CachedResult {
58
+ fromCache?: boolean
59
+ package?: PackageData
60
+ }
61
+
62
+ interface _SlimmedManifest {
63
+ name: string
64
+ version: string
65
+ dependencies?: Record<string, string>
66
+ peerDependencies?: Record<string, string>
67
+ optionalDependencies?: Record<string, string>
68
+ peerDependenciesMeta?: Record<string, string>
69
+ bin?: Record<string, string>
70
+ engines?: Record<string, string>
71
+ dist: {
72
+ tarball: string
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ultra-aggressive slimming for packument versions (used in /:upstream/:pkg responses)
78
+ * Only includes the absolute minimum fields needed for dependency resolution and installation
79
+ * Fields included: name, version, dependencies, peerDependencies, optionalDependencies, peerDependenciesMeta, bin, engines, dist.tarball
80
+ */
81
+ export async function slimPackumentVersion(
82
+ manifest: any,
83
+ context: SlimPackumentContext = {},
84
+ ): Promise<SlimmedManifest | null> {
85
+ try {
86
+ if (!manifest) return null
87
+
88
+ // Parse manifest if it's a string
89
+
90
+ let parsed: Record<string, any>
91
+ if (typeof manifest === 'string') {
92
+ try {
93
+ parsed = JSON.parse(manifest) as Record<string, any>
94
+ } catch (_e) {
95
+ parsed = manifest as unknown as Record<string, any>
96
+ }
97
+ } else {
98
+ parsed = manifest as Record<string, any>
99
+ }
100
+
101
+ // For packuments, only include the most essential fields
102
+ const slimmed: _SlimmedManifest = {
103
+ name: parsed.name as string,
104
+ version: parsed.version as string,
105
+ dependencies: (parsed.dependencies ?? {}) as Record<
106
+ string,
107
+ string
108
+ >,
109
+ peerDependencies: (parsed.peerDependencies ?? {}) as Record<
110
+ string,
111
+ string
112
+ >,
113
+ optionalDependencies: (parsed.optionalDependencies ??
114
+ {}) as Record<string, string>,
115
+ peerDependenciesMeta: (parsed.peerDependenciesMeta ??
116
+ {}) as Record<string, string>,
117
+ bin: (parsed.bin ?? {}) as Record<string, string>,
118
+ engines: (parsed.engines ?? {}) as Record<string, string>,
119
+ dist: {
120
+ tarball: await rewriteTarballUrlIfNeeded(
121
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
122
+ (parsed.dist?.tarball ?? '') as string,
123
+ parsed.name as string,
124
+ parsed.version as string,
125
+ context,
126
+ ),
127
+ },
128
+ }
129
+
130
+ // Remove undefined fields to keep response clean
131
+ Object.keys(slimmed).forEach(key => {
132
+ if (
133
+ key !== 'dist' &&
134
+ key !== 'name' &&
135
+ key !== 'version' &&
136
+ slimmed[key as keyof _SlimmedManifest] === undefined
137
+ ) {
138
+ delete slimmed[key as keyof _SlimmedManifest]
139
+ }
140
+ })
141
+
142
+ // Remove empty objects
143
+
144
+ if (Object.keys(slimmed.dependencies ?? {}).length === 0) {
145
+ delete slimmed.dependencies
146
+ }
147
+
148
+ if (Object.keys(slimmed.peerDependencies ?? {}).length === 0) {
149
+ delete slimmed.peerDependencies
150
+ }
151
+ if (
152
+ Object.keys(slimmed.peerDependenciesMeta ?? {}).length === 0
153
+ ) {
154
+ delete slimmed.peerDependenciesMeta
155
+ }
156
+ if (
157
+ Object.keys(slimmed.optionalDependencies ?? {}).length === 0
158
+ ) {
159
+ delete slimmed.optionalDependencies
160
+ }
161
+
162
+ if (Object.keys(slimmed.engines ?? {}).length === 0) {
163
+ delete slimmed.engines
164
+ }
165
+
166
+ return slimmed as SlimmedManifest
167
+ } catch (_err) {
168
+ // Hono logger will capture the error context automatically
169
+ return null
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Rewrite tarball URLs to point to our registry instead of the original registry
175
+ * Only rewrite if context is provided, otherwise return original URL
176
+ */
177
+ export async function rewriteTarballUrlIfNeeded(
178
+ _originalUrl: string,
179
+ packageName: string,
180
+ version: string,
181
+ context: SlimPackumentContext = {},
182
+ ): Promise<string> {
183
+ try {
184
+ const { upstream, protocol, host } = context
185
+
186
+ if (!upstream || !protocol || !host) {
187
+ // If no context, create a local tarball URL
188
+ return `${URL}/${createFile({ pkg: packageName, version })}`
189
+ }
190
+
191
+ // Create a proper upstream tarball URL that points to our registry
192
+ // Format: https://our-domain/upstream/package/-/package-version.tgz
193
+ // For scoped packages like @scope/package, we need to preserve the full name
194
+ const packageFileName =
195
+ packageName.includes('/') ?
196
+ packageName.split('/').pop() // For @scope/package, use just 'package'
197
+ : packageName // For regular packages, use the full name
198
+
199
+ return `${protocol}://${host}/${upstream}/${packageName}/-/${packageFileName}-${version}.tgz`
200
+ } catch (_err) {
201
+ // Fallback to local URL format
202
+ return `${URL}/${createFile({ pkg: packageName, version })}`
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Helper function to properly decode scoped package names from URL parameters
208
+ * Handles cases where special characters in package names are URL-encoded
209
+ */
210
+ function decodePackageName(
211
+ scope: string,
212
+ pkg?: string,
213
+ ): string | null {
214
+ if (!scope) return null
215
+
216
+ // Decode URL-encoded characters in both scope and pkg
217
+ const decodedScope = decodeURIComponent(scope)
218
+ const decodedPkg = pkg ? decodeURIComponent(pkg) : null
219
+
220
+ // Handle scoped packages correctly
221
+ if (decodedScope.startsWith('@')) {
222
+ // If we have both scope and pkg, combine them
223
+ if (decodedPkg && decodedPkg !== '-') {
224
+ return `${decodedScope}/${decodedPkg}`
225
+ }
226
+
227
+ // If scope contains an encoded slash, it might be the full package name
228
+ if (decodedScope.includes('/')) {
229
+ return decodedScope
230
+ }
231
+
232
+ // Just the scope
233
+ return decodedScope
234
+ } else {
235
+ // Unscoped package - scope is actually the package name
236
+ return decodedScope
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Determines if a package is available only through proxy or is locally published
242
+ * A package is considered proxied if it doesn't exist locally but PROXY is enabled
243
+ */
244
+ function _isProxiedPackage(
245
+ packageData: ParsedPackage | null,
246
+ ): boolean {
247
+ // If the package doesn't exist locally but PROXY is enabled
248
+ if (!packageData && PROXY) {
249
+ return true
250
+ }
251
+
252
+ // If the package is marked as proxied (has a source field indicating where it came from)
253
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
254
+ if (packageData && (packageData as any).source === 'proxy') {
255
+ return true
256
+ }
257
+
258
+ return false
259
+ }
260
+
261
+ export async function getPackageTarball(c: HonoContext) {
262
+ try {
263
+ let { scope, pkg } = c.req.param() as {
264
+ scope: string
265
+ pkg: string
266
+ }
267
+ const acceptsIntegrity = c.req.header('accepts-integrity')
268
+
269
+ // Debug: getPackageTarball called with pkg and path (logged by Hono middleware)
270
+
271
+ // If no route parameters, extract package name from path (for upstream routes)
272
+ if (!scope && !pkg) {
273
+ const path = c.req.path
274
+ const pathSegments = path.split('/').filter(Boolean)
275
+
276
+ // For upstream routes like /npm/lodash/-/lodash-4.17.21.tgz
277
+ const upstream = c.get('upstream')
278
+ if (upstream && pathSegments.length > 1) {
279
+ // Find the /-/ segment
280
+ const tarballIndex = pathSegments.findIndex(
281
+ segment => segment === '-',
282
+ )
283
+ if (tarballIndex > 1) {
284
+ // Package name is the segments between upstream and /-/
285
+ const packageSegments = pathSegments.slice(1, tarballIndex)
286
+ if (
287
+ packageSegments[0]?.startsWith('@') &&
288
+ packageSegments.length > 1
289
+ ) {
290
+ // Scoped package: @scope/package
291
+ pkg = `${packageSegments[0]}/${packageSegments[1]}`
292
+ } else {
293
+ // Regular package
294
+ pkg = packageSegments[0] || ''
295
+ }
296
+ }
297
+ } else {
298
+ // Direct tarball routes (without upstream)
299
+ const tarballIndex = pathSegments.findIndex(
300
+ segment => segment === '-',
301
+ )
302
+ if (tarballIndex > 0) {
303
+ const packageSegments = pathSegments.slice(0, tarballIndex)
304
+ if (
305
+ packageSegments[0]?.startsWith('@') &&
306
+ packageSegments.length > 1
307
+ ) {
308
+ // Scoped package: @scope/package
309
+ pkg = `${packageSegments[0]}/${packageSegments[1]}`
310
+ } else {
311
+ // Regular package
312
+ pkg = packageSegments[0] || ''
313
+ }
314
+ }
315
+ }
316
+ } else {
317
+ // Handle scoped and unscoped packages correctly with URL decoding
318
+ try {
319
+ // For tarball requests, if scope is undefined/null, pkg should contain the package name
320
+ if (!scope || scope === 'undefined') {
321
+ if (!pkg) {
322
+ throw new Error('Missing package name')
323
+ }
324
+ pkg = decodeURIComponent(pkg)
325
+ // Hono middleware logs debug information
326
+ } else {
327
+ const packageName = decodePackageName(scope, pkg)
328
+ if (!packageName) {
329
+ throw new Error('Invalid scoped package name')
330
+ }
331
+ pkg = packageName
332
+ // Hono middleware logs debug information
333
+ }
334
+ } catch (_err) {
335
+ // Hono middleware logs error information
336
+ return c.json({ error: 'Invalid package name' }, 400)
337
+ }
338
+ }
339
+
340
+ // Ensure we have a package name
341
+ if (!pkg) {
342
+ return c.json({ error: 'Invalid package name' }, 400)
343
+ }
344
+
345
+ let tarball = c.req.path.split('/').pop()
346
+ if (!tarball?.endsWith('.tgz')) {
347
+ // Hono middleware logs error information
348
+ return c.json({ error: 'Invalid tarball name' }, 400)
349
+ }
350
+
351
+ // Check if the tarball filename contains a dist-tag like "latest" instead of a version
352
+ // and resolve it to the actual version number
353
+ const packageFileName =
354
+ pkg.includes('/') ? pkg.split('/').pop() : pkg
355
+ const prefix = `${packageFileName}-`
356
+ const suffix = '.tgz'
357
+
358
+ if (tarball.startsWith(prefix) && tarball.endsWith(suffix)) {
359
+ const versionFromTarball = tarball.slice(
360
+ prefix.length,
361
+ -suffix.length,
362
+ )
363
+
364
+ // If version looks like a dist-tag (not a semver), try to resolve it
365
+ if (
366
+ versionFromTarball &&
367
+ !semver.valid(versionFromTarball) &&
368
+ !semver.validRange(versionFromTarball)
369
+ ) {
370
+ // This might be a dist-tag like "latest", try to resolve it
371
+ try {
372
+ const upstream = c.get('upstream')
373
+ if (upstream) {
374
+ // Get packument data to find the actual version for this dist-tag
375
+ const upstreamConfig = getUpstreamConfig(upstream)
376
+ if (upstreamConfig) {
377
+ const packumentUrl = buildUpstreamUrl(
378
+ upstreamConfig,
379
+ pkg,
380
+ )
381
+ const packumentResponse = await fetch(packumentUrl, {
382
+ method: 'GET',
383
+ headers: {
384
+ 'User-Agent': 'vlt-serverless-registry',
385
+ Accept: 'application/json',
386
+ },
387
+ })
388
+
389
+ if (packumentResponse.ok) {
390
+ const packumentData = await packumentResponse.json()
391
+ const distTags =
392
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
393
+ ((packumentData as Record<string, any>)[
394
+ 'dist-tags'
395
+ ] as Record<string, string>) || {}
396
+ const actualVersion = distTags[versionFromTarball]
397
+
398
+ if (actualVersion) {
399
+ // Update the tarball filename with the actual version
400
+ tarball = `${packageFileName}-${actualVersion}.tgz`
401
+ }
402
+ }
403
+ }
404
+ }
405
+ } catch (_error) {
406
+ // If dist-tag resolution fails, continue with original tarball name
407
+ // and let the upstream handle it (which will likely 404)
408
+ }
409
+ }
410
+ }
411
+
412
+ const filename = `${pkg}/${tarball}`
413
+
414
+ // If integrity checking is requested, get the expected integrity from manifest
415
+ let expectedIntegrity: string | null = null
416
+ if (acceptsIntegrity) {
417
+ try {
418
+ // Extract version from tarball name
419
+ const versionMatch = new RegExp(
420
+ `${pkg.split('/').pop()}-(.*)\\.tgz`,
421
+ ).exec(tarball)
422
+ if (versionMatch) {
423
+ const version = versionMatch[1]
424
+ const spec = `${pkg}@${version}`
425
+
426
+ // Get the version from DB
427
+ const versionData = await getDb(c).getVersion(spec)
428
+
429
+ if (versionData?.manifest) {
430
+ let manifest: any
431
+ try {
432
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
433
+ manifest =
434
+ typeof versionData.manifest === 'string' ?
435
+ JSON.parse(versionData.manifest)
436
+ : versionData.manifest
437
+ } catch (_e) {
438
+ // Hono middleware logs error information
439
+ }
440
+
441
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
442
+ if (manifest?.dist?.integrity) {
443
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
444
+ expectedIntegrity = manifest.dist.integrity
445
+ // Hono middleware logs integrity information
446
+
447
+ // Simple string comparison with the provided integrity
448
+ if (acceptsIntegrity !== expectedIntegrity) {
449
+ // Hono middleware logs integrity error
450
+ return c.json(
451
+ {
452
+ error: 'Integrity check failed',
453
+ code: 'EINTEGRITY',
454
+ expected: expectedIntegrity,
455
+ actual: acceptsIntegrity,
456
+ },
457
+ 400,
458
+ )
459
+ }
460
+
461
+ // Hono middleware logs integrity verification
462
+ } else {
463
+ // Hono middleware logs integrity information
464
+ }
465
+ } else {
466
+ // Hono middleware logs integrity information
467
+ }
468
+ }
469
+ } catch (_err) {
470
+ // Hono middleware logs integrity error
471
+ }
472
+ }
473
+
474
+ // Try to get the file from our bucket first
475
+ try {
476
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
477
+ const file = await c.env.BUCKET.get(filename)
478
+
479
+ // If file exists locally, stream it
480
+ if (file) {
481
+ try {
482
+ // We've already verified integrity above if needed
483
+ const headers = new Headers({
484
+ 'Content-Type': 'application/octet-stream',
485
+ 'Cache-Control': 'public, max-age=31536000',
486
+ })
487
+
488
+ return new Response(
489
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
490
+ file.body,
491
+ {
492
+ status: 200,
493
+ headers,
494
+ },
495
+ )
496
+ } catch (_err) {
497
+ // Hono middleware logs streaming error
498
+ // Fall through to proxy if available
499
+ }
500
+ }
501
+ } catch (_err) {
502
+ // Hono middleware logs storage error
503
+ // Continue to proxy if available, otherwise fall through to 404
504
+ }
505
+
506
+ // If file doesn't exist and proxying is enabled, try to get it from upstream
507
+ if (PROXY) {
508
+ try {
509
+ // Construct the correct URL for scoped and unscoped packages
510
+ const tarballPath =
511
+ pkg.includes('/') ?
512
+ `${pkg}/-/${tarball}`
513
+ : `${pkg}/-/${tarball}`
514
+
515
+ // Get the upstream configuration
516
+ const upstream = c.get('upstream')
517
+ let source: string
518
+
519
+ if (upstream) {
520
+ // Use upstream-specific URL
521
+ const upstreamConfig = getUpstreamConfig(upstream)
522
+ if (!upstreamConfig) {
523
+ return c.json(
524
+ { error: `Unknown upstream: ${upstream}` },
525
+ 400,
526
+ )
527
+ }
528
+ source = `${upstreamConfig.url}/${tarballPath}`
529
+ } else {
530
+ // Use default proxy URL
531
+ source = `${PROXY_URL}/${tarballPath}`
532
+ }
533
+
534
+ // Hono middleware logs proxy information
535
+
536
+ // First do a HEAD request to check size
537
+ const headResponse = await fetch(source, {
538
+ method: 'HEAD',
539
+ headers: {
540
+ 'User-Agent': 'vlt-serverless-registry',
541
+ },
542
+ })
543
+
544
+ if (!headResponse.ok) {
545
+ // Hono middleware logs proxy error
546
+ return c.json(
547
+ { error: 'Failed to check package size' },
548
+ 502,
549
+ )
550
+ }
551
+
552
+ const contentLength = parseInt(
553
+ headResponse.headers.get('content-length') || '0',
554
+ 10,
555
+ )
556
+
557
+ // Get the package response first, since we'll need it for all size cases
558
+ const response = await fetch(source, {
559
+ headers: {
560
+ Accept: 'application/octet-stream',
561
+ 'User-Agent': 'vlt-serverless-registry',
562
+ },
563
+ })
564
+
565
+ if (!response.ok || !response.body) {
566
+ // Hono middleware logs proxy error
567
+ return c.json({ error: 'Failed to fetch package' }, 502)
568
+ }
569
+
570
+ // For very large packages (100MB+), stream directly to client without storing
571
+ if (contentLength > 100_000_000) {
572
+ // Hono middleware logs large package streaming
573
+
574
+ const readable = response.body
575
+
576
+ // Return the stream to the client immediately
577
+ return new Response(readable, {
578
+ status: 200,
579
+ headers: new Headers({
580
+ 'Content-Type': 'application/octet-stream',
581
+ 'Content-Length': contentLength.toString(),
582
+ 'Cache-Control': 'public, max-age=31536000',
583
+ }),
584
+ })
585
+ }
586
+
587
+ // For medium-sized packages (10-100MB), stream directly to client and store async
588
+ if (contentLength > 10_000_000) {
589
+ // Clone the response since we'll need it twice
590
+ const [clientResponse, storageResponse] =
591
+ response.body.tee()
592
+
593
+ // No integrity check when storing proxied packages
594
+ c.executionCtx.waitUntil(
595
+ (async () => {
596
+ try {
597
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
598
+ await c.env.BUCKET.put(filename, storageResponse, {
599
+ httpMetadata: {
600
+ contentType: 'application/octet-stream',
601
+ cacheControl: 'public, max-age=31536000',
602
+ // Store the integrity value if we have it from the manifest
603
+ ...(expectedIntegrity && {
604
+ integrity: expectedIntegrity,
605
+ }),
606
+ },
607
+ })
608
+ // Hono middleware logs successful storage
609
+ } catch (_err) {
610
+ // Hono middleware logs storage error
611
+ }
612
+ })(),
613
+ )
614
+
615
+ // Stream directly to client
616
+ return new Response(clientResponse, {
617
+ status: 200,
618
+ headers: new Headers({
619
+ 'Content-Type': 'application/octet-stream',
620
+ 'Content-Length': contentLength.toString(),
621
+ 'Cache-Control': 'public, max-age=31536000',
622
+ }),
623
+ })
624
+ }
625
+
626
+ // For smaller packages, we can use the tee() approach safely
627
+ const [stream1, stream2] = response.body.tee()
628
+
629
+ // Store in R2 bucket asynchronously without integrity check for proxied packages
630
+ c.executionCtx.waitUntil(
631
+ (async () => {
632
+ try {
633
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
634
+ await c.env.BUCKET.put(filename, stream1, {
635
+ httpMetadata: {
636
+ contentType: 'application/octet-stream',
637
+ cacheControl: 'public, max-age=31536000',
638
+ // Store the integrity value if we have it from the manifest
639
+ ...(expectedIntegrity && {
640
+ integrity: expectedIntegrity,
641
+ }),
642
+ },
643
+ })
644
+ // Hono middleware logs successful storage
645
+ } catch (_err) {
646
+ // Hono middleware logs storage error
647
+ }
648
+ })(),
649
+ )
650
+
651
+ // Return the second stream to the client immediately
652
+ return new Response(stream2, {
653
+ status: 200,
654
+ headers: new Headers({
655
+ 'Content-Type': 'application/octet-stream',
656
+ 'Content-Length': contentLength.toString(),
657
+ 'Cache-Control': 'public, max-age=31536000',
658
+ }),
659
+ })
660
+ } catch (_err) {
661
+ // Hono middleware logs network error
662
+ return c.json(
663
+ { error: 'Failed to contact upstream registry' },
664
+ 502,
665
+ )
666
+ }
667
+ }
668
+
669
+ return c.json({ error: 'Not found' }, 404)
670
+ } catch (_err) {
671
+ // Hono middleware logs general error
672
+ return c.json({ error: 'Internal server error' }, 500)
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Get a single package version manifest
678
+ */
679
+ export async function getPackageManifest(c: HonoContext) {
680
+ try {
681
+ let { scope, pkg } = c.req.param() as {
682
+ scope: string
683
+ pkg: string
684
+ }
685
+
686
+ // If no route parameters, extract package name from path (for upstream routes)
687
+ if (!scope && !pkg) {
688
+ const path = c.req.path
689
+ const pathSegments = path.split('/').filter(Boolean)
690
+
691
+ // For upstream routes like /npm/express/4.18.2
692
+ const upstream = c.get('upstream')
693
+ if (upstream && pathSegments.length > 2) {
694
+ // Package name starts after upstream, version is last segment
695
+ const packageSegments = pathSegments.slice(1, -1) // Remove upstream and version
696
+ if (
697
+ packageSegments[0]?.startsWith('@') &&
698
+ packageSegments.length > 1
699
+ ) {
700
+ // Scoped package: @scope/package
701
+ pkg = `${packageSegments[0]}/${packageSegments[1]}`
702
+ } else {
703
+ // Regular package
704
+ pkg = packageSegments[0] || ''
705
+ }
706
+ } else if (pathSegments.length > 1) {
707
+ // Direct manifest routes (without upstream)
708
+ const packageSegments = pathSegments.slice(0, -1) // Remove version
709
+ if (
710
+ packageSegments[0]?.startsWith('@') &&
711
+ packageSegments.length > 1
712
+ ) {
713
+ // Scoped package: @scope/package
714
+ pkg = `${packageSegments[0]}/${packageSegments[1]}`
715
+ } else {
716
+ // Regular package
717
+ pkg = packageSegments[0] || ''
718
+ }
719
+ }
720
+ } else {
721
+ // Handle scoped packages correctly
722
+ try {
723
+ if (scope && pkg) {
724
+ // Scoped package
725
+ const packageName =
726
+ scope.startsWith('@') ?
727
+ `${scope}/${pkg}`
728
+ : `@${scope}/${pkg}`
729
+ pkg = packageName
730
+ } else if (scope) {
731
+ // Unscoped package (scope is actually the package name)
732
+ pkg = scope
733
+ }
734
+
735
+ if (!pkg) {
736
+ throw new Error('Invalid package name')
737
+ }
738
+ } catch (_err) {
739
+ // Hono middleware logs error information
740
+ return c.json({ error: 'Invalid package name' }, 400)
741
+ }
742
+ }
743
+
744
+ // Extract version from URL path
745
+ const pathParts = c.req.path.split('/')
746
+ const versionIndex = pathParts.findIndex(part => part === pkg) + 1
747
+ let version = pathParts[versionIndex] || 'latest'
748
+
749
+ // Decode URL-encoded version (e.g., %3E%3D1.0.0%20%3C2.0.0 becomes >=1.0.0 <2.0.0)
750
+ version = decodeURIComponent(version)
751
+
752
+ // Hono middleware logs manifest request information
753
+
754
+ // If it's a semver range, try to resolve it to a specific version
755
+ let resolvedVersion = version
756
+ if (semver.validRange(version) && !semver.valid(version)) {
757
+ // This is a range, try to find the best matching version
758
+ try {
759
+ const packageData = await getDb(c).getPackage(pkg)
760
+ if (packageData) {
761
+ const versions = await getDb(c).getVersionsByPackage(pkg)
762
+
763
+ if (versions.length > 0) {
764
+ const availableVersions = versions.map(v => v.version)
765
+
766
+ const bestMatch = semver.maxSatisfying(
767
+ availableVersions,
768
+ version,
769
+ )
770
+ if (bestMatch) {
771
+ resolvedVersion = bestMatch
772
+ // Hono middleware logs version resolution
773
+ }
774
+ }
775
+ }
776
+ } catch (_err) {
777
+ // Hono middleware logs version range error
778
+ }
779
+ }
780
+
781
+ // Get the version from our database
782
+ const versionData = await c
783
+ .get('db')
784
+ .getVersion(`${pkg}@${resolvedVersion}`)
785
+
786
+ if (versionData) {
787
+ // Convert the full manifest to a slimmed version for the response
788
+
789
+ const slimmedManifest = slimManifest(versionData.manifest)
790
+
791
+ // Ensure we have correct name, version and tarball URL
792
+
793
+ const ret = {
794
+ ...slimmedManifest,
795
+ name: pkg,
796
+
797
+ version: resolvedVersion,
798
+
799
+ dist: {
800
+ ...slimmedManifest.dist,
801
+ tarball: `${URL}/${createFile({ pkg, version: resolvedVersion })}`,
802
+ },
803
+ }
804
+
805
+ // Set proper headers for npm/bun
806
+ c.header('Content-Type', 'application/json')
807
+ c.header('Cache-Control', 'public, max-age=300') // 5 minute cache
808
+
809
+ return c.json(ret, 200)
810
+ }
811
+
812
+ // If not found locally and we have an upstream, try to fetch from upstream
813
+ const upstream = c.get('upstream')
814
+ if (upstream && PROXY) {
815
+ try {
816
+ // Get the upstream configuration
817
+ const upstreamConfig = getUpstreamConfig(upstream)
818
+ if (!upstreamConfig) {
819
+ return c.json(
820
+ { error: `Unknown upstream: ${upstream}` },
821
+ 400,
822
+ )
823
+ }
824
+
825
+ const upstreamUrl = `${upstreamConfig.url}/${pkg}/${resolvedVersion}`
826
+
827
+ const response = await fetch(upstreamUrl, {
828
+ method: 'GET',
829
+ headers: {
830
+ 'User-Agent': 'vlt-registry/1.0.0',
831
+ Accept: 'application/json',
832
+ },
833
+ })
834
+
835
+ if (!response.ok) {
836
+ if (response.status === 404) {
837
+ return c.json({ error: 'Version not found' }, 404)
838
+ }
839
+ return c.json(
840
+ { error: 'Failed to fetch upstream manifest' },
841
+ 502,
842
+ )
843
+ }
844
+
845
+ const upstreamManifest = await response.json()
846
+
847
+ // Rewrite tarball URL to point to our registry with upstream prefix
848
+
849
+ if (
850
+ upstreamManifest &&
851
+ typeof upstreamManifest === 'object' &&
852
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
853
+ (upstreamManifest as any).dist?.tarball
854
+ ) {
855
+ const requestUrl = new globalThis.URL(c.req.url)
856
+ const protocol = requestUrl.protocol.slice(0, -1) // Remove trailing ':'
857
+ const host = c.req.header('host') ?? 'localhost:1337'
858
+ const context = {
859
+ protocol,
860
+ host,
861
+ upstream,
862
+ }
863
+
864
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
865
+ ;(upstreamManifest as any).dist.tarball =
866
+ await rewriteTarballUrlIfNeeded(
867
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
868
+ String((upstreamManifest as any).dist.tarball),
869
+ pkg,
870
+ resolvedVersion,
871
+ context,
872
+ )
873
+ }
874
+
875
+ // Set proper headers for npm/bun
876
+ c.header('Content-Type', 'application/json')
877
+ c.header('Cache-Control', 'public, max-age=300') // 5 minute cache
878
+
879
+ return c.json(upstreamManifest as Record<string, any>, 200)
880
+ } catch (_err) {
881
+ // Fall through to 404
882
+ }
883
+ }
884
+
885
+ return c.json({ error: 'Version not found' }, 404)
886
+ } catch (_err) {
887
+ // Hono middleware logs error information
888
+ return c.json({ error: 'Internal server error' }, 500)
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Get package dist-tags
894
+ */
895
+ export async function getPackageDistTags(c: HonoContext) {
896
+ try {
897
+ const scope = c.req.param('scope')
898
+ const pkg = c.req.param('pkg')
899
+ const tag = c.req.param('tag')
900
+
901
+ // Determine the package name based on route parameters
902
+ let packageName: string | null = null
903
+ if (scope && pkg) {
904
+ // Scoped package: /-/package/:scope%2f:pkg/dist-tags
905
+ packageName = decodePackageName(scope, pkg)
906
+ } else if (pkg) {
907
+ // Unscoped package: /-/package/:pkg/dist-tags
908
+ packageName = decodeURIComponent(pkg)
909
+ }
910
+
911
+ if (!packageName) {
912
+ return c.json({ error: 'Invalid package name' }, 400)
913
+ }
914
+
915
+ // Set response headers
916
+ c.header('Content-Type', 'application/json')
917
+ c.header('Cache-Control', 'no-cache, no-store, must-revalidate')
918
+
919
+ const packageData = await getDb(c).getPackage(packageName)
920
+
921
+ if (!packageData) {
922
+ return c.json({ error: 'Package not found' }, 404)
923
+ }
924
+
925
+ // Check if this package is proxied and should not allow dist-tag operations
926
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
927
+ if ((packageData as any).source === 'proxy') {
928
+ return c.json(
929
+ {
930
+ error:
931
+ 'Cannot perform dist-tag operations on proxied packages',
932
+ },
933
+ 403,
934
+ )
935
+ }
936
+
937
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
938
+ const distTags = packageData.tags ?? {}
939
+
940
+ // If no tag specified, return all tags
941
+ if (!tag) {
942
+ // If no tags exist, return default latest tag
943
+ if (Object.keys(distTags).length === 0) {
944
+ return c.json({ latest: '' })
945
+ }
946
+ return c.json(distTags)
947
+ }
948
+
949
+ // Return specific tag
950
+ const tagValue = distTags[tag]
951
+ if (tagValue !== undefined) {
952
+ return c.json({ [tag]: tagValue })
953
+ }
954
+ return c.json({ error: `Tag '${tag}' not found` }, 404)
955
+ } catch (_err) {
956
+ // Hono middleware logs error information
957
+ return c.json({ error: 'Internal server error' }, 500)
958
+ }
959
+ }
960
+
961
+ /**
962
+ * Set/update a package dist-tag
963
+ */
964
+ export async function putPackageDistTag(c: HonoContext) {
965
+ try {
966
+ const scope = c.req.param('scope')
967
+ const pkg = c.req.param('pkg')
968
+ const tag = c.req.param('tag')
969
+
970
+ // Determine the package name based on route parameters
971
+ let packageName: string | null = null
972
+ if (scope && pkg) {
973
+ // Scoped package: /-/package/:scope%2f:pkg/dist-tags/:tag
974
+ packageName = decodePackageName(scope, pkg)
975
+ } else if (pkg) {
976
+ // Unscoped package: /-/package/:pkg/dist-tags/:tag
977
+ packageName = decodeURIComponent(pkg)
978
+ }
979
+
980
+ if (!packageName) {
981
+ return c.json({ error: 'Invalid package name' }, 400)
982
+ }
983
+
984
+ const version = await c.req.text()
985
+
986
+ if (!version || !tag) {
987
+ return c.json({ error: 'Tag and version are required' }, 400)
988
+ }
989
+
990
+ // Validate that tag name is not a valid semver range
991
+ if (semver.validRange(tag)) {
992
+ return c.json(
993
+ {
994
+ error: `Tag name must not be a valid SemVer range: ${tag}`,
995
+ },
996
+ 400,
997
+ )
998
+ }
999
+
1000
+ const packageData = await getDb(c).getPackage(packageName)
1001
+
1002
+ if (!packageData) {
1003
+ return c.json({ error: 'Package not found' }, 404)
1004
+ }
1005
+
1006
+ // Check if this package is proxied and should not allow dist-tag operations
1007
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1008
+ if ((packageData as any).source === 'proxy') {
1009
+ return c.json(
1010
+ {
1011
+ error:
1012
+ 'Cannot perform dist-tag operations on proxied packages',
1013
+ },
1014
+ 403,
1015
+ )
1016
+ }
1017
+
1018
+ // Validate that the version exists
1019
+ const versionSpec = `${packageName}@${version}`
1020
+ const versionData = await getDb(c).getVersion(versionSpec)
1021
+ if (!versionData) {
1022
+ return c.json(
1023
+ {
1024
+ error: `Version ${version} not found`,
1025
+ },
1026
+ 404,
1027
+ )
1028
+ }
1029
+
1030
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1031
+ const distTags = packageData.tags ?? {}
1032
+ distTags[tag] = version
1033
+
1034
+ await getDb(c).upsertPackage(packageName, distTags)
1035
+
1036
+ return c.json(distTags, 201)
1037
+ } catch (_err) {
1038
+ // Hono middleware logs error information
1039
+ return c.json({ error: 'Internal server error' }, 500)
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Delete a package dist-tag
1045
+ */
1046
+ export async function deletePackageDistTag(c: HonoContext) {
1047
+ try {
1048
+ const scope = c.req.param('scope')
1049
+ const pkg = c.req.param('pkg')
1050
+ const tag = c.req.param('tag')
1051
+
1052
+ // Determine the package name based on route parameters
1053
+ let packageName: string | null = null
1054
+ if (scope && pkg) {
1055
+ // Scoped package: /-/package/:scope%2f:pkg/dist-tags/:tag
1056
+ packageName = decodePackageName(scope, pkg)
1057
+ } else if (pkg) {
1058
+ // Unscoped package: /-/package/:pkg/dist-tags/:tag
1059
+ packageName = decodeURIComponent(pkg)
1060
+ }
1061
+
1062
+ if (!packageName) {
1063
+ return c.json({ error: 'Invalid package name' }, 400)
1064
+ }
1065
+
1066
+ // Tag is always provided by the route parameter
1067
+ if (!tag) {
1068
+ return c.json({ error: 'Tag is required' }, 400)
1069
+ }
1070
+
1071
+ if (tag === 'latest') {
1072
+ return c.json({ error: 'Cannot delete the "latest" tag' }, 400)
1073
+ }
1074
+
1075
+ const packageData = await getDb(c).getPackage(packageName)
1076
+
1077
+ if (!packageData) {
1078
+ return c.json({ error: 'Package not found' }, 404)
1079
+ }
1080
+
1081
+ // Check if this package is proxied and should not allow dist-tag operations
1082
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1083
+ if ((packageData as any).source === 'proxy') {
1084
+ return c.json(
1085
+ {
1086
+ error:
1087
+ 'Cannot perform dist-tag operations on proxied packages',
1088
+ },
1089
+ 403,
1090
+ )
1091
+ }
1092
+
1093
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1094
+ const distTags = packageData.tags ?? {}
1095
+
1096
+ const tagValue = distTags[tag]
1097
+ if (tagValue === undefined) {
1098
+ return c.json({ error: `Tag ${tag} not found` }, 404)
1099
+ }
1100
+
1101
+ delete distTags[tag]
1102
+
1103
+ await getDb(c).upsertPackage(packageName, distTags)
1104
+
1105
+ return c.json(distTags)
1106
+ } catch (_err) {
1107
+ // Hono middleware logs error information
1108
+ return c.json({ error: 'Internal server error' }, 500)
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Handle general package routes (packument, manifest, tarball)
1114
+ */
1115
+ export async function handlePackageRoute(c: HonoContext) {
1116
+ try {
1117
+ const path = c.req.path
1118
+
1119
+ // Check if this is a tarball request
1120
+ if (path.includes('/-/')) {
1121
+ return await getPackageTarball(c)
1122
+ }
1123
+
1124
+ // Check if this has a version (manifest request)
1125
+ const pathParts = path.split('/').filter(Boolean) // Remove empty strings
1126
+
1127
+ // For upstream routes like /npm/lodash/1.0.0, we need to account for the upstream prefix
1128
+ const upstream = c.get('upstream')
1129
+ let packageStartIndex = 0
1130
+
1131
+ if (upstream) {
1132
+ // Skip the upstream name in the path
1133
+ packageStartIndex = 1
1134
+ }
1135
+
1136
+ // Check if we have a version segment after the package name
1137
+ let hasVersionSegment = false
1138
+ if (pathParts.length > packageStartIndex + 1) {
1139
+ const potentialVersion = pathParts[packageStartIndex + 1]
1140
+ // Handle scoped packages: @scope/package/version
1141
+ if (pathParts[packageStartIndex]?.startsWith('@')) {
1142
+ // For scoped packages, version is at index packageStartIndex + 2
1143
+ const versionSegment = pathParts[packageStartIndex + 2]
1144
+ hasVersionSegment =
1145
+ pathParts.length > packageStartIndex + 2 &&
1146
+ Boolean(versionSegment && !versionSegment.startsWith('-'))
1147
+ } else {
1148
+ // For regular packages, version is at index packageStartIndex + 1
1149
+ hasVersionSegment = Boolean(
1150
+ potentialVersion && !potentialVersion.startsWith('-'),
1151
+ )
1152
+ }
1153
+ }
1154
+
1155
+ if (hasVersionSegment) {
1156
+ return await getPackageManifest(c)
1157
+ }
1158
+
1159
+ // Otherwise it's a packument request
1160
+ return await getPackagePackument(c)
1161
+ } catch (_err) {
1162
+ // Hono middleware logs error information
1163
+ return c.json({ error: 'Internal server error' }, 500)
1164
+ }
1165
+ }
1166
+
1167
+ /**
1168
+ * Publish a package (create or update)
1169
+ */
1170
+ export async function publishPackage(c: HonoContext) {
1171
+ try {
1172
+ const pkg = decodeURIComponent(c.req.param('pkg'))
1173
+
1174
+ // Validate package name
1175
+ const validation = validate(pkg)
1176
+ if (!validation.validForNewPackages) {
1177
+ return c.json(
1178
+ {
1179
+ error: 'Invalid package name',
1180
+ reason:
1181
+ validation.errors?.join(', ') ||
1182
+ 'Package name is not valid',
1183
+ },
1184
+ 400,
1185
+ )
1186
+ }
1187
+
1188
+ // Get package data from request body
1189
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1190
+ const packageData = await c.req.json()
1191
+
1192
+ if (!packageData || typeof packageData !== 'object') {
1193
+ return c.json({ error: 'Invalid package data' }, 400)
1194
+ }
1195
+
1196
+ // Extract version information
1197
+
1198
+ const versions =
1199
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-unsafe-member-access
1200
+ (packageData.versions as Record<string, any>) || {}
1201
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-unsafe-member-access
1202
+ const distTags = (packageData['dist-tags'] as Record<
1203
+ string,
1204
+ string
1205
+ >) || {
1206
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1207
+ latest: packageData.version as string,
1208
+ }
1209
+
1210
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1211
+ if (!packageData.version && !Object.keys(versions).length) {
1212
+ return c.json(
1213
+ { error: 'Package must have at least one version' },
1214
+ 400,
1215
+ )
1216
+ }
1217
+
1218
+ // If this is a single version publish, structure it properly
1219
+ if (
1220
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1221
+ packageData.version &&
1222
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1223
+ !versions[packageData.version as string]
1224
+ ) {
1225
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1226
+ const version = packageData.version as string
1227
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1228
+ versions[version] = {
1229
+ ...packageData,
1230
+ name: pkg,
1231
+ version,
1232
+ dist: {
1233
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1234
+ ...(packageData.dist as Record<string, any>),
1235
+ tarball:
1236
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1237
+ (packageData.dist?.tarball as string) ||
1238
+ `${c.req.url.split('/').slice(0, 3).join('/')}/${pkg}/-/${pkg.split('/').pop()}-${version}.tgz`,
1239
+ },
1240
+ }
1241
+ }
1242
+
1243
+ // Store package metadata
1244
+ await getDb(c).upsertPackage(
1245
+ pkg,
1246
+ distTags,
1247
+ new Date().toISOString(),
1248
+ )
1249
+
1250
+ // Store each version
1251
+ const versionPromises = Object.entries(versions).map(
1252
+ ([version, manifest]) => {
1253
+ const typedManifest = manifest as Record<string, any>
1254
+ return getDb(c).upsertVersion(
1255
+ `${pkg}@${version}`,
1256
+ typedManifest as PackageManifest,
1257
+ (typedManifest.publishedAt as string) ||
1258
+ new Date().toISOString(),
1259
+ )
1260
+ },
1261
+ )
1262
+
1263
+ await Promise.all(versionPromises)
1264
+
1265
+ return c.json({ success: true }, 201)
1266
+ } catch (error) {
1267
+ // TODO: Replace with proper logging system
1268
+ // eslint-disable-next-line no-console
1269
+ console.error('Package publish error:', error)
1270
+ return c.json({ error: 'Failed to publish package' }, 500)
1271
+ }
1272
+ }
1273
+
1274
+ export async function getPackagePackument(c: HonoContext) {
1275
+ try {
1276
+ // Try to get name from route parameters first (for direct routes)
1277
+ let name = c.req.param('pkg')
1278
+ const _scope = c.req.param('scope')
1279
+
1280
+ // If no route parameter, extract from path (for upstream routes)
1281
+ if (!name) {
1282
+ const path = c.req.path
1283
+ const pathSegments = path.split('/').filter(Boolean)
1284
+
1285
+ // For upstream routes like /npm/lodash, skip the upstream name
1286
+ const upstream = c.get('upstream')
1287
+ if (upstream && pathSegments.length > 1) {
1288
+ // Handle scoped packages: /npm/@scope/package
1289
+ if (
1290
+ pathSegments[1]?.startsWith('@') &&
1291
+ pathSegments.length > 2
1292
+ ) {
1293
+ name = `${pathSegments[1]}/${pathSegments[2]}`
1294
+ } else {
1295
+ name = pathSegments[1] || ''
1296
+ }
1297
+ } else if (pathSegments.length > 0) {
1298
+ // Handle direct package routes
1299
+ if (
1300
+ pathSegments[0]?.startsWith('@') &&
1301
+ pathSegments.length > 1
1302
+ ) {
1303
+ name = `${pathSegments[0]}/${pathSegments[1] || ''}`
1304
+ } else {
1305
+ name = pathSegments[0] || ''
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ // Get the versionRange query parameter
1311
+ const versionRange = c.req.query('versionRange')
1312
+
1313
+ // Hono middleware logs packument request information
1314
+
1315
+ // Name is always provided by the route parameter or extracted from path
1316
+ if (!name) {
1317
+ return c.json({ error: 'Package name is required' }, 400)
1318
+ }
1319
+
1320
+ // Check if versionRange is a valid semver range
1321
+ const isValidRange =
1322
+ versionRange && semver.validRange(versionRange)
1323
+ const hasInvalidRange = versionRange && !isValidRange
1324
+
1325
+ if (hasInvalidRange) {
1326
+ // Hono middleware logs invalid semver range
1327
+ return c.json(
1328
+ { error: `Invalid semver range: ${versionRange}` },
1329
+ 400,
1330
+ )
1331
+ }
1332
+
1333
+ // Check if this is an explicit upstream route (like /npm/lodash)
1334
+ const explicitUpstream = c.get('upstream')
1335
+
1336
+ // For explicit upstream routes, always use upstream logic
1337
+ // For other routes, check if package exists locally first
1338
+ let localPkg = null
1339
+ if (!explicitUpstream) {
1340
+ localPkg = await getDb(c).getPackage(name)
1341
+ }
1342
+
1343
+ // Use racing cache strategy when:
1344
+ // 1. Explicit upstream is specified (like /npm/lodash)
1345
+ // 2. PROXY is enabled and package doesn't exist locally
1346
+ const upstream =
1347
+ explicitUpstream || (PROXY && !localPkg ? 'npm' : null)
1348
+ if (upstream) {
1349
+ // Hono middleware logs racing cache strategy information
1350
+
1351
+ const fetchUpstreamFn = async () => {
1352
+ // Get the appropriate upstream configuration
1353
+ const upstreamConfig = getUpstreamConfig(upstream)
1354
+ if (!upstreamConfig) {
1355
+ throw new Error(`Unknown upstream: ${upstream}`)
1356
+ }
1357
+
1358
+ const upstreamUrl = buildUpstreamUrl(upstreamConfig, name)
1359
+
1360
+ const response = await fetch(upstreamUrl, {
1361
+ method: 'GET',
1362
+ headers: {
1363
+ 'User-Agent': 'vlt-registry/1.0.0',
1364
+ Accept: 'application/json',
1365
+ },
1366
+ })
1367
+
1368
+ if (!response.ok) {
1369
+ if (response.status === 404) {
1370
+ throw new Error('Package not found')
1371
+ }
1372
+ throw new Error(`Upstream error: ${response.status}`)
1373
+ }
1374
+
1375
+ const upstreamData: _UpstreamData = await response.json()
1376
+
1377
+ // Prepare data for storage with consistent structure
1378
+ const packageData: PackageData = {
1379
+ name,
1380
+ 'dist-tags': upstreamData['dist-tags'] ?? {
1381
+ latest:
1382
+ Object.keys(upstreamData.versions ?? {}).pop() ?? '',
1383
+ },
1384
+ versions: {},
1385
+ time: {
1386
+ modified:
1387
+ upstreamData.time?.modified ?? new Date().toISOString(),
1388
+ },
1389
+ }
1390
+
1391
+ // Store timing information for each version
1392
+ if (upstreamData.time) {
1393
+ Object.entries(upstreamData.time).forEach(
1394
+ ([version, time]) => {
1395
+ if (version !== 'modified' && version !== 'created') {
1396
+ packageData.time[version] = time
1397
+ }
1398
+ },
1399
+ )
1400
+ }
1401
+
1402
+ // For fast response, only process essential versions synchronously
1403
+ if (upstreamData.versions) {
1404
+ const requestUrl = new globalThis.URL(c.req.url)
1405
+ const protocol = requestUrl.protocol.slice(0, -1) // Remove trailing ':'
1406
+ const host = c.req.header('host') ?? 'localhost:1337'
1407
+ const context = {
1408
+ protocol,
1409
+ host,
1410
+ upstream: upstream,
1411
+ }
1412
+
1413
+ // Get essential versions (latest, plus any matching the version range if specified)
1414
+ const distTags = upstreamData['dist-tags'] ?? {}
1415
+ const latestVersion = distTags.latest
1416
+ const essentialVersions = new Set<string>()
1417
+
1418
+ // Always include latest
1419
+ if (latestVersion) {
1420
+ essentialVersions.add(latestVersion)
1421
+ }
1422
+
1423
+ // If version range specified, include only matching versions (up to 10 for performance)
1424
+ if (isValidRange) {
1425
+ const matchingVersions = Object.keys(
1426
+ upstreamData.versions,
1427
+ )
1428
+ .filter(v => semver.satisfies(v, versionRange))
1429
+ .slice(0, 10) // Limit to 10 versions for performance
1430
+ matchingVersions.forEach(v => essentialVersions.add(v))
1431
+ } else {
1432
+ // For packument requests without version range, include only the 5 most recent versions
1433
+ const sortedVersions = Object.keys(upstreamData.versions)
1434
+ .sort((a, b) => semver.rcompare(a, b))
1435
+ .slice(0, 5)
1436
+ sortedVersions.forEach(v => essentialVersions.add(v))
1437
+ }
1438
+
1439
+ // Process only essential versions synchronously for fast response
1440
+ for (const version of essentialVersions) {
1441
+ const manifest = upstreamData.versions[version]
1442
+ if (manifest) {
1443
+ const slimmedManifest = slimManifest(
1444
+ manifest as PackageManifest,
1445
+ context,
1446
+ )
1447
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1448
+ if (slimmedManifest) {
1449
+ packageData.versions[version] = slimmedManifest
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ // Process all other versions in background for complete caching
1455
+ c.executionCtx.waitUntil(
1456
+ (async () => {
1457
+ try {
1458
+ const allVersionStoragePromises: Promise<unknown>[] =
1459
+ []
1460
+
1461
+ // Process all versions for complete database storage
1462
+ Object.entries(upstreamData.versions ?? {}).forEach(
1463
+ ([version, manifest]) => {
1464
+ const versionSpec = `${name}@${version}`
1465
+ const manifestForStorage = {
1466
+ name: name,
1467
+ version: version,
1468
+ ...slimManifest(
1469
+ manifest as PackageManifest,
1470
+ context,
1471
+ ),
1472
+ } as PackageManifest
1473
+
1474
+ allVersionStoragePromises.push(
1475
+ getDb(c)
1476
+ .upsertCachedVersion(
1477
+ versionSpec,
1478
+ manifestForStorage,
1479
+ upstream,
1480
+ upstreamData.time?.[version] ??
1481
+ new Date().toISOString(),
1482
+ )
1483
+ .catch((_err: unknown) => {
1484
+ // Log error but don't fail the background task
1485
+ }),
1486
+ )
1487
+ },
1488
+ )
1489
+
1490
+ // Store package metadata and all versions
1491
+ await Promise.all([
1492
+ ...allVersionStoragePromises,
1493
+ getDb(c)
1494
+ .upsertCachedPackage(
1495
+ name,
1496
+ packageData['dist-tags'],
1497
+ upstream,
1498
+ packageData.time.modified,
1499
+ )
1500
+ .catch((_err: unknown) => {
1501
+ // Log error but don't fail the background task
1502
+ }),
1503
+ ])
1504
+ } catch (_err) {
1505
+ // Log error but don't fail the request
1506
+ }
1507
+ })(),
1508
+ )
1509
+ }
1510
+
1511
+ // Return just the packageData for caching - the cache function handles storage metadata separately
1512
+ return packageData
1513
+ }
1514
+
1515
+ try {
1516
+ const result = await getCachedPackageWithRefresh(
1517
+ c,
1518
+ name,
1519
+ fetchUpstreamFn,
1520
+ {
1521
+ packumentTtlMinutes: 60, // Cache packuments for 1 hour
1522
+ staleWhileRevalidateMinutes: 240, // Allow stale data for 4 hours while refreshing
1523
+ upstream: upstream,
1524
+ },
1525
+ )
1526
+
1527
+ if (result.fromCache && result.package) {
1528
+ // Hono middleware logs cached data usage
1529
+
1530
+ // If we have cached data, still need to check if we need to filter by version range
1531
+ if (isValidRange) {
1532
+ const filteredVersions: Record<string, unknown> = {}
1533
+ Object.keys(result.package.versions).forEach(version => {
1534
+ if (semver.satisfies(version, versionRange)) {
1535
+ filteredVersions[version] =
1536
+ result.package?.versions[version]
1537
+ }
1538
+ })
1539
+ result.package.versions = filteredVersions
1540
+ }
1541
+
1542
+ return c.json(result.package, 200)
1543
+ } else if (result.package) {
1544
+ // Hono middleware logs fresh upstream data usage
1545
+ return c.json(result.package, 200)
1546
+ } else {
1547
+ return c.json({ error: 'Package data not available' }, 500)
1548
+ }
1549
+ } catch (error) {
1550
+ // Return more specific error codes
1551
+ if ((error as Error).message.includes('Package not found')) {
1552
+ return c.json({ error: `Package '${name}' not found` }, 404)
1553
+ }
1554
+
1555
+ return c.json({ error: 'Failed to fetch package data' }, 502)
1556
+ }
1557
+ }
1558
+
1559
+ // Fallback to original logic when PROXY is disabled
1560
+ const pkg = await getDb(c).getPackage(name)
1561
+ const now = new Date()
1562
+
1563
+ // Initialize the consistent packument response structure
1564
+ const packageData: PackageData = {
1565
+ name,
1566
+ 'dist-tags': { latest: '' },
1567
+ versions: {},
1568
+ time: {
1569
+ modified: now.toISOString(),
1570
+ },
1571
+ }
1572
+
1573
+ if (pkg) {
1574
+ // Update dist-tags from the database
1575
+ packageData['dist-tags'] = pkg.tags
1576
+
1577
+ // Update modified time
1578
+ if (pkg.lastUpdated) {
1579
+ packageData.time.modified = pkg.lastUpdated
1580
+ }
1581
+ }
1582
+
1583
+ // Get all versions for this package
1584
+ try {
1585
+ const allVersions = await getDb(c).getVersionsByPackage(name)
1586
+
1587
+ if (allVersions.length > 0) {
1588
+ // Hono middleware logs version count information
1589
+
1590
+ // Add all versions to the packument, use slimmed manifests
1591
+ for (const versionData of allVersions) {
1592
+ // Extract version from spec (format: "package@version")
1593
+ const versionParts = versionData.spec.split('@')
1594
+ const version = versionParts[versionParts.length - 1]
1595
+
1596
+ // Ensure version is defined before proceeding
1597
+ if (!version) {
1598
+ continue
1599
+ }
1600
+
1601
+ // Skip versions that don't satisfy the version range if provided
1602
+ if (
1603
+ isValidRange &&
1604
+ !semver.satisfies(version, versionRange)
1605
+ ) {
1606
+ continue
1607
+ }
1608
+
1609
+ // Use slimManifest to create a smaller response
1610
+ const slimmedManifest = slimManifest(
1611
+ versionData.manifest as PackageManifest,
1612
+ )
1613
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1614
+ if (slimmedManifest) {
1615
+ packageData.versions[version] = slimmedManifest
1616
+ }
1617
+ packageData.time[version] =
1618
+ versionData.published_at ?? new Date().toISOString()
1619
+ }
1620
+ } else {
1621
+ // Hono middleware logs no versions found
1622
+
1623
+ // Add at least the latest version as a fallback if it satisfies the range
1624
+
1625
+ const latestVersion = packageData['dist-tags'].latest
1626
+ const satisfiesRange =
1627
+ !isValidRange ||
1628
+ (latestVersion ?
1629
+ semver.satisfies(latestVersion, versionRange)
1630
+ : false)
1631
+ if (latestVersion && satisfiesRange) {
1632
+ const versionData = await getDb(c).getVersion(
1633
+ `${name}@${latestVersion}`,
1634
+ )
1635
+ if (versionData) {
1636
+ const slimmedManifest = slimManifest(
1637
+ versionData.manifest as PackageManifest,
1638
+ )
1639
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1640
+ if (slimmedManifest) {
1641
+ packageData.versions[latestVersion] = slimmedManifest
1642
+ }
1643
+ packageData.time[latestVersion] =
1644
+ versionData.published_at ?? new Date().toISOString()
1645
+ } else {
1646
+ // Create a mock version for testing
1647
+ const mockManifest: PackageManifest = {
1648
+ name: name,
1649
+ version: latestVersion,
1650
+ description: `Mock package for ${name}`,
1651
+ dist: {
1652
+ tarball: `${URL}/${name}/-/${name}-${latestVersion}.tgz`,
1653
+ },
1654
+ }
1655
+ packageData.versions[latestVersion] = mockManifest
1656
+ }
1657
+ }
1658
+ }
1659
+ } catch (_err) {
1660
+ // Hono middleware logs database error
1661
+
1662
+ // Create a basic version if none are found
1663
+ const latestVersion = packageData['dist-tags'].latest
1664
+ if (latestVersion) {
1665
+ const mockManifest: PackageManifest = {
1666
+ name: name,
1667
+ version: latestVersion,
1668
+ description: `Package ${name}`,
1669
+ dist: {
1670
+ tarball: `${URL}/${name}/-/${name}-${latestVersion}.tgz`,
1671
+ },
1672
+ }
1673
+ packageData.versions[latestVersion] = mockManifest
1674
+ }
1675
+ }
1676
+
1677
+ return c.json(packageData, 200)
1678
+ } catch (_err) {
1679
+ // Hono middleware logs error information
1680
+ return c.json({ error: 'Internal server error' }, 500)
1681
+ }
1682
+ }
1683
+
1684
+ /**
1685
+ * Handle root package route - checks for local package existence and redirects to upstream if not found
1686
+ * This is used for the `/:pkg` route to handle package discovery
1687
+ */
1688
+ export async function handleRootPackageRoute(c: HonoContext) {
1689
+ const pkg = decodeURIComponent(c.req.param('pkg'))
1690
+
1691
+ // Skip if this looks like a static asset or internal route
1692
+ if (
1693
+ pkg.includes('.') ||
1694
+ pkg.startsWith('-') ||
1695
+ pkg.startsWith('_')
1696
+ ) {
1697
+ // For static assets, let other routes handle this
1698
+ return new Response(null, { status: 404 })
1699
+ }
1700
+
1701
+ // Check if this package exists locally first
1702
+ try {
1703
+ const localPackage = await getDb(c).getPackage(pkg)
1704
+ if (localPackage) {
1705
+ // Package exists locally, handle it with the local package route handler
1706
+ return await handlePackageRoute(c)
1707
+ }
1708
+ } catch (error) {
1709
+ // eslint-disable-next-line no-console
1710
+ console.error('Error checking local package:', error)
1711
+ // Continue to upstream redirect on database error
1712
+ }
1713
+
1714
+ // Package doesn't exist locally, redirect to default upstream
1715
+ const { getDefaultUpstream } = await import('../utils/upstream.ts')
1716
+ const defaultUpstream = getDefaultUpstream()
1717
+ return c.redirect(`/${defaultUpstream}/${pkg}`, 302)
1718
+ }
1719
+
1720
+ /**
1721
+ * Handle package publishing - validates authentication and delegates to publishPackage
1722
+ * This is used for the `PUT /:pkg` route to handle package publishing
1723
+ */
1724
+ export async function handlePackagePublish(c: HonoContext) {
1725
+ const authHeader =
1726
+ c.req.header('authorization') || c.req.header('Authorization')
1727
+
1728
+ // Check for authentication
1729
+ if (!authHeader) {
1730
+ return c.json(
1731
+ {
1732
+ error: 'Authentication required',
1733
+ reason:
1734
+ 'You must be logged in to publish packages. Run "npm adduser" first.',
1735
+ },
1736
+ 401,
1737
+ )
1738
+ }
1739
+
1740
+ // Extract token and verify
1741
+ const token =
1742
+ authHeader.startsWith('Bearer ') ?
1743
+ authHeader.substring(7).trim()
1744
+ : null
1745
+ if (!token) {
1746
+ return c.json(
1747
+ {
1748
+ error: 'Invalid authentication format',
1749
+ reason:
1750
+ 'Authorization header must be in "Bearer <token>" format',
1751
+ },
1752
+ 401,
1753
+ )
1754
+ }
1755
+
1756
+ // Verify token has package publishing permissions
1757
+ const { verifyToken } = await import('../utils/auth.ts')
1758
+ const isValid = await verifyToken(token, c)
1759
+ if (!isValid) {
1760
+ return c.json(
1761
+ {
1762
+ error: 'Invalid or insufficient permissions',
1763
+ reason: 'Token does not have permission to publish packages',
1764
+ },
1765
+ 403,
1766
+ )
1767
+ }
1768
+
1769
+ // Delegate to publishPackage function
1770
+ return publishPackage(c)
1771
+ }
1772
+
1773
+ /**
1774
+ * Handle package version route - checks for local package existence and redirects to upstream if not found
1775
+ * This is used for the `/:pkg/:version` route to handle package version requests
1776
+ */
1777
+ export async function handlePackageVersion(c: HonoContext) {
1778
+ const pkg = decodeURIComponent(c.req.param('pkg'))
1779
+ const version = c.req.param('version')
1780
+
1781
+ // Skip if this looks like a static asset or internal route
1782
+ if (
1783
+ pkg.includes('.') ||
1784
+ pkg.startsWith('-') ||
1785
+ pkg.startsWith('_')
1786
+ ) {
1787
+ return new Response(null, { status: 404 })
1788
+ }
1789
+
1790
+ // Check if this package exists locally first
1791
+ try {
1792
+ const localPackage = await getDb(c).getPackage(pkg)
1793
+ if (localPackage) {
1794
+ // Package exists locally, handle it with the local package route handler
1795
+ return await handlePackageRoute(c)
1796
+ }
1797
+ } catch (error) {
1798
+ // eslint-disable-next-line no-console
1799
+ console.error('Error checking local package version:', error)
1800
+ // Continue to upstream redirect on database error
1801
+ }
1802
+
1803
+ // Package doesn't exist locally, redirect to default upstream
1804
+ const { getDefaultUpstream } = await import('../utils/upstream.ts')
1805
+ const defaultUpstream = getDefaultUpstream()
1806
+ return c.redirect(`/${defaultUpstream}/${pkg}/${version}`, 302)
1807
+ }
1808
+
1809
+ /**
1810
+ * Handle package tarball route - checks for local package existence and redirects to upstream if not found
1811
+ * This is used for the `/:pkg/-/:tarball` route to handle package tarball requests
1812
+ */
1813
+ export async function handlePackageTarball(c: HonoContext) {
1814
+ const pkg = decodeURIComponent(c.req.param('pkg'))
1815
+
1816
+ // Skip if this looks like a static asset or internal route
1817
+ if (
1818
+ pkg.includes('.') ||
1819
+ pkg.startsWith('-') ||
1820
+ pkg.startsWith('_')
1821
+ ) {
1822
+ return new Response(null, { status: 404 })
1823
+ }
1824
+
1825
+ // Check if this package exists locally first
1826
+ try {
1827
+ const localPackage = await getDb(c).getPackage(pkg)
1828
+ if (localPackage) {
1829
+ // Package exists locally, handle it with the local package route handler
1830
+ return await handlePackageRoute(c)
1831
+ }
1832
+ } catch (error) {
1833
+ // eslint-disable-next-line no-console
1834
+ console.error('Error checking local package tarball:', error)
1835
+ // Continue to upstream redirect on database error
1836
+ }
1837
+
1838
+ // Package doesn't exist locally, redirect to default upstream
1839
+ const { getDefaultUpstream } = await import('../utils/upstream.ts')
1840
+ const defaultUpstream = getDefaultUpstream()
1841
+ const tarball = c.req.param('tarball')
1842
+ return c.redirect(`/${defaultUpstream}/${pkg}/-/${tarball}`, 302)
1843
+ }
1844
+
1845
+ /**
1846
+ * Validates upstream and sets context, then delegates to handlePackageRoute
1847
+ * Common logic shared by all upstream routes
1848
+ */
1849
+ async function validateUpstreamAndDelegate(
1850
+ c: HonoContext,
1851
+ ): Promise<Response> {
1852
+ const upstream = c.req.param('upstream')
1853
+
1854
+ // Import validation functions dynamically to avoid circular dependencies
1855
+ const { isValidUpstreamName, getUpstreamConfig } = await import(
1856
+ '../utils/upstream.ts'
1857
+ )
1858
+
1859
+ // Validate upstream name
1860
+ if (!isValidUpstreamName(upstream)) {
1861
+ return c.json(
1862
+ { error: `Invalid or reserved upstream name: ${upstream}` },
1863
+ 400,
1864
+ )
1865
+ }
1866
+
1867
+ // Check if upstream is configured
1868
+ const upstreamConfig = getUpstreamConfig(upstream)
1869
+ if (!upstreamConfig) {
1870
+ return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
1871
+ }
1872
+
1873
+ // Set upstream context and delegate to handlePackageRoute
1874
+ c.set('upstream', upstream)
1875
+ return await handlePackageRoute(c)
1876
+ }
1877
+
1878
+ /**
1879
+ * Handle upstream package requests like /npm/lodash, /jsr/@std/fs
1880
+ * This is used for the `/:upstream/:pkg` route
1881
+ */
1882
+ export async function handleUpstreamPackage(c: HonoContext) {
1883
+ return validateUpstreamAndDelegate(c)
1884
+ }
1885
+
1886
+ /**
1887
+ * Handle unencoded scoped package tarball requests like /npm/@types/node/-/node-18.0.0.tgz
1888
+ * This is used for the `/:upstream/:scope/:pkg/-/:tarball` route (most specific - 5 segments)
1889
+ */
1890
+ export async function handleUpstreamScopedTarball(c: HonoContext) {
1891
+ return validateUpstreamAndDelegate(c)
1892
+ }
1893
+
1894
+ /**
1895
+ * Handle unencoded scoped package versions like /npm/@types/node/18.0.0
1896
+ * This is used for the `/:upstream/:scope/:pkg/:version` route
1897
+ */
1898
+ export async function handleUpstreamScopedVersion(c: HonoContext) {
1899
+ return validateUpstreamAndDelegate(c)
1900
+ }
1901
+
1902
+ /**
1903
+ * Handle URL-encoded scoped packages like /npm/@babel%2Fcore
1904
+ * This is used for the `/:upstream/:scope%2f:pkg` route
1905
+ */
1906
+ export async function handleUpstreamEncodedScoped(c: HonoContext) {
1907
+ return validateUpstreamAndDelegate(c)
1908
+ }
1909
+
1910
+ /**
1911
+ * Unified route handler for 3-segment paths: /npm/pkg/version OR /npm/@scope/package
1912
+ * This is used for the `/:upstream/:param2/:param3` route
1913
+ */
1914
+ export async function handleUpstreamUnified(c: HonoContext) {
1915
+ return validateUpstreamAndDelegate(c)
1916
+ }
1917
+
1918
+ /**
1919
+ * Handle upstream tarball requests like /npm/lodash/-/lodash-4.17.21.tgz
1920
+ * This is used for the `/:upstream/:pkg/-/:tarball` route
1921
+ */
1922
+ export async function handleUpstreamTarball(c: HonoContext) {
1923
+ return validateUpstreamAndDelegate(c)
1924
+ }
1925
+
1926
+ // Route definitions for OpenAPI documentation
1927
+
1928
+ // Package manifest routes
1929
+ export const getPackageRoute = createRoute({
1930
+ method: 'get',
1931
+ path: '/{pkg}',
1932
+ tags: ['Packages'],
1933
+ summary: 'Get Package Manifest',
1934
+ description: `Get the full package manifest (packument) for a package
1935
+ \`\`\`bash
1936
+ $ npm view lodash
1937
+ \`\`\``,
1938
+ request: {
1939
+ params: z.object({
1940
+ pkg: z.string(),
1941
+ }),
1942
+ },
1943
+ responses: {
1944
+ 200: {
1945
+ content: {
1946
+ 'application/json': {
1947
+ schema: z.object({
1948
+ name: z.string(),
1949
+ 'dist-tags': z.record(z.string()),
1950
+ versions: z.record(z.any()),
1951
+ }),
1952
+ },
1953
+ },
1954
+ description: 'Package manifest',
1955
+ },
1956
+ 404: {
1957
+ content: {
1958
+ 'application/json': {
1959
+ schema: z.object({
1960
+ error: z.string(),
1961
+ }),
1962
+ },
1963
+ },
1964
+ description: 'Package not found',
1965
+ },
1966
+ },
1967
+ })
1968
+
1969
+ export const getScopedPackageRoute = createRoute({
1970
+ method: 'get',
1971
+ path: '/{scope}/{pkg}',
1972
+ tags: ['Packages'],
1973
+ summary: 'Get Scoped Package Manifest',
1974
+ description: `Get the full package manifest for a scoped package
1975
+ \`\`\`bash
1976
+ $ npm view @types/node
1977
+ \`\`\``,
1978
+ request: {
1979
+ params: z.object({
1980
+ scope: z.string(),
1981
+ pkg: z.string(),
1982
+ }),
1983
+ },
1984
+ responses: {
1985
+ 200: {
1986
+ content: {
1987
+ 'application/json': {
1988
+ schema: z.object({
1989
+ name: z.string(),
1990
+ 'dist-tags': z.record(z.string()),
1991
+ versions: z.record(z.any()),
1992
+ }),
1993
+ },
1994
+ },
1995
+ description: 'Package manifest',
1996
+ },
1997
+ 404: {
1998
+ content: {
1999
+ 'application/json': {
2000
+ schema: z.object({
2001
+ error: z.string(),
2002
+ }),
2003
+ },
2004
+ },
2005
+ description: 'Package not found',
2006
+ },
2007
+ },
2008
+ })
2009
+
2010
+ // Package version routes
2011
+ export const getPackageVersionRoute = createRoute({
2012
+ method: 'get',
2013
+ path: '/{pkg}/{version}',
2014
+ tags: ['Packages'],
2015
+ summary: 'Get Package Version Manifest',
2016
+ description: `Get the manifest for a specific version of a package
2017
+ \`\`\`bash
2018
+ $ npm view lodash@4.17.21
2019
+ \`\`\``,
2020
+ request: {
2021
+ params: z.object({
2022
+ pkg: z.string(),
2023
+ version: z.string(),
2024
+ }),
2025
+ },
2026
+ responses: {
2027
+ 200: {
2028
+ content: {
2029
+ 'application/json': {
2030
+ schema: z.object({
2031
+ name: z.string(),
2032
+ version: z.string(),
2033
+ dist: z.object({
2034
+ tarball: z.string(),
2035
+ shasum: z.string(),
2036
+ }),
2037
+ }),
2038
+ },
2039
+ },
2040
+ description: 'Package version manifest',
2041
+ },
2042
+ 404: {
2043
+ content: {
2044
+ 'application/json': {
2045
+ schema: z.object({
2046
+ error: z.string(),
2047
+ }),
2048
+ },
2049
+ },
2050
+ description: 'Package version not found',
2051
+ },
2052
+ },
2053
+ })
2054
+
2055
+ export const getScopedPackageVersionRoute = createRoute({
2056
+ method: 'get',
2057
+ path: '/{scope}/{pkg}/{version}',
2058
+ tags: ['Packages'],
2059
+ summary: 'Get Scoped Package Version Manifest',
2060
+ description: `Get the manifest for a specific version of a scoped package
2061
+ \`\`\`bash
2062
+ $ npm view @types/node@18.0.0
2063
+ \`\`\``,
2064
+ request: {
2065
+ params: z.object({
2066
+ scope: z.string(),
2067
+ pkg: z.string(),
2068
+ version: z.string(),
2069
+ }),
2070
+ },
2071
+ responses: {
2072
+ 200: {
2073
+ content: {
2074
+ 'application/json': {
2075
+ schema: z.object({
2076
+ name: z.string(),
2077
+ version: z.string(),
2078
+ dist: z.object({
2079
+ tarball: z.string(),
2080
+ shasum: z.string(),
2081
+ }),
2082
+ }),
2083
+ },
2084
+ },
2085
+ description: 'Package version manifest',
2086
+ },
2087
+ 404: {
2088
+ content: {
2089
+ 'application/json': {
2090
+ schema: z.object({
2091
+ error: z.string(),
2092
+ }),
2093
+ },
2094
+ },
2095
+ description: 'Package version not found',
2096
+ },
2097
+ },
2098
+ })
2099
+
2100
+ // Package tarball routes
2101
+ export const getPackageTarballRoute = createRoute({
2102
+ method: 'get',
2103
+ path: '/{pkg}/-/{tarball}',
2104
+ tags: ['Packages'],
2105
+ summary: 'Download Package Tarball',
2106
+ description: `Download the tarball for a specific version of a package`,
2107
+ request: {
2108
+ params: z.object({
2109
+ pkg: z.string(),
2110
+ tarball: z.string(),
2111
+ }),
2112
+ },
2113
+ responses: {
2114
+ 200: {
2115
+ content: {
2116
+ 'application/octet-stream': {
2117
+ schema: z.string().openapi({ format: 'binary' }),
2118
+ },
2119
+ },
2120
+ description: 'Package tarball',
2121
+ },
2122
+ 404: {
2123
+ content: {
2124
+ 'application/json': {
2125
+ schema: z.object({
2126
+ error: z.string(),
2127
+ }),
2128
+ },
2129
+ },
2130
+ description: 'Tarball not found',
2131
+ },
2132
+ },
2133
+ })
2134
+
2135
+ export const getScopedPackageTarballRoute = createRoute({
2136
+ method: 'get',
2137
+ path: '/{scope}/{pkg}/-/{tarball}',
2138
+ tags: ['Packages'],
2139
+ summary: 'Download Scoped Package Tarball',
2140
+ description: `Download the tarball for a specific version of a scoped package`,
2141
+ request: {
2142
+ params: z.object({
2143
+ scope: z.string(),
2144
+ pkg: z.string(),
2145
+ tarball: z.string(),
2146
+ }),
2147
+ },
2148
+ responses: {
2149
+ 200: {
2150
+ content: {
2151
+ 'application/octet-stream': {
2152
+ schema: z.string().openapi({ format: 'binary' }),
2153
+ },
2154
+ },
2155
+ description: 'Package tarball',
2156
+ },
2157
+ 404: {
2158
+ content: {
2159
+ 'application/json': {
2160
+ schema: z.object({
2161
+ error: z.string(),
2162
+ }),
2163
+ },
2164
+ },
2165
+ description: 'Tarball not found',
2166
+ },
2167
+ },
2168
+ })
2169
+
2170
+ // Package publishing route
2171
+ export const publishPackageRoute = createRoute({
2172
+ method: 'put',
2173
+ path: '/{pkg}',
2174
+ tags: ['Packages'],
2175
+ summary: 'Publish Package',
2176
+ description: `Publish a new version of a package
2177
+ \`\`\`bash
2178
+ $ npm publish
2179
+ \`\`\``,
2180
+ request: {
2181
+ params: z.object({
2182
+ pkg: z.string(),
2183
+ }),
2184
+ body: {
2185
+ content: {
2186
+ 'application/json': {
2187
+ schema: z.object({
2188
+ name: z.string(),
2189
+ versions: z.record(z.any()),
2190
+ 'dist-tags': z.record(z.string()),
2191
+ _attachments: z.record(z.any()).optional(),
2192
+ }),
2193
+ },
2194
+ },
2195
+ },
2196
+ },
2197
+ responses: {
2198
+ 200: {
2199
+ content: {
2200
+ 'application/json': {
2201
+ schema: z.object({
2202
+ ok: z.boolean(),
2203
+ id: z.string(),
2204
+ rev: z.string(),
2205
+ }),
2206
+ },
2207
+ },
2208
+ description: 'Package published successfully',
2209
+ },
2210
+ 400: {
2211
+ content: {
2212
+ 'application/json': {
2213
+ schema: z.object({
2214
+ error: z.string(),
2215
+ }),
2216
+ },
2217
+ },
2218
+ description: 'Bad request',
2219
+ },
2220
+ 401: {
2221
+ content: {
2222
+ 'application/json': {
2223
+ schema: z.object({
2224
+ error: z.string(),
2225
+ }),
2226
+ },
2227
+ },
2228
+ description: 'Authentication required',
2229
+ },
2230
+ 403: {
2231
+ content: {
2232
+ 'application/json': {
2233
+ schema: z.object({
2234
+ error: z.string(),
2235
+ }),
2236
+ },
2237
+ },
2238
+ description: 'Insufficient permissions',
2239
+ },
2240
+ },
2241
+ })
2242
+
2243
+ // Dist-tag route definitions
2244
+ export const getPackageDistTagsRoute = createRoute({
2245
+ method: 'get',
2246
+ path: '/-/package/{pkg}/dist-tags',
2247
+ tags: ['Dist-Tags'],
2248
+ summary: 'Get Package Dist Tags',
2249
+ description: `Get all dist-tags for a package
2250
+ \`\`\`bash
2251
+ $ npm dist-tag ls mypackage
2252
+ \`\`\``,
2253
+ request: {
2254
+ params: z.object({
2255
+ pkg: z.string(),
2256
+ }),
2257
+ },
2258
+ responses: {
2259
+ 200: {
2260
+ content: {
2261
+ 'application/json': {
2262
+ schema: z.record(z.string()),
2263
+ },
2264
+ },
2265
+ description: 'Package dist-tags',
2266
+ },
2267
+ 404: {
2268
+ content: {
2269
+ 'application/json': {
2270
+ schema: z.object({
2271
+ error: z.string(),
2272
+ }),
2273
+ },
2274
+ },
2275
+ description: 'Package not found',
2276
+ },
2277
+ },
2278
+ })
2279
+
2280
+ export const putPackageDistTagRoute = createRoute({
2281
+ method: 'put',
2282
+ path: '/-/package/{pkg}/dist-tags/{tag}',
2283
+ tags: ['Dist-Tags'],
2284
+ summary: 'Set Package Dist Tag',
2285
+ description: `Set or update a dist-tag for a package version
2286
+ \`\`\`bash
2287
+ $ npm dist-tag add mypackage@1.0.0 beta
2288
+ \`\`\``,
2289
+ request: {
2290
+ params: z.object({
2291
+ pkg: z.string(),
2292
+ tag: z.string(),
2293
+ }),
2294
+ body: {
2295
+ content: {
2296
+ 'text/plain': {
2297
+ schema: z.string(),
2298
+ },
2299
+ },
2300
+ },
2301
+ },
2302
+ responses: {
2303
+ 201: {
2304
+ content: {
2305
+ 'application/json': {
2306
+ schema: z.object({
2307
+ ok: z.boolean(),
2308
+ id: z.string(),
2309
+ rev: z.string(),
2310
+ }),
2311
+ },
2312
+ },
2313
+ description: 'Dist-tag set successfully',
2314
+ },
2315
+ 400: {
2316
+ content: {
2317
+ 'application/json': {
2318
+ schema: z.object({
2319
+ error: z.string(),
2320
+ }),
2321
+ },
2322
+ },
2323
+ description: 'Invalid version or tag',
2324
+ },
2325
+ 403: {
2326
+ content: {
2327
+ 'application/json': {
2328
+ schema: z.object({
2329
+ error: z.string(),
2330
+ }),
2331
+ },
2332
+ },
2333
+ description: 'Cannot modify dist-tags on proxied packages',
2334
+ },
2335
+ 404: {
2336
+ content: {
2337
+ 'application/json': {
2338
+ schema: z.object({
2339
+ error: z.string(),
2340
+ }),
2341
+ },
2342
+ },
2343
+ description: 'Package not found',
2344
+ },
2345
+ },
2346
+ })
2347
+
2348
+ export const deletePackageDistTagRoute = createRoute({
2349
+ method: 'delete',
2350
+ path: '/-/package/{pkg}/dist-tags/{tag}',
2351
+ tags: ['Dist-Tags'],
2352
+ summary: 'Delete Package Dist Tag',
2353
+ description: `Delete a dist-tag from a package
2354
+ \`\`\`bash
2355
+ $ npm dist-tag rm mypackage beta
2356
+ \`\`\``,
2357
+ request: {
2358
+ params: z.object({
2359
+ pkg: z.string(),
2360
+ tag: z.string(),
2361
+ }),
2362
+ },
2363
+ responses: {
2364
+ 200: {
2365
+ content: {
2366
+ 'application/json': {
2367
+ schema: z.object({
2368
+ ok: z.boolean(),
2369
+ id: z.string(),
2370
+ rev: z.string(),
2371
+ }),
2372
+ },
2373
+ },
2374
+ description: 'Dist-tag deleted successfully',
2375
+ },
2376
+ 400: {
2377
+ content: {
2378
+ 'application/json': {
2379
+ schema: z.object({
2380
+ error: z.string(),
2381
+ }),
2382
+ },
2383
+ },
2384
+ description: 'Cannot delete latest tag or invalid request',
2385
+ },
2386
+ 403: {
2387
+ content: {
2388
+ 'application/json': {
2389
+ schema: z.object({
2390
+ error: z.string(),
2391
+ }),
2392
+ },
2393
+ },
2394
+ description: 'Cannot modify dist-tags on proxied packages',
2395
+ },
2396
+ 404: {
2397
+ content: {
2398
+ 'application/json': {
2399
+ schema: z.object({
2400
+ error: z.string(),
2401
+ }),
2402
+ },
2403
+ },
2404
+ description: 'Package or tag not found',
2405
+ },
2406
+ },
2407
+ })
2408
+
2409
+ // =============================================================================
2410
+ // Upstream Package Routes
2411
+ // =============================================================================
2412
+
2413
+ // Upstream package manifest routes
2414
+ export const getUpstreamPackageRoute = createRoute({
2415
+ method: 'get',
2416
+ path: '/{upstream}/{pkg}',
2417
+ tags: ['Packages'],
2418
+ summary: 'Get upstream package manifest',
2419
+ description:
2420
+ 'Retrieve package manifest from upstream registry (e.g., npm, jsr)',
2421
+ request: {
2422
+ params: z.object({
2423
+ upstream: z.string().min(1).openapi({
2424
+ description: 'Upstream registry name (e.g., npm, jsr)',
2425
+ }),
2426
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2427
+ }),
2428
+ },
2429
+ responses: {
2430
+ 200: {
2431
+ content: {
2432
+ 'application/json': {
2433
+ schema: z
2434
+ .object({
2435
+ name: z.string(),
2436
+ 'dist-tags': z.record(z.string()),
2437
+ versions: z.record(z.unknown()),
2438
+ time: z.record(z.string()),
2439
+ })
2440
+ .openapi({ description: 'Package manifest data' }),
2441
+ },
2442
+ },
2443
+ description: 'Package manifest retrieved successfully',
2444
+ },
2445
+ 404: {
2446
+ content: {
2447
+ 'application/json': {
2448
+ schema: z.object({
2449
+ error: z.string(),
2450
+ }),
2451
+ },
2452
+ },
2453
+ description: 'Package not found in upstream registry',
2454
+ },
2455
+ },
2456
+ })
2457
+
2458
+ export const getUpstreamScopedPackageRoute = createRoute({
2459
+ method: 'get',
2460
+ path: '/{upstream}/{scope}/{pkg}',
2461
+ tags: ['Packages'],
2462
+ summary: 'Get upstream scoped package manifest',
2463
+ description:
2464
+ 'Retrieve scoped package manifest from upstream registry',
2465
+ request: {
2466
+ params: z.object({
2467
+ upstream: z
2468
+ .string()
2469
+ .min(1)
2470
+ .openapi({ description: 'Upstream registry name' }),
2471
+ scope: z
2472
+ .string()
2473
+ .min(1)
2474
+ .openapi({ description: 'Package scope (e.g., @types)' }),
2475
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2476
+ }),
2477
+ },
2478
+ responses: {
2479
+ 200: {
2480
+ content: {
2481
+ 'application/json': {
2482
+ schema: z.object({
2483
+ name: z.string(),
2484
+ 'dist-tags': z.record(z.string()),
2485
+ versions: z.record(z.unknown()),
2486
+ time: z.record(z.string()),
2487
+ }),
2488
+ },
2489
+ },
2490
+ description: 'Scoped package manifest retrieved successfully',
2491
+ },
2492
+ 404: {
2493
+ content: {
2494
+ 'application/json': {
2495
+ schema: z.object({
2496
+ error: z.string(),
2497
+ }),
2498
+ },
2499
+ },
2500
+ description: 'Scoped package not found in upstream registry',
2501
+ },
2502
+ },
2503
+ })
2504
+
2505
+ // Upstream package version routes
2506
+ export const getUpstreamPackageVersionRoute = createRoute({
2507
+ method: 'get',
2508
+ path: '/{upstream}/{pkg}/{version}',
2509
+ tags: ['Packages'],
2510
+ summary: 'Get upstream package version manifest',
2511
+ description:
2512
+ 'Retrieve specific version manifest from upstream registry',
2513
+ request: {
2514
+ params: z.object({
2515
+ upstream: z
2516
+ .string()
2517
+ .min(1)
2518
+ .openapi({ description: 'Upstream registry name' }),
2519
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2520
+ version: z
2521
+ .string()
2522
+ .min(1)
2523
+ .openapi({ description: 'Package version' }),
2524
+ }),
2525
+ },
2526
+ responses: {
2527
+ 200: {
2528
+ content: {
2529
+ 'application/json': {
2530
+ schema: z.object({
2531
+ name: z.string(),
2532
+ version: z.string(),
2533
+ dependencies: z.record(z.string()).optional(),
2534
+ peerDependencies: z.record(z.string()).optional(),
2535
+ optionalDependencies: z.record(z.string()).optional(),
2536
+ peerDependenciesMeta: z.record(z.string()).optional(),
2537
+ bin: z.record(z.string()).optional(),
2538
+ engines: z.record(z.string()).optional(),
2539
+ dist: z.object({
2540
+ tarball: z.string(),
2541
+ }),
2542
+ }),
2543
+ },
2544
+ },
2545
+ description: 'Package version manifest retrieved successfully',
2546
+ },
2547
+ 404: {
2548
+ content: {
2549
+ 'application/json': {
2550
+ schema: z.object({
2551
+ error: z.string(),
2552
+ }),
2553
+ },
2554
+ },
2555
+ description: 'Package version not found in upstream registry',
2556
+ },
2557
+ },
2558
+ })
2559
+
2560
+ export const getUpstreamScopedPackageVersionRoute = createRoute({
2561
+ method: 'get',
2562
+ path: '/{upstream}/{scope}/{pkg}/{version}',
2563
+ tags: ['Packages'],
2564
+ summary: 'Get upstream scoped package version manifest',
2565
+ description:
2566
+ 'Retrieve specific version manifest for scoped package from upstream registry',
2567
+ request: {
2568
+ params: z.object({
2569
+ upstream: z
2570
+ .string()
2571
+ .min(1)
2572
+ .openapi({ description: 'Upstream registry name' }),
2573
+ scope: z
2574
+ .string()
2575
+ .min(1)
2576
+ .openapi({ description: 'Package scope' }),
2577
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2578
+ version: z
2579
+ .string()
2580
+ .min(1)
2581
+ .openapi({ description: 'Package version' }),
2582
+ }),
2583
+ },
2584
+ responses: {
2585
+ 200: {
2586
+ content: {
2587
+ 'application/json': {
2588
+ schema: z.object({
2589
+ name: z.string(),
2590
+ version: z.string(),
2591
+ dependencies: z.record(z.string()).optional(),
2592
+ peerDependencies: z.record(z.string()).optional(),
2593
+ optionalDependencies: z.record(z.string()).optional(),
2594
+ peerDependenciesMeta: z.record(z.string()).optional(),
2595
+ bin: z.record(z.string()).optional(),
2596
+ engines: z.record(z.string()).optional(),
2597
+ dist: z.object({
2598
+ tarball: z.string(),
2599
+ }),
2600
+ }),
2601
+ },
2602
+ },
2603
+ description:
2604
+ 'Scoped package version manifest retrieved successfully',
2605
+ },
2606
+ 404: {
2607
+ content: {
2608
+ 'application/json': {
2609
+ schema: z.object({
2610
+ error: z.string(),
2611
+ }),
2612
+ },
2613
+ },
2614
+ description:
2615
+ 'Scoped package version not found in upstream registry',
2616
+ },
2617
+ },
2618
+ })
2619
+
2620
+ // Upstream package tarball routes
2621
+ export const getUpstreamPackageTarballRoute = createRoute({
2622
+ method: 'get',
2623
+ path: '/{upstream}/{pkg}/-/{tarball}',
2624
+ tags: ['Packages'],
2625
+ summary: 'Download upstream package tarball',
2626
+ description: 'Download package tarball from upstream registry',
2627
+ request: {
2628
+ params: z.object({
2629
+ upstream: z
2630
+ .string()
2631
+ .min(1)
2632
+ .openapi({ description: 'Upstream registry name' }),
2633
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2634
+ tarball: z
2635
+ .string()
2636
+ .min(1)
2637
+ .openapi({ description: 'Tarball filename' }),
2638
+ }),
2639
+ },
2640
+ responses: {
2641
+ 200: {
2642
+ content: {
2643
+ 'application/octet-stream': {
2644
+ schema: z.string().openapi({ format: 'binary' }),
2645
+ },
2646
+ },
2647
+ description: 'Package tarball downloaded successfully',
2648
+ },
2649
+ 404: {
2650
+ content: {
2651
+ 'application/json': {
2652
+ schema: z.object({
2653
+ error: z.string(),
2654
+ }),
2655
+ },
2656
+ },
2657
+ description: 'Package tarball not found in upstream registry',
2658
+ },
2659
+ },
2660
+ })
2661
+
2662
+ export const getUpstreamScopedPackageTarballRoute = createRoute({
2663
+ method: 'get',
2664
+ path: '/{upstream}/{scope}/{pkg}/-/{tarball}',
2665
+ tags: ['Packages'],
2666
+ summary: 'Download upstream scoped package tarball',
2667
+ description:
2668
+ 'Download scoped package tarball from upstream registry',
2669
+ request: {
2670
+ params: z.object({
2671
+ upstream: z
2672
+ .string()
2673
+ .min(1)
2674
+ .openapi({ description: 'Upstream registry name' }),
2675
+ scope: z
2676
+ .string()
2677
+ .min(1)
2678
+ .openapi({ description: 'Package scope' }),
2679
+ pkg: z.string().min(1).openapi({ description: 'Package name' }),
2680
+ tarball: z
2681
+ .string()
2682
+ .min(1)
2683
+ .openapi({ description: 'Tarball filename' }),
2684
+ }),
2685
+ },
2686
+ responses: {
2687
+ 200: {
2688
+ content: {
2689
+ 'application/octet-stream': {
2690
+ schema: z.string().openapi({ format: 'binary' }),
2691
+ },
2692
+ },
2693
+ description: 'Scoped package tarball downloaded successfully',
2694
+ },
2695
+ 404: {
2696
+ content: {
2697
+ 'application/json': {
2698
+ schema: z.object({
2699
+ error: z.string(),
2700
+ }),
2701
+ },
2702
+ },
2703
+ description:
2704
+ 'Scoped package tarball not found in upstream registry',
2705
+ },
2706
+ },
2707
+ })
2708
+
2709
+ // Special upstream routes for URL-encoded scoped packages
2710
+ export const getUpstreamEncodedScopedPackageRoute = createRoute({
2711
+ method: 'get',
2712
+ path: '/{upstream}/{scope%2f:pkg}',
2713
+ tags: ['Packages'],
2714
+ summary: 'Get upstream URL-encoded scoped package',
2715
+ description:
2716
+ 'Retrieve scoped package manifest using URL-encoded scope format',
2717
+ request: {
2718
+ params: z.object({
2719
+ upstream: z
2720
+ .string()
2721
+ .min(1)
2722
+ .openapi({ description: 'Upstream registry name' }),
2723
+ 'scope%2f:pkg': z.string().min(1).openapi({
2724
+ description:
2725
+ 'URL-encoded scoped package name (e.g., @babel%2Fcore)',
2726
+ }),
2727
+ }),
2728
+ },
2729
+ responses: {
2730
+ 200: {
2731
+ content: {
2732
+ 'application/json': {
2733
+ schema: z.object({
2734
+ name: z.string(),
2735
+ 'dist-tags': z.record(z.string()),
2736
+ versions: z.record(z.unknown()),
2737
+ time: z.record(z.string()),
2738
+ }),
2739
+ },
2740
+ },
2741
+ description:
2742
+ 'URL-encoded scoped package manifest retrieved successfully',
2743
+ },
2744
+ 404: {
2745
+ content: {
2746
+ 'application/json': {
2747
+ schema: z.object({
2748
+ error: z.string(),
2749
+ }),
2750
+ },
2751
+ },
2752
+ description:
2753
+ 'URL-encoded scoped package not found in upstream registry',
2754
+ },
2755
+ },
2756
+ })
2757
+
2758
+ // Unified upstream route (handles both scoped packages and versions)
2759
+ export const getUpstreamUnifiedRoute = createRoute({
2760
+ method: 'get',
2761
+ path: '/{upstream}/{param2}/{param3}',
2762
+ tags: ['Packages'],
2763
+ summary: 'Unified upstream route handler',
2764
+ description:
2765
+ 'Handles both /upstream/@scope/package and /upstream/package/version patterns',
2766
+ request: {
2767
+ params: z.object({
2768
+ upstream: z
2769
+ .string()
2770
+ .min(1)
2771
+ .openapi({ description: 'Upstream registry name' }),
2772
+ param2: z
2773
+ .string()
2774
+ .min(1)
2775
+ .openapi({ description: 'Either package name or scope' }),
2776
+ param3: z
2777
+ .string()
2778
+ .min(1)
2779
+ .openapi({ description: 'Either version or package name' }),
2780
+ }),
2781
+ },
2782
+ responses: {
2783
+ 200: {
2784
+ content: {
2785
+ 'application/json': {
2786
+ schema: z.union([
2787
+ z.object({
2788
+ name: z.string(),
2789
+ 'dist-tags': z.record(z.string()),
2790
+ versions: z.record(z.unknown()),
2791
+ time: z.record(z.string()),
2792
+ }),
2793
+ z.object({
2794
+ name: z.string(),
2795
+ version: z.string(),
2796
+ dependencies: z.record(z.string()).optional(),
2797
+ peerDependencies: z.record(z.string()).optional(),
2798
+ optionalDependencies: z.record(z.string()).optional(),
2799
+ peerDependenciesMeta: z.record(z.string()).optional(),
2800
+ bin: z.record(z.string()).optional(),
2801
+ engines: z.record(z.string()).optional(),
2802
+ dist: z.object({
2803
+ tarball: z.string(),
2804
+ }),
2805
+ }),
2806
+ ]),
2807
+ },
2808
+ },
2809
+ description:
2810
+ 'Package manifest or version data retrieved successfully',
2811
+ },
2812
+ 404: {
2813
+ content: {
2814
+ 'application/json': {
2815
+ schema: z.object({
2816
+ error: z.string(),
2817
+ }),
2818
+ },
2819
+ },
2820
+ description: 'Package not found in upstream registry',
2821
+ },
2822
+ },
2823
+ })