@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.
- package/DEPLOY.md +163 -0
- package/LICENSE +114 -10
- package/config.ts +221 -0
- package/dist/README.md +1 -1
- package/dist/bin/vsr.js +8 -6
- package/dist/index.js +3 -6
- package/dist/index.js.map +2 -2
- package/drizzle.config.js +40 -0
- package/info/COMPARISONS.md +37 -0
- package/info/CONFIGURATION.md +143 -0
- package/info/CONTRIBUTING.md +32 -0
- package/info/DATABASE_SETUP.md +108 -0
- package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
- package/info/PROJECT_STRUCTURE.md +291 -0
- package/info/ROADMAP.md +27 -0
- package/info/SUPPORT.md +39 -0
- package/info/TESTING.md +301 -0
- package/info/USER_SUPPORT.md +31 -0
- package/package.json +49 -6
- package/scripts/build-assets.js +31 -0
- package/scripts/build-bin.js +63 -0
- package/src/assets/public/images/bg.png +0 -0
- package/src/assets/public/images/clients/logo-bun.png +0 -0
- package/src/assets/public/images/clients/logo-deno.png +0 -0
- package/src/assets/public/images/clients/logo-npm.png +0 -0
- package/src/assets/public/images/clients/logo-pnpm.png +0 -0
- package/src/assets/public/images/clients/logo-vlt.png +0 -0
- package/src/assets/public/images/clients/logo-yarn.png +0 -0
- package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
- package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
- package/src/assets/public/images/favicon/favicon.ico +0 -0
- package/src/assets/public/images/favicon/favicon.svg +3 -0
- package/src/assets/public/images/favicon/site.webmanifest +21 -0
- package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
- package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
- package/src/assets/public/styles/styles.css +231 -0
- package/src/bin/demo/package.json +6 -0
- package/src/bin/demo/vlt.json +1 -0
- package/src/bin/vsr.ts +496 -0
- package/src/db/client.ts +590 -0
- package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
- package/src/db/migrations/0000_initial.sql +29 -0
- package/src/db/migrations/0001_uuid_validation.sql +35 -0
- package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
- package/src/db/migrations/drop.sql +3 -0
- package/src/db/migrations/meta/0000_snapshot.json +104 -0
- package/src/db/migrations/meta/0001_snapshot.json +155 -0
- package/src/db/migrations/meta/_journal.json +20 -0
- package/src/db/schema.ts +43 -0
- package/src/index.ts +434 -0
- package/src/middleware/config.ts +79 -0
- package/src/middleware/telemetry.ts +43 -0
- package/src/queue/index.ts +97 -0
- package/src/routes/access.ts +852 -0
- package/src/routes/docs.ts +63 -0
- package/src/routes/misc.ts +469 -0
- package/src/routes/packages.ts +2823 -0
- package/src/routes/ping.ts +39 -0
- package/src/routes/search.ts +131 -0
- package/src/routes/static.ts +74 -0
- package/src/routes/tokens.ts +259 -0
- package/src/routes/users.ts +68 -0
- package/src/utils/auth.ts +202 -0
- package/src/utils/cache.ts +587 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/database.ts +69 -0
- package/src/utils/docs.ts +146 -0
- package/src/utils/packages.ts +453 -0
- package/src/utils/response.ts +125 -0
- package/src/utils/routes.ts +64 -0
- package/src/utils/spa.ts +52 -0
- package/src/utils/tracing.ts +52 -0
- package/src/utils/upstream.ts +172 -0
- package/tsconfig.json +16 -0
- package/tsconfig.worker.json +3 -0
- package/typedoc.mjs +2 -0
- package/types.ts +598 -0
- package/vitest.config.ts +25 -0
- package/vlt.json.example +56 -0
- 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
|
+
})
|