@vltpkg/vsr 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -0
- package/.prettierrc +7 -0
- package/CONTRIBUTING.md +228 -0
- package/LICENSE.md +110 -0
- package/README.md +373 -0
- package/bin/vsr.ts +29 -0
- package/config.ts +124 -0
- package/debug-npm.js +19 -0
- package/drizzle.config.js +33 -0
- package/package.json +80 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/api.ts +2246 -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 +219 -0
- package/src/db/client.ts +544 -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 +41 -0
- package/src/index.ts +709 -0
- package/src/routes/access.ts +263 -0
- package/src/routes/auth.ts +93 -0
- package/src/routes/index.ts +135 -0
- package/src/routes/packages.ts +924 -0
- package/src/routes/search.ts +50 -0
- package/src/routes/static.ts +53 -0
- package/src/routes/tokens.ts +102 -0
- package/src/routes/users.ts +14 -0
- package/src/utils/auth.ts +145 -0
- package/src/utils/cache.ts +466 -0
- package/src/utils/database.ts +44 -0
- package/src/utils/packages.ts +337 -0
- package/src/utils/response.ts +100 -0
- package/src/utils/routes.ts +47 -0
- package/src/utils/spa.ts +14 -0
- package/src/utils/tracing.ts +63 -0
- package/src/utils/upstream.ts +131 -0
- package/test/README.md +91 -0
- package/test/access.test.js +760 -0
- package/test/cloudflare-waituntil.test.js +141 -0
- package/test/db.test.js +447 -0
- package/test/dist-tag.test.js +415 -0
- package/test/e2e.test.js +904 -0
- package/test/hono-context.test.js +250 -0
- package/test/integrity-validation.test.js +183 -0
- package/test/json-response.test.js +76 -0
- package/test/manifest-slimming.test.js +449 -0
- package/test/packument-consistency.test.js +351 -0
- package/test/packument-version-range.test.js +144 -0
- package/test/performance.test.js +162 -0
- package/test/route-with-waituntil.test.js +298 -0
- package/test/run-tests.js +151 -0
- package/test/setup-cache-tests.js +190 -0
- package/test/setup.js +64 -0
- package/test/stale-while-revalidate.test.js +273 -0
- package/test/static-assets.test.js +85 -0
- package/test/upstream-routing.test.js +86 -0
- package/test/utils/test-helpers.js +84 -0
- package/test/waituntil-correct.test.js +208 -0
- package/test/waituntil-demo.test.js +138 -0
- package/test/waituntil-readme.md +113 -0
- package/tsconfig.json +37 -0
- package/types.ts +446 -0
- package/vitest.config.js +95 -0
- package/wrangler.json +58 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import validate from 'validate-npm-package-name'
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import * as semver from 'semver'
|
|
5
|
+
import { accepts } from 'hono/accepts'
|
|
6
|
+
import { DOMAIN, PROXY, PROXY_URL } from '../../config.ts'
|
|
7
|
+
import {
|
|
8
|
+
parsePackageSpec,
|
|
9
|
+
getUpstreamConfig,
|
|
10
|
+
buildUpstreamUrl,
|
|
11
|
+
isProxyEnabled,
|
|
12
|
+
isValidUpstreamName,
|
|
13
|
+
getDefaultUpstream
|
|
14
|
+
} from '../utils/upstream.ts'
|
|
15
|
+
import {
|
|
16
|
+
extractPackageJSON,
|
|
17
|
+
packageSpec,
|
|
18
|
+
createFile,
|
|
19
|
+
createVersion,
|
|
20
|
+
slimManifest
|
|
21
|
+
} from '../utils/packages.ts'
|
|
22
|
+
import { getCachedPackageWithRefresh, getCachedVersionWithRefresh, isTarballCached, getTarballStoragePath, cacheTarball } from '../utils/cache.ts'
|
|
23
|
+
import type {
|
|
24
|
+
HonoContext,
|
|
25
|
+
PackageManifest,
|
|
26
|
+
SlimmedManifest,
|
|
27
|
+
ParsedPackage,
|
|
28
|
+
ParsedVersion,
|
|
29
|
+
UpstreamConfig,
|
|
30
|
+
PackageSpec
|
|
31
|
+
} from '../../types.ts'
|
|
32
|
+
|
|
33
|
+
interface SlimPackumentContext {
|
|
34
|
+
protocol?: string
|
|
35
|
+
host?: string
|
|
36
|
+
upstream?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TarballRequestParams {
|
|
40
|
+
scope: string
|
|
41
|
+
pkg: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PackageRouteSegments {
|
|
45
|
+
upstream?: string
|
|
46
|
+
packageName: string
|
|
47
|
+
segments: string[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ultra-aggressive slimming for packument versions (used in /:upstream/:pkg responses)
|
|
52
|
+
* Only includes the absolute minimum fields needed for dependency resolution and installation
|
|
53
|
+
* Fields included: name, version, dependencies, peerDependencies, optionalDependencies, peerDependenciesMeta, bin, engines, dist.tarball
|
|
54
|
+
*/
|
|
55
|
+
function slimPackumentVersion(manifest: any, context: SlimPackumentContext = {}): SlimmedManifest {
|
|
56
|
+
if (!manifest) return {} as SlimmedManifest
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Parse manifest if it's a string
|
|
60
|
+
let parsed: any
|
|
61
|
+
if (typeof manifest === 'string') {
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(manifest)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
parsed = manifest
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
parsed = manifest
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For packuments, only include the most essential fields
|
|
72
|
+
const slimmed: any = {
|
|
73
|
+
name: parsed.name,
|
|
74
|
+
version: parsed.version,
|
|
75
|
+
dependencies: parsed.dependencies || {},
|
|
76
|
+
peerDependencies: parsed.peerDependencies || {},
|
|
77
|
+
optionalDependencies: parsed.optionalDependencies || {},
|
|
78
|
+
peerDependenciesMeta: parsed.peerDependenciesMeta || {},
|
|
79
|
+
bin: parsed.bin,
|
|
80
|
+
engines: parsed.engines,
|
|
81
|
+
dist: {
|
|
82
|
+
tarball: rewriteTarballUrlIfNeeded(parsed.dist?.tarball || '', parsed.name, parsed.version, context)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Remove undefined fields to keep response clean
|
|
87
|
+
Object.keys(slimmed).forEach(key => {
|
|
88
|
+
if (key !== 'dist' && slimmed[key] === undefined) {
|
|
89
|
+
delete slimmed[key]
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Remove empty objects
|
|
94
|
+
if (Object.keys(slimmed.dependencies || {}).length === 0) {
|
|
95
|
+
delete slimmed.dependencies
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(slimmed.peerDependencies || {}).length === 0) {
|
|
98
|
+
delete slimmed.peerDependencies
|
|
99
|
+
}
|
|
100
|
+
if (Object.keys(slimmed.peerDependenciesMeta || {}).length === 0) {
|
|
101
|
+
delete slimmed.peerDependenciesMeta
|
|
102
|
+
}
|
|
103
|
+
if (Object.keys(slimmed.optionalDependencies || {}).length === 0) {
|
|
104
|
+
delete slimmed.optionalDependencies
|
|
105
|
+
}
|
|
106
|
+
if (Object.keys(slimmed.engines || {}).length === 0) {
|
|
107
|
+
delete slimmed.engines
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return slimmed as SlimmedManifest
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`[ERROR] Failed to slim packument version: ${(err as Error).message}`)
|
|
113
|
+
return (manifest || {}) as SlimmedManifest
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rewrite tarball URLs to point to our registry instead of the original registry
|
|
119
|
+
* Only rewrite if context is provided, otherwise return original URL
|
|
120
|
+
*/
|
|
121
|
+
function rewriteTarballUrlIfNeeded(
|
|
122
|
+
originalUrl: string,
|
|
123
|
+
packageName: string,
|
|
124
|
+
version: string,
|
|
125
|
+
context: SlimPackumentContext = {}
|
|
126
|
+
): string {
|
|
127
|
+
// Only rewrite if we have context indicating this is a proxied request
|
|
128
|
+
if (!context.upstream || !originalUrl || !packageName || !version) {
|
|
129
|
+
return originalUrl
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Extract the protocol and host from the context or use defaults
|
|
134
|
+
const protocol = context.protocol || 'http'
|
|
135
|
+
const host = context.host || 'localhost:1337'
|
|
136
|
+
const upstream = context.upstream
|
|
137
|
+
|
|
138
|
+
// Create the new tarball URL pointing to our registry
|
|
139
|
+
// For scoped packages like @scope/package, the filename should be package-version.tgz
|
|
140
|
+
// For unscoped packages like package, the filename should be package-version.tgz
|
|
141
|
+
const packageBaseName = packageName.includes('/') ? packageName.split('/')[1] : packageName
|
|
142
|
+
const filename = `${packageBaseName}-${version}.tgz`
|
|
143
|
+
const newUrl = `${protocol}://${host}/${upstream}/${packageName}/-/${filename}`
|
|
144
|
+
|
|
145
|
+
console.log(`[TARBALL_REWRITE] ${originalUrl} -> ${newUrl}`)
|
|
146
|
+
return newUrl
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(`[ERROR] Failed to rewrite tarball URL: ${(err as Error).message}`)
|
|
149
|
+
return originalUrl
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Helper function to properly decode scoped package names from URL parameters
|
|
155
|
+
* Handles cases where special characters in package names are URL-encoded
|
|
156
|
+
*/
|
|
157
|
+
function decodePackageName(scope: string, pkg?: string): string | null {
|
|
158
|
+
if (!scope) return null
|
|
159
|
+
|
|
160
|
+
// Decode URL-encoded characters in both scope and pkg
|
|
161
|
+
const decodedScope = decodeURIComponent(scope)
|
|
162
|
+
const decodedPkg = pkg ? decodeURIComponent(pkg) : null
|
|
163
|
+
|
|
164
|
+
// Handle scoped packages correctly
|
|
165
|
+
if (decodedScope.startsWith('@')) {
|
|
166
|
+
// If we have both scope and pkg, combine them
|
|
167
|
+
if (decodedPkg && decodedPkg !== '-') {
|
|
168
|
+
return `${decodedScope}/${decodedPkg}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If scope contains an encoded slash, it might be the full package name
|
|
172
|
+
if (decodedScope.includes('/')) {
|
|
173
|
+
return decodedScope
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Just the scope
|
|
177
|
+
return decodedScope
|
|
178
|
+
} else {
|
|
179
|
+
// Unscoped package - scope is actually the package name
|
|
180
|
+
return decodedScope
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Determines if a package is available only through proxy or is locally published
|
|
186
|
+
* A package is considered proxied if it doesn't exist locally but PROXY is enabled
|
|
187
|
+
*/
|
|
188
|
+
function isProxiedPackage(packageData: ParsedPackage | null): boolean {
|
|
189
|
+
// If the package doesn't exist locally but PROXY is enabled
|
|
190
|
+
if (!packageData && PROXY) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If the package is marked as proxied (has a source field indicating where it came from)
|
|
195
|
+
if (packageData && (packageData as any).source === 'proxy') {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function getPackageTarball(c: HonoContext) {
|
|
203
|
+
try {
|
|
204
|
+
let { scope, pkg } = c.req.param() as { scope: string; pkg: string }
|
|
205
|
+
const acceptsIntegrity = c.req.header('accepts-integrity')
|
|
206
|
+
|
|
207
|
+
console.log(`[DEBUG] getPackageTarball called with pkg="${pkg}", path="${c.req.path}"`)
|
|
208
|
+
|
|
209
|
+
// Handle scoped and unscoped packages correctly with URL decoding
|
|
210
|
+
try {
|
|
211
|
+
// For tarball requests, if scope is undefined/null, pkg should contain the package name
|
|
212
|
+
if (!scope || scope === 'undefined') {
|
|
213
|
+
if (!pkg) {
|
|
214
|
+
throw new Error('Missing package name')
|
|
215
|
+
}
|
|
216
|
+
pkg = decodeURIComponent(pkg)
|
|
217
|
+
console.log(`[DEBUG] Unscoped package: "${pkg}"`)
|
|
218
|
+
} else {
|
|
219
|
+
const packageName = decodePackageName(scope, pkg)
|
|
220
|
+
if (!packageName) {
|
|
221
|
+
throw new Error('Invalid scoped package name')
|
|
222
|
+
}
|
|
223
|
+
pkg = packageName
|
|
224
|
+
console.log(`[DEBUG] Scoped package: "${pkg}"`)
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error(`[ERROR] Failed to parse package name: ${(err as Error).message}`)
|
|
228
|
+
console.error(`[ERROR] Input parameters: scope="${scope}", pkg="${pkg}"`)
|
|
229
|
+
return c.json({ error: 'Invalid package name' }, 400)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tarball = c.req.path.split('/').pop()
|
|
233
|
+
if (!tarball || !tarball.endsWith('.tgz')) {
|
|
234
|
+
console.error(`[ERROR] Invalid tarball name: ${tarball}`)
|
|
235
|
+
return c.json({ error: 'Invalid tarball name' }, 400)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const filename = `${pkg}/${tarball}`
|
|
239
|
+
|
|
240
|
+
// If integrity checking is requested, get the expected integrity from manifest
|
|
241
|
+
let expectedIntegrity: string | null = null
|
|
242
|
+
if (acceptsIntegrity) {
|
|
243
|
+
try {
|
|
244
|
+
// Extract version from tarball name
|
|
245
|
+
const versionMatch = tarball.match(new RegExp(`${pkg.split('/').pop()}-(.*)\\.tgz`))
|
|
246
|
+
if (versionMatch) {
|
|
247
|
+
const version = versionMatch[1]
|
|
248
|
+
const spec = `${pkg}@${version}`
|
|
249
|
+
|
|
250
|
+
// Get the version from DB
|
|
251
|
+
const versionData = await c.db.getVersion(spec)
|
|
252
|
+
|
|
253
|
+
if (versionData && versionData.manifest) {
|
|
254
|
+
let manifest: any
|
|
255
|
+
try {
|
|
256
|
+
manifest = typeof versionData.manifest === 'string' ?
|
|
257
|
+
JSON.parse(versionData.manifest) : versionData.manifest
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.error(`[ERROR] Failed to parse manifest for ${spec}: ${(e as Error).message}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (manifest && manifest.dist && manifest.dist.integrity) {
|
|
263
|
+
expectedIntegrity = manifest.dist.integrity
|
|
264
|
+
console.log(`[INTEGRITY] Found expected integrity for ${filename}: ${expectedIntegrity}`)
|
|
265
|
+
|
|
266
|
+
// Simple string comparison with the provided integrity
|
|
267
|
+
if (acceptsIntegrity !== expectedIntegrity) {
|
|
268
|
+
console.error(`[INTEGRITY ERROR] Provided integrity (${acceptsIntegrity}) does not match expected integrity (${expectedIntegrity}) for ${filename}`)
|
|
269
|
+
return c.json({
|
|
270
|
+
error: 'Integrity check failed',
|
|
271
|
+
code: 'EINTEGRITY',
|
|
272
|
+
expected: expectedIntegrity,
|
|
273
|
+
actual: acceptsIntegrity
|
|
274
|
+
}, 400)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(`[INTEGRITY] Verified integrity for ${filename}`)
|
|
278
|
+
} else {
|
|
279
|
+
console.log(`[INTEGRITY] No integrity information found in manifest for ${spec}`)
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`[INTEGRITY] No version data found for ${spec}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error(`[INTEGRITY ERROR] Error checking integrity for ${filename}: ${(err as Error).message}`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Try to get the file from our bucket first
|
|
291
|
+
try {
|
|
292
|
+
const file = await c.env.BUCKET.get(filename)
|
|
293
|
+
|
|
294
|
+
// If file exists locally, stream it
|
|
295
|
+
if (file) {
|
|
296
|
+
try {
|
|
297
|
+
// We've already verified integrity above if needed
|
|
298
|
+
const headers = new Headers({
|
|
299
|
+
'Content-Type': 'application/octet-stream',
|
|
300
|
+
'Cache-Control': 'public, max-age=31536000',
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return new Response(file.body, {
|
|
304
|
+
status: 200,
|
|
305
|
+
headers
|
|
306
|
+
})
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(`[ERROR] Failed to stream local tarball ${filename}: ${(err as Error).message}`)
|
|
309
|
+
// Fall through to proxy if available
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error(`[STORAGE ERROR] Failed to get tarball from bucket ${filename}: ${(err as Error).message}`)
|
|
314
|
+
// Continue to proxy if available, otherwise fall through to 404
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If file doesn't exist and proxying is enabled, try to get it from upstream
|
|
318
|
+
if (PROXY) {
|
|
319
|
+
try {
|
|
320
|
+
// Construct the correct URL for scoped and unscoped packages
|
|
321
|
+
const tarballPath = pkg.includes('/') ?
|
|
322
|
+
`${pkg}/-/${tarball}` :
|
|
323
|
+
`${pkg}/-/${tarball}`
|
|
324
|
+
|
|
325
|
+
const source = `${PROXY_URL}/${tarballPath}`
|
|
326
|
+
console.log(`[PROXY] Fetching tarball from ${source}`)
|
|
327
|
+
|
|
328
|
+
// First do a HEAD request to check size
|
|
329
|
+
const headResponse = await fetch(source, {
|
|
330
|
+
method: 'HEAD',
|
|
331
|
+
headers: {
|
|
332
|
+
'User-Agent': 'vlt-serverless-registry'
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
if (!headResponse.ok) {
|
|
337
|
+
console.error(`[PROXY ERROR] HEAD request failed for ${filename}: ${headResponse.status}`)
|
|
338
|
+
return c.json({ error: 'Failed to check package size' }, 502)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const contentLength = parseInt(headResponse.headers.get('content-length') || '0', 10)
|
|
342
|
+
|
|
343
|
+
// Get the package response first, since we'll need it for all size cases
|
|
344
|
+
const response = await fetch(source, {
|
|
345
|
+
headers: {
|
|
346
|
+
'Accept': 'application/octet-stream',
|
|
347
|
+
'User-Agent': 'vlt-serverless-registry'
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (!response.ok || !response.body) {
|
|
352
|
+
console.error(`[PROXY ERROR] Failed to fetch package ${filename}: ${response.status}`)
|
|
353
|
+
return c.json({ error: 'Failed to fetch package' }, 502)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// For very large packages (100MB+), stream directly to client without storing
|
|
357
|
+
if (contentLength > 100_000_000) {
|
|
358
|
+
console.log(`[PROXY] Package is very large (${contentLength} bytes), streaming directly to client`)
|
|
359
|
+
|
|
360
|
+
const readable = response.body
|
|
361
|
+
|
|
362
|
+
// Return the stream to the client immediately
|
|
363
|
+
return new Response(readable, {
|
|
364
|
+
status: 200,
|
|
365
|
+
headers: new Headers({
|
|
366
|
+
'Content-Type': 'application/octet-stream',
|
|
367
|
+
'Content-Length': contentLength.toString(),
|
|
368
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// For medium-sized packages (10-100MB), stream directly to client and store async
|
|
374
|
+
if (contentLength > 10_000_000) {
|
|
375
|
+
// Clone the response since we'll need it twice
|
|
376
|
+
const [clientResponse, storageResponse] = response.body.tee()
|
|
377
|
+
|
|
378
|
+
// No integrity check when storing proxied packages
|
|
379
|
+
c.executionCtx.waitUntil((async () => {
|
|
380
|
+
try {
|
|
381
|
+
await c.env.BUCKET.put(filename, storageResponse, {
|
|
382
|
+
httpMetadata: {
|
|
383
|
+
contentType: 'application/octet-stream',
|
|
384
|
+
cacheControl: 'public, max-age=31536000',
|
|
385
|
+
// Store the integrity value if we have it from the manifest
|
|
386
|
+
...(expectedIntegrity && { integrity: expectedIntegrity })
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
console.log(`[PROXY] Successfully stored tarball ${filename}`)
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(`[STORAGE ERROR] Failed to store tarball ${filename}: ${(err as Error).message}`)
|
|
392
|
+
}
|
|
393
|
+
})())
|
|
394
|
+
|
|
395
|
+
// Stream directly to client
|
|
396
|
+
return new Response(clientResponse, {
|
|
397
|
+
status: 200,
|
|
398
|
+
headers: new Headers({
|
|
399
|
+
'Content-Type': 'application/octet-stream',
|
|
400
|
+
'Content-Length': contentLength.toString(),
|
|
401
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// For smaller packages, we can use the tee() approach safely
|
|
407
|
+
const [stream1, stream2] = response.body.tee()
|
|
408
|
+
|
|
409
|
+
// Store in R2 bucket asynchronously without integrity check for proxied packages
|
|
410
|
+
c.executionCtx.waitUntil((async () => {
|
|
411
|
+
try {
|
|
412
|
+
await c.env.BUCKET.put(filename, stream1, {
|
|
413
|
+
httpMetadata: {
|
|
414
|
+
contentType: 'application/octet-stream',
|
|
415
|
+
cacheControl: 'public, max-age=31536000',
|
|
416
|
+
// Store the integrity value if we have it from the manifest
|
|
417
|
+
...(expectedIntegrity && { integrity: expectedIntegrity })
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
console.log(`[PROXY] Successfully stored tarball ${filename}`)
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error(`[STORAGE ERROR] Failed to store tarball ${filename}: ${(err as Error).message}`)
|
|
423
|
+
}
|
|
424
|
+
})())
|
|
425
|
+
|
|
426
|
+
// Return the second stream to the client immediately
|
|
427
|
+
return new Response(stream2, {
|
|
428
|
+
status: 200,
|
|
429
|
+
headers: new Headers({
|
|
430
|
+
'Content-Type': 'application/octet-stream',
|
|
431
|
+
'Content-Length': contentLength.toString(),
|
|
432
|
+
'Cache-Control': 'public, max-age=31536000'
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(`[PROXY ERROR] Network error fetching tarball ${filename}: ${(err as Error).message}`)
|
|
438
|
+
return c.json({ error: 'Failed to contact upstream registry' }, 502)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return c.json({ error: 'Not found' }, 404)
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error(`[ERROR] Unhandled error in getPackageTarball: ${(err as Error).message}`)
|
|
445
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get a single package version manifest
|
|
451
|
+
*/
|
|
452
|
+
export async function getPackageManifest(c: HonoContext) {
|
|
453
|
+
try {
|
|
454
|
+
let { scope, pkg } = c.req.param() as { scope: string; pkg: string }
|
|
455
|
+
|
|
456
|
+
// Handle scoped packages correctly with URL decoding
|
|
457
|
+
try {
|
|
458
|
+
const packageName = decodePackageName(scope, pkg)
|
|
459
|
+
|
|
460
|
+
if (!packageName) {
|
|
461
|
+
throw new Error('Invalid package name')
|
|
462
|
+
}
|
|
463
|
+
pkg = packageName
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.error(`[ERROR] Failed to parse package name: ${(err as Error).message}`)
|
|
466
|
+
return c.json({ error: 'Invalid package name' }, 400)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Extract version from URL path
|
|
470
|
+
const pathParts = c.req.path.split('/')
|
|
471
|
+
const versionIndex = pathParts.findIndex(part => part === pkg) + 1
|
|
472
|
+
let version = pathParts[versionIndex] || 'latest'
|
|
473
|
+
|
|
474
|
+
// Decode URL-encoded version (e.g., %3E%3D1.0.0%20%3C2.0.0 becomes >=1.0.0 <2.0.0)
|
|
475
|
+
version = decodeURIComponent(version)
|
|
476
|
+
|
|
477
|
+
console.log(`[MANIFEST] Requesting manifest for ${pkg}@${version}`)
|
|
478
|
+
|
|
479
|
+
// If it's a semver range, try to resolve it to a specific version
|
|
480
|
+
let resolvedVersion = version
|
|
481
|
+
if (semver.validRange(version) && !semver.valid(version)) {
|
|
482
|
+
// This is a range, try to find the best matching version
|
|
483
|
+
try {
|
|
484
|
+
const packageData = await c.db.getPackage(pkg)
|
|
485
|
+
if (packageData) {
|
|
486
|
+
const versions = await c.db.getVersionsByPackage(pkg)
|
|
487
|
+
if (versions && versions.length > 0) {
|
|
488
|
+
const availableVersions = versions.map((v: any) => v.version)
|
|
489
|
+
const bestMatch = semver.maxSatisfying(availableVersions, version)
|
|
490
|
+
if (bestMatch) {
|
|
491
|
+
resolvedVersion = bestMatch
|
|
492
|
+
console.log(`[MANIFEST] Resolved range ${version} to version ${resolvedVersion}`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.error(`[ERROR] Failed to resolve version range: ${(err as Error).message}`)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Get the version from our database
|
|
502
|
+
const versionData = await c.db.getVersion(`${pkg}@${resolvedVersion}`)
|
|
503
|
+
|
|
504
|
+
if (versionData) {
|
|
505
|
+
// Convert the full manifest to a slimmed version for the response
|
|
506
|
+
const slimmedManifest = slimManifest(versionData.manifest)
|
|
507
|
+
|
|
508
|
+
// Ensure we have correct name, version and tarball URL
|
|
509
|
+
const ret = {
|
|
510
|
+
...slimmedManifest,
|
|
511
|
+
name: pkg,
|
|
512
|
+
version: resolvedVersion,
|
|
513
|
+
dist: {
|
|
514
|
+
...slimmedManifest.dist,
|
|
515
|
+
tarball: `${DOMAIN}/${createFile({ pkg, version: resolvedVersion })}`,
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Set proper headers for npm/bun
|
|
520
|
+
c.header('Content-Type', 'application/json')
|
|
521
|
+
c.header('Cache-Control', 'public, max-age=300') // 5 minute cache
|
|
522
|
+
|
|
523
|
+
return c.json(ret, 200)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return c.json({ error: 'Version not found' }, 404)
|
|
527
|
+
} catch (err) {
|
|
528
|
+
console.error(`[ERROR] Failed to get manifest: ${(err as Error).message}`)
|
|
529
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get package dist-tags
|
|
535
|
+
*/
|
|
536
|
+
export async function getPackageDistTags(c: HonoContext) {
|
|
537
|
+
try {
|
|
538
|
+
let { scope, pkg } = c.req.param() as { scope: string; pkg: string }
|
|
539
|
+
const tag = c.req.param('tag')
|
|
540
|
+
|
|
541
|
+
// Handle scoped packages correctly with URL decoding
|
|
542
|
+
try {
|
|
543
|
+
const packageName = decodePackageName(scope, pkg)
|
|
544
|
+
if (!packageName) {
|
|
545
|
+
throw new Error('Invalid package name')
|
|
546
|
+
}
|
|
547
|
+
pkg = packageName
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error(`[ERROR] Failed to parse package name: ${(err as Error).message}`)
|
|
550
|
+
return c.json({ error: 'Invalid package name' }, 400)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
console.log(`[DIST-TAGS] Getting dist-tags for ${pkg}${tag ? ` (tag: ${tag})` : ''}`)
|
|
554
|
+
|
|
555
|
+
const packageData = await c.db.getPackage(pkg)
|
|
556
|
+
|
|
557
|
+
if (!packageData) {
|
|
558
|
+
return c.json({ error: 'Package not found' }, 404)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const distTags = packageData.tags || {}
|
|
562
|
+
|
|
563
|
+
if (tag) {
|
|
564
|
+
// Return specific tag
|
|
565
|
+
if (distTags[tag]) {
|
|
566
|
+
return c.json({ [tag]: distTags[tag] })
|
|
567
|
+
} else {
|
|
568
|
+
return c.json({ error: `Tag '${tag}' not found` }, 404)
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
// Return all tags
|
|
572
|
+
return c.json(distTags)
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error(`[ERROR] Failed to get dist-tags: ${(err as Error).message}`)
|
|
576
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Set/update a package dist-tag
|
|
582
|
+
*/
|
|
583
|
+
export async function putPackageDistTag(c: HonoContext) {
|
|
584
|
+
try {
|
|
585
|
+
let { scope, pkg } = c.req.param() as { scope: string; pkg: string }
|
|
586
|
+
const tag = c.req.param('tag')
|
|
587
|
+
|
|
588
|
+
// Handle scoped packages correctly with URL decoding
|
|
589
|
+
try {
|
|
590
|
+
const packageName = decodePackageName(scope, pkg)
|
|
591
|
+
if (!packageName) {
|
|
592
|
+
throw new Error('Invalid package name')
|
|
593
|
+
}
|
|
594
|
+
pkg = packageName
|
|
595
|
+
} catch (err) {
|
|
596
|
+
console.error(`[ERROR] Failed to parse package name: ${(err as Error).message}`)
|
|
597
|
+
return c.json({ error: 'Invalid package name' }, 400)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const version = await c.req.text()
|
|
601
|
+
|
|
602
|
+
if (!tag || !version) {
|
|
603
|
+
return c.json({ error: 'Tag and version are required' }, 400)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
console.log(`[DIST-TAGS] Setting ${pkg}@${tag} -> ${version}`)
|
|
607
|
+
|
|
608
|
+
const packageData = await c.db.getPackage(pkg)
|
|
609
|
+
|
|
610
|
+
if (!packageData) {
|
|
611
|
+
return c.json({ error: 'Package not found' }, 404)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const distTags = packageData.tags || {}
|
|
615
|
+
distTags[tag] = version
|
|
616
|
+
|
|
617
|
+
await c.db.upsertPackage(pkg, distTags)
|
|
618
|
+
|
|
619
|
+
return c.json(distTags, 201)
|
|
620
|
+
} catch (err) {
|
|
621
|
+
console.error(`[ERROR] Failed to set dist-tag: ${(err as Error).message}`)
|
|
622
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Delete a package dist-tag
|
|
628
|
+
*/
|
|
629
|
+
export async function deletePackageDistTag(c: HonoContext) {
|
|
630
|
+
try {
|
|
631
|
+
let { scope, pkg } = c.req.param() as { scope: string; pkg: string }
|
|
632
|
+
const tag = c.req.param('tag')
|
|
633
|
+
|
|
634
|
+
// Handle scoped packages correctly with URL decoding
|
|
635
|
+
try {
|
|
636
|
+
const packageName = decodePackageName(scope, pkg)
|
|
637
|
+
if (!packageName) {
|
|
638
|
+
throw new Error('Invalid package name')
|
|
639
|
+
}
|
|
640
|
+
pkg = packageName
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(`[ERROR] Failed to parse package name: ${(err as Error).message}`)
|
|
643
|
+
return c.json({ error: 'Invalid package name' }, 400)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!tag) {
|
|
647
|
+
return c.json({ error: 'Tag is required' }, 400)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (tag === 'latest') {
|
|
651
|
+
return c.json({ error: 'Cannot delete latest tag' }, 400)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(`[DIST-TAGS] Deleting ${pkg}@${tag}`)
|
|
655
|
+
|
|
656
|
+
const packageData = await c.db.getPackage(pkg)
|
|
657
|
+
|
|
658
|
+
if (!packageData) {
|
|
659
|
+
return c.json({ error: 'Package not found' }, 404)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const distTags = packageData.tags || {}
|
|
663
|
+
|
|
664
|
+
if (!distTags[tag]) {
|
|
665
|
+
return c.json({ error: `Tag '${tag}' not found` }, 404)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
delete distTags[tag]
|
|
669
|
+
|
|
670
|
+
await c.db.upsertPackage(pkg, distTags)
|
|
671
|
+
|
|
672
|
+
return c.json(distTags)
|
|
673
|
+
} catch (err) {
|
|
674
|
+
console.error(`[ERROR] Failed to delete dist-tag: ${(err as Error).message}`)
|
|
675
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Handle general package routes (packument, manifest, tarball)
|
|
681
|
+
*/
|
|
682
|
+
export async function handlePackageRoute(c: HonoContext) {
|
|
683
|
+
try {
|
|
684
|
+
const path = c.req.path
|
|
685
|
+
|
|
686
|
+
// Check if this is a tarball request
|
|
687
|
+
if (path.includes('/-/')) {
|
|
688
|
+
return getPackageTarball(c)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Check if this has a version (manifest request)
|
|
692
|
+
const pathParts = path.split('/')
|
|
693
|
+
if (pathParts.length >= 3 && pathParts[2] && !pathParts[2].startsWith('-')) {
|
|
694
|
+
return getPackageManifest(c)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Otherwise it's a packument request
|
|
698
|
+
return getPackagePackument(c)
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(`[ERROR] Failed to handle package route: ${(err as Error).message}`)
|
|
701
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export async function getPackagePackument(c: HonoContext) {
|
|
706
|
+
try {
|
|
707
|
+
const name = c.req.param('pkg')
|
|
708
|
+
const scope = c.req.param('scope')
|
|
709
|
+
// Get the versionRange query parameter
|
|
710
|
+
const versionRange = c.req.query('versionRange')
|
|
711
|
+
|
|
712
|
+
console.log(`[DEBUG] getPackagePackument called for: name=${name}, scope=${scope}${versionRange ? `, with version range: ${versionRange}` : ''}`)
|
|
713
|
+
|
|
714
|
+
if (!name) {
|
|
715
|
+
console.log(`[ERROR] No package name provided in parameters`)
|
|
716
|
+
return c.json({ error: 'Package name is required' }, 400)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Check if versionRange is a valid semver range
|
|
720
|
+
const isValidRange = versionRange && semver.validRange(versionRange)
|
|
721
|
+
if (versionRange && !isValidRange) {
|
|
722
|
+
console.log(`[DEBUG] Invalid semver range provided: ${versionRange}`)
|
|
723
|
+
return c.json({ error: `Invalid semver range: ${versionRange}` }, 400)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Use racing cache strategy when PROXY is enabled or upstream is specified
|
|
727
|
+
const upstream = (c as any).upstream || (PROXY ? 'npm' : null)
|
|
728
|
+
if (upstream) {
|
|
729
|
+
console.log(`[RACING] Using racing cache strategy for packument: ${name} from upstream: ${upstream}`)
|
|
730
|
+
|
|
731
|
+
const fetchUpstreamFn = async () => {
|
|
732
|
+
console.log(`[RACING] Fetching packument from upstream for: ${name}`)
|
|
733
|
+
|
|
734
|
+
// Get the appropriate upstream configuration
|
|
735
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
736
|
+
if (!upstreamConfig) {
|
|
737
|
+
throw new Error(`Unknown upstream: ${upstream}`)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const upstreamUrl = buildUpstreamUrl(upstreamConfig, name)
|
|
741
|
+
console.log(`[RACING] Fetching from URL: ${upstreamUrl}`)
|
|
742
|
+
|
|
743
|
+
const response = await fetch(upstreamUrl, {
|
|
744
|
+
headers: {
|
|
745
|
+
'Accept': 'application/json',
|
|
746
|
+
'User-Agent': 'vlt-serverless-registry'
|
|
747
|
+
}
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
if (!response.ok) {
|
|
751
|
+
if (response.status === 404) {
|
|
752
|
+
throw new Error(`Package not found: ${name}`)
|
|
753
|
+
}
|
|
754
|
+
throw new Error(`Upstream returned ${response.status}`)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const upstreamData = await response.json() as any
|
|
758
|
+
console.log(`[RACING] Successfully fetched packument for: ${name}, has ${Object.keys(upstreamData.versions || {}).length} versions`)
|
|
759
|
+
|
|
760
|
+
// Prepare data for storage with consistent structure
|
|
761
|
+
const packageData = {
|
|
762
|
+
name,
|
|
763
|
+
'dist-tags': upstreamData['dist-tags'] || { latest: Object.keys(upstreamData.versions || {}).pop() || '' },
|
|
764
|
+
versions: {} as Record<string, any>,
|
|
765
|
+
time: {
|
|
766
|
+
modified: upstreamData.time?.modified || new Date().toISOString()
|
|
767
|
+
} as Record<string, string>
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Store timing information for each version
|
|
771
|
+
if (upstreamData.time) {
|
|
772
|
+
Object.entries(upstreamData.time).forEach(([version, time]) => {
|
|
773
|
+
if (version !== 'modified' && version !== 'created') {
|
|
774
|
+
packageData.time[version] = time as string
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Process versions and apply version range filter if needed
|
|
780
|
+
if (upstreamData.versions) {
|
|
781
|
+
const protocol = new URL(c.req.url).protocol.slice(0, -1) // Remove trailing ':'
|
|
782
|
+
const host = c.req.header('host') || 'localhost:1337'
|
|
783
|
+
const context = { protocol, host, upstream }
|
|
784
|
+
|
|
785
|
+
Object.entries(upstreamData.versions).forEach(([version, manifest]) => {
|
|
786
|
+
// Skip versions that don't satisfy the range if a valid range is provided
|
|
787
|
+
if (isValidRange && !semver.satisfies(version, versionRange)) {
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Create a slimmed version of the manifest for the response with context for URL rewriting
|
|
792
|
+
packageData.versions[version] = slimManifest(manifest as any, context)
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Return just the packageData for caching - the cache function handles storage metadata separately
|
|
797
|
+
return packageData
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
const result = await getCachedPackageWithRefresh(c, name, fetchUpstreamFn, {
|
|
802
|
+
packumentTtlMinutes: 5,
|
|
803
|
+
upstream
|
|
804
|
+
}) as any
|
|
805
|
+
|
|
806
|
+
if (result.fromCache) {
|
|
807
|
+
console.log(`[RACING] Using cached data for: ${name}${result.stale ? ' (stale)' : ''}`)
|
|
808
|
+
|
|
809
|
+
// If we have cached data, still need to check if we need to filter by version range
|
|
810
|
+
if (isValidRange && result.package?.versions) {
|
|
811
|
+
const filteredVersions: Record<string, any> = {}
|
|
812
|
+
Object.keys(result.package.versions).forEach(version => {
|
|
813
|
+
if (semver.satisfies(version, versionRange)) {
|
|
814
|
+
filteredVersions[version] = result.package.versions[version]
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
result.package.versions = filteredVersions
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return c.json(result.package, 200)
|
|
821
|
+
} else {
|
|
822
|
+
console.log(`[RACING] Using fresh upstream data for: ${name}`)
|
|
823
|
+
return c.json(result.package, 200)
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error(`[RACING ERROR] Failed to get package ${name}: ${(error as Error).message}`)
|
|
827
|
+
|
|
828
|
+
// Return more specific error codes
|
|
829
|
+
if ((error as Error).message.includes('Package not found')) {
|
|
830
|
+
return c.json({ error: `Package '${name}' not found` }, 404)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return c.json({ error: 'Failed to fetch package data' }, 502)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Fallback to original logic when PROXY is disabled
|
|
838
|
+
const pkg = await c.db.getPackage(name)
|
|
839
|
+
const now = new Date()
|
|
840
|
+
|
|
841
|
+
// Initialize the consistent packument response structure
|
|
842
|
+
const packageData = {
|
|
843
|
+
name,
|
|
844
|
+
'dist-tags': { latest: '' } as any,
|
|
845
|
+
versions: {} as Record<string, any>,
|
|
846
|
+
time: {
|
|
847
|
+
modified: now.toISOString()
|
|
848
|
+
} as Record<string, string>
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (pkg) {
|
|
852
|
+
// Update dist-tags from the database
|
|
853
|
+
packageData['dist-tags'] = pkg.tags || { latest: '' }
|
|
854
|
+
|
|
855
|
+
// Update modified time
|
|
856
|
+
if (pkg.lastUpdated) {
|
|
857
|
+
packageData.time.modified = pkg.lastUpdated
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Get all versions for this package
|
|
862
|
+
try {
|
|
863
|
+
const allVersions = await c.db.getVersionsByPackage(name)
|
|
864
|
+
|
|
865
|
+
if (allVersions && allVersions.length > 0) {
|
|
866
|
+
console.log(`[DEBUG] Found ${allVersions.length} versions for ${name} in the database`)
|
|
867
|
+
|
|
868
|
+
// Add all versions to the packument, use slimmed manifests
|
|
869
|
+
for (const versionData of allVersions) {
|
|
870
|
+
// Skip versions that don't satisfy the version range if provided
|
|
871
|
+
if (isValidRange && !semver.satisfies((versionData as any).version, versionRange)) {
|
|
872
|
+
continue
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Use slimManifest to create a smaller response
|
|
876
|
+
packageData.versions[(versionData as any).version] = slimManifest((versionData as any).manifest)
|
|
877
|
+
packageData.time[(versionData as any).version] = (versionData as any).published_at
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
console.log(`[DEBUG] No versions found for ${name} in the database`)
|
|
881
|
+
|
|
882
|
+
// Add at least the latest version as a fallback if it satisfies the range
|
|
883
|
+
const latestVersion = packageData['dist-tags'].latest
|
|
884
|
+
if (latestVersion && (!isValidRange || semver.satisfies(latestVersion, versionRange))) {
|
|
885
|
+
const versionData = await c.db.getVersion(`${name}@${latestVersion}`)
|
|
886
|
+
if (versionData) {
|
|
887
|
+
packageData.versions[latestVersion] = slimManifest(versionData.manifest)
|
|
888
|
+
packageData.time[latestVersion] = (versionData as any).published_at
|
|
889
|
+
} else {
|
|
890
|
+
// Create a mock version for testing
|
|
891
|
+
packageData.versions[latestVersion] = {
|
|
892
|
+
name: name,
|
|
893
|
+
version: latestVersion,
|
|
894
|
+
description: `Mock package for ${name}`,
|
|
895
|
+
dist: {
|
|
896
|
+
tarball: `${DOMAIN}/${name}/-/${name}-${latestVersion}.tgz`
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch (err) {
|
|
903
|
+
console.error(`[DB ERROR] Failed to get versions for package ${name}: ${(err as Error).message}`)
|
|
904
|
+
|
|
905
|
+
// Create a basic version if none are found
|
|
906
|
+
const latestVersion = packageData['dist-tags'].latest
|
|
907
|
+
if (latestVersion) {
|
|
908
|
+
packageData.versions[latestVersion] = {
|
|
909
|
+
name: name,
|
|
910
|
+
version: latestVersion,
|
|
911
|
+
description: `Package ${name}`,
|
|
912
|
+
dist: {
|
|
913
|
+
tarball: `${DOMAIN}/${name}/-/${name}-${latestVersion}.tgz`
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return c.json(packageData, 200)
|
|
920
|
+
} catch (err) {
|
|
921
|
+
console.error(`[ERROR] Failed to get packument: ${(err as Error).message}`)
|
|
922
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
923
|
+
}
|
|
924
|
+
}
|