@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { EXPOSE_DOCS, API_DOCS, VERSION } from '../config.ts'
|
|
2
|
+
import * as Sentry from "@sentry/cloudflare"
|
|
3
|
+
import { Hono } from 'hono'
|
|
4
|
+
import { HTTPException } from 'hono/http-exception'
|
|
5
|
+
import { requestId } from 'hono/request-id'
|
|
6
|
+
import { bearerAuth } from 'hono/bearer-auth'
|
|
7
|
+
import { except } from 'hono/combine'
|
|
8
|
+
import { apiReference } from '@scalar/hono-api-reference'
|
|
9
|
+
import { secureHeaders } from 'hono/secure-headers'
|
|
10
|
+
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
11
|
+
import { getApp } from './utils/spa.ts'
|
|
12
|
+
import { verifyToken } from './utils/auth.ts'
|
|
13
|
+
import { mountDatabase } from './utils/database.ts'
|
|
14
|
+
import { jsonResponseHandler } from './utils/response.ts'
|
|
15
|
+
import { requiresToken, catchAll, notFound, isOK } from './utils/routes.ts'
|
|
16
|
+
import { handleStaticAssets } from './routes/static.ts'
|
|
17
|
+
import { getUsername, getUserProfile } from './routes/users.ts'
|
|
18
|
+
import { getToken, putToken, postToken, deleteToken } from './routes/tokens.ts'
|
|
19
|
+
import {
|
|
20
|
+
getPackageDistTags,
|
|
21
|
+
putPackageDistTag,
|
|
22
|
+
deletePackageDistTag,
|
|
23
|
+
handlePackageRoute,
|
|
24
|
+
getPackagePackument,
|
|
25
|
+
getPackageManifest,
|
|
26
|
+
getPackageTarball,
|
|
27
|
+
} from './routes/packages.ts'
|
|
28
|
+
import {
|
|
29
|
+
listPackagesAccess,
|
|
30
|
+
getPackageAccessStatus,
|
|
31
|
+
setPackageAccessStatus,
|
|
32
|
+
grantPackageAccess,
|
|
33
|
+
revokePackageAccess,
|
|
34
|
+
} from './routes/access.ts'
|
|
35
|
+
import { searchPackages } from './routes/search.ts'
|
|
36
|
+
import { handleLogin, handleCallback, requiresAuth } from './routes/auth.ts'
|
|
37
|
+
import { sessionMonitor } from './utils/tracing.ts'
|
|
38
|
+
import { getUpstreamConfig, buildUpstreamUrl, isValidUpstreamName, getDefaultUpstream } from './utils/upstream.ts'
|
|
39
|
+
import { createDatabaseOperations } from './db/client.ts'
|
|
40
|
+
import type { Environment } from '../types.ts'
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------
|
|
43
|
+
// App Initialization
|
|
44
|
+
// ("strict mode" is turned off to ensure that routes like
|
|
45
|
+
// `/hello` & `/hello/` are handled the same way - ref.
|
|
46
|
+
// https://hono.dev/docs/api/hono#strict-mode)
|
|
47
|
+
// ---------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const app = new Hono<{ Bindings: Environment }>({ strict: false })
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------
|
|
52
|
+
// Middleware
|
|
53
|
+
// ---------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
app.use(trimTrailingSlash())
|
|
56
|
+
app.use('*', requestId())
|
|
57
|
+
app.use('*', jsonResponseHandler() as any)
|
|
58
|
+
app.use('*', secureHeaders())
|
|
59
|
+
app.use('*', mountDatabase as any)
|
|
60
|
+
app.use('*', sessionMonitor as any)
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------
|
|
63
|
+
// Home
|
|
64
|
+
// (single page application)
|
|
65
|
+
// ---------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
app.get('/', async (c) => c.html(await getApp()))
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------
|
|
70
|
+
// API Documentation
|
|
71
|
+
// ---------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
if (EXPOSE_DOCS) {
|
|
74
|
+
app.get('/docs', apiReference(API_DOCS as any))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------
|
|
78
|
+
// Health Check
|
|
79
|
+
// ---------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
app.get('/-/ping', isOK as any)
|
|
82
|
+
app.get('/health', isOK as any)
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------
|
|
85
|
+
// Search Routes
|
|
86
|
+
// ---------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
app.get('/-/search', searchPackages as any)
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------
|
|
91
|
+
// Authentication Routes
|
|
92
|
+
// ---------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
app.get('/-/auth/callback', handleCallback as any)
|
|
95
|
+
app.get('/-/auth/login', handleLogin as any)
|
|
96
|
+
app.get('/-/auth/user', requiresAuth as any, isOK as any)
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------
|
|
99
|
+
// Authorization Verification Middleware
|
|
100
|
+
// ---------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
app.use('*', except(requiresToken as any, bearerAuth({ verifyToken }) as any))
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------
|
|
105
|
+
// User Routes
|
|
106
|
+
// ---------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
app.get('/-/whoami', getUsername as any)
|
|
109
|
+
app.get('/-/user', getUserProfile as any)
|
|
110
|
+
|
|
111
|
+
// Handle npm login/adduser (for publishing) - temporary development endpoint
|
|
112
|
+
app.put('/-/user/org.couchdb.user:*', async (c: any) => {
|
|
113
|
+
console.log(`[AUTH] Login attempt`)
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const body = await c.req.json()
|
|
117
|
+
console.log(`[AUTH] Login request for user: ${body.name}`)
|
|
118
|
+
|
|
119
|
+
// For development, accept any login and return a token
|
|
120
|
+
const token = 'npm_' + Math.random().toString(36).substr(2, 30)
|
|
121
|
+
|
|
122
|
+
return c.json({
|
|
123
|
+
ok: true,
|
|
124
|
+
id: `org.couchdb.user:${body.name || 'test-user'}`,
|
|
125
|
+
rev: '1-' + Math.random().toString(36).substr(2, 10),
|
|
126
|
+
token: token
|
|
127
|
+
})
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(`[AUTH ERROR] ${(err as Error).message}`)
|
|
130
|
+
return c.json({ error: 'Invalid request body' }, 400)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------
|
|
135
|
+
// Project Routes
|
|
136
|
+
// TODO: Remove extranerous routes once GUI updates
|
|
137
|
+
// ---------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
app.get(['/dashboard.json', '/local/dashboard.json', '/-/projects'], async (c: any) => {
|
|
140
|
+
const data = await fetch(`http://localhost:${process.env.DAEMON_PORT || 3000}/dashboard.json`)
|
|
141
|
+
return c.json(await data.json())
|
|
142
|
+
})
|
|
143
|
+
app.get(['/app-data.json', '/local/app-data.json', '/-/info'], (c: any) => c.json({
|
|
144
|
+
buildVersion: VERSION,
|
|
145
|
+
}))
|
|
146
|
+
|
|
147
|
+
// Capture specific extraneous local routes & redirect to root
|
|
148
|
+
// Note: This must be more specific to not interfere with package routes
|
|
149
|
+
app.get('/local/dashboard.json', (c: any) => c.redirect('/', 308))
|
|
150
|
+
app.get('/local/app-data.json', (c: any) => c.redirect('/', 308))
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------
|
|
153
|
+
// Token Routes
|
|
154
|
+
// ---------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
app.get('/-/tokens', getToken as any)
|
|
157
|
+
app.post('/-/tokens', postToken as any)
|
|
158
|
+
app.put('/-/tokens', putToken as any)
|
|
159
|
+
app.delete('/-/tokens/:token', deleteToken as any)
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------
|
|
162
|
+
// Dist-tag Routes
|
|
163
|
+
// ---------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
// Unscoped packages
|
|
166
|
+
app.get('/-/package/:pkg/dist-tags', getPackageDistTags as any)
|
|
167
|
+
app.get('/-/package/:pkg/dist-tags/:tag', getPackageDistTags as any)
|
|
168
|
+
app.put('/-/package/:pkg/dist-tags/:tag', putPackageDistTag as any)
|
|
169
|
+
app.delete('/-/package/:pkg/dist-tags/:tag', deletePackageDistTag as any)
|
|
170
|
+
|
|
171
|
+
// Scoped packages
|
|
172
|
+
app.get('/-/package/:scope%2f:pkg/dist-tags', getPackageDistTags as any)
|
|
173
|
+
app.get('/-/package/:scope%2f:pkg/dist-tags/:tag', getPackageDistTags as any)
|
|
174
|
+
app.put('/-/package/:scope%2f:pkg/dist-tags/:tag', putPackageDistTag as any)
|
|
175
|
+
app.delete('/-/package/:scope%2f:pkg/dist-tags/:tag', deletePackageDistTag as any)
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------
|
|
178
|
+
// Package Access Management Routes
|
|
179
|
+
// ---------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
app.get('/-/package/:pkg/access', getPackageAccessStatus as any)
|
|
182
|
+
app.put('/-/package/:pkg/access', setPackageAccessStatus as any)
|
|
183
|
+
app.get('/-/package/:scope%2f:pkg/access', getPackageAccessStatus as any)
|
|
184
|
+
app.put('/-/package/:scope%2f:pkg/access', setPackageAccessStatus as any)
|
|
185
|
+
app.get('/-/package/list', listPackagesAccess as any)
|
|
186
|
+
app.put('/-/package/:pkg/collaborators/:username', grantPackageAccess as any)
|
|
187
|
+
app.delete('/-/package/:pkg/collaborators/:username', revokePackageAccess as any)
|
|
188
|
+
app.put('/-/package/:scope%2f:pkg/collaborators/:username', grantPackageAccess as any)
|
|
189
|
+
app.delete('/-/package/:scope%2f:pkg/collaborators/:username', revokePackageAccess as any)
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------
|
|
192
|
+
// Security/Audit Endpoints
|
|
193
|
+
// (npm audit, npm audit fix, etc.)
|
|
194
|
+
// ---------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
// Handle npm audit bulk endpoint (used by npm audit)
|
|
197
|
+
app.post('/-/npm/v1/security/advisories/bulk', (c: any) => {
|
|
198
|
+
console.log(`[AUDIT] Rejecting audit bulk request - security auditing not supported`)
|
|
199
|
+
return c.json({
|
|
200
|
+
error: 'Security auditing is not supported by this registry',
|
|
201
|
+
code: 'E_AUDIT_NOT_SUPPORTED',
|
|
202
|
+
detail: 'This private registry does not provide security vulnerability data. Please use `npm audit --registry=https://registry.npmjs.org` to audit against the public npm registry.'
|
|
203
|
+
}, 501)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Handle npm audit quick endpoint
|
|
207
|
+
app.post('/-/npm/v1/security/audits/quick', (c: any) => {
|
|
208
|
+
console.log(`[AUDIT] Rejecting audit quick request - security auditing not supported`)
|
|
209
|
+
return c.json({
|
|
210
|
+
error: 'Security auditing is not supported by this registry',
|
|
211
|
+
code: 'E_AUDIT_NOT_SUPPORTED',
|
|
212
|
+
detail: 'This private registry does not provide security vulnerability data. Please use `npm audit --registry=https://registry.npmjs.org` to audit against the public npm registry.'
|
|
213
|
+
}, 501)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Handle npm ping (health check) - this one we can support
|
|
217
|
+
app.get('/-/npm/v1/ping', (c: any) => {
|
|
218
|
+
return c.json({}, 200)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Handle npm fund endpoint (used by npm fund)
|
|
222
|
+
app.get('/-/npm/v1/funds', (c: any) => {
|
|
223
|
+
console.log(`[FUND] Rejecting fund request - funding data not supported`)
|
|
224
|
+
return c.json({
|
|
225
|
+
error: 'Funding data is not supported by this registry',
|
|
226
|
+
code: 'E_FUND_NOT_SUPPORTED',
|
|
227
|
+
detail: 'This private registry does not provide package funding information.'
|
|
228
|
+
}, 501)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Handle vulnerability database endpoints
|
|
232
|
+
app.get('/-/npm/v1/security/advisories', (c: any) => {
|
|
233
|
+
console.log(`[AUDIT] Rejecting advisories request - security auditing not supported`)
|
|
234
|
+
return c.json({
|
|
235
|
+
error: 'Security advisory data is not supported by this registry',
|
|
236
|
+
code: 'E_ADVISORY_NOT_SUPPORTED',
|
|
237
|
+
detail: 'This private registry does not provide security vulnerability data.'
|
|
238
|
+
}, 501)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Handle package metadata bulk endpoints (used by some npm operations)
|
|
242
|
+
app.post('/-/npm/v1/packages/bulk', (c: any) => {
|
|
243
|
+
console.log(`[BULK] Rejecting bulk package metadata request - not supported`)
|
|
244
|
+
return c.json({
|
|
245
|
+
error: 'Bulk package metadata operations are not supported by this registry',
|
|
246
|
+
code: 'E_BULK_NOT_SUPPORTED',
|
|
247
|
+
detail: 'This private registry does not support bulk metadata operations.'
|
|
248
|
+
}, 501)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Handle other security/audit endpoints (must come before general npm v1 catch-all)
|
|
252
|
+
app.all('/-/npm/v1/security/*', (c: any) => {
|
|
253
|
+
console.log(`[AUDIT] Rejecting security request to ${c.req.path} - not supported`)
|
|
254
|
+
return c.json({
|
|
255
|
+
error: 'Security endpoints are not supported by this registry',
|
|
256
|
+
code: 'E_SECURITY_NOT_SUPPORTED',
|
|
257
|
+
detail: 'This private registry does not provide security vulnerability data. Please use the public npm registry for security-related operations.'
|
|
258
|
+
}, 501)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Handle other npm v1 endpoints that we don't support (catch-all for remaining npm v1 routes)
|
|
262
|
+
app.all('/-/npm/v1/*', (c: any) => {
|
|
263
|
+
console.log(`[NPM_V1] Rejecting unsupported npm v1 request to ${c.req.path}`)
|
|
264
|
+
return c.json({
|
|
265
|
+
error: 'This npm v1 endpoint is not supported by this registry',
|
|
266
|
+
code: 'E_ENDPOINT_NOT_SUPPORTED',
|
|
267
|
+
detail: `The endpoint ${c.req.path} is not implemented by this private registry.`
|
|
268
|
+
}, 501)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------
|
|
272
|
+
// Redirect Legacy NPM Routing Warts
|
|
273
|
+
// (maximizes backwards compatibility)
|
|
274
|
+
// ---------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
app.get('/-/v1/search', (c: any) => c.redirect('/-/search', 308))
|
|
277
|
+
app.get('/-/npm/v1/user', (c: any) => c.redirect('/-/user', 308))
|
|
278
|
+
app.get('/-/npm/v1/tokens', (c: any) => c.redirect('/-/tokens', 308))
|
|
279
|
+
app.post('/-/npm/v1/tokens', (c: any) => c.redirect('/-/tokens', 308))
|
|
280
|
+
app.put('/-/npm/v1/tokens', (c: any) => c.redirect('/-/tokens', 308))
|
|
281
|
+
app.delete('/-/npm/v1/tokens/token/:token', (c: any) => {
|
|
282
|
+
return c.redirect(`/-/tokens/${c.req.param('token')}`, 308)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------
|
|
286
|
+
// Static Asset Routes
|
|
287
|
+
// (must come before wildcard package routes)
|
|
288
|
+
// ---------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
// Note: Wrangler serves files from src/assets/ at root level, now consolidated under /public/ prefix
|
|
291
|
+
app.get('/favicon.ico', notFound as any)
|
|
292
|
+
app.get('/robots.txt', notFound as any)
|
|
293
|
+
// app.get('/static/*', notFound)
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------
|
|
296
|
+
// Upstream Package Routes
|
|
297
|
+
// (must come before catch-all package routes)
|
|
298
|
+
// ---------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
// Handle upstream package requests like /npm/lodash, /jsr/@std/fs
|
|
301
|
+
app.get('/:upstream/:pkg', async (c: any) => {
|
|
302
|
+
const upstream = c.req.param('upstream')
|
|
303
|
+
const pkg = c.req.param('pkg')
|
|
304
|
+
|
|
305
|
+
console.log(`[UPSTREAM ROUTE] Called with upstream=${upstream}, pkg=${pkg}`)
|
|
306
|
+
|
|
307
|
+
// Validate upstream name
|
|
308
|
+
if (!isValidUpstreamName(upstream)) {
|
|
309
|
+
console.log(`[UPSTREAM ROUTE] Invalid upstream name: ${upstream}`)
|
|
310
|
+
return c.json({ error: `Invalid or reserved upstream name: ${upstream}` }, 400)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if upstream is configured
|
|
314
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
315
|
+
if (!upstreamConfig) {
|
|
316
|
+
return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log(`[UPSTREAM] Package request for ${pkg} from ${upstream}`)
|
|
320
|
+
|
|
321
|
+
// Set upstream context and forward to package handler
|
|
322
|
+
c.upstream = upstream
|
|
323
|
+
|
|
324
|
+
// Create a mock parameter function that returns the package name as both scope and pkg
|
|
325
|
+
const originalParam = c.req.param
|
|
326
|
+
c.req.param = (key: string) => {
|
|
327
|
+
if (key === 'scope') return pkg
|
|
328
|
+
if (key === 'pkg') return pkg // Return the package name for 'pkg' parameter too
|
|
329
|
+
return originalParam.call(c.req, key)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return getPackagePackument(c)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// Handle scoped packages like /npm/@babel/core
|
|
336
|
+
app.get('/:upstream/:scope%2f:pkg', async (c: any) => {
|
|
337
|
+
const upstream = c.req.param('upstream')
|
|
338
|
+
const scope = c.req.param('scope')
|
|
339
|
+
const pkg = c.req.param('pkg')
|
|
340
|
+
|
|
341
|
+
// Validate upstream name
|
|
342
|
+
if (!isValidUpstreamName(upstream)) {
|
|
343
|
+
return c.json({ error: `Invalid or reserved upstream name: ${upstream}` }, 400)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check if upstream is configured
|
|
347
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
348
|
+
if (!upstreamConfig) {
|
|
349
|
+
return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const fullPackageName = `@${scope}/${pkg}`
|
|
353
|
+
console.log(`[UPSTREAM] Scoped package request for ${fullPackageName} from ${upstream}`)
|
|
354
|
+
|
|
355
|
+
// Set upstream context
|
|
356
|
+
c.upstream = upstream
|
|
357
|
+
|
|
358
|
+
// Create a mock parameter function that returns the full package name as both scope and pkg
|
|
359
|
+
const originalParam = c.req.param
|
|
360
|
+
c.req.param = (key: string) => {
|
|
361
|
+
if (key === 'scope') return fullPackageName
|
|
362
|
+
if (key === 'pkg') return fullPackageName // Return the full package name for 'pkg' parameter too
|
|
363
|
+
return originalParam.call(c.req, key)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return getPackagePackument(c)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// Handle upstream version requests like /npm/lodash/4.17.21
|
|
370
|
+
app.get('/:upstream/:pkg/:version', async (c: any) => {
|
|
371
|
+
const upstream = c.req.param('upstream')
|
|
372
|
+
const pkg = c.req.param('pkg')
|
|
373
|
+
const version = c.req.param('version')
|
|
374
|
+
|
|
375
|
+
// Validate upstream name
|
|
376
|
+
if (!isValidUpstreamName(upstream)) {
|
|
377
|
+
return c.json({ error: `Invalid or reserved upstream name: ${upstream}` }, 400)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if upstream is configured
|
|
381
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
382
|
+
if (!upstreamConfig) {
|
|
383
|
+
return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log(`[UPSTREAM] Version request for ${pkg}@${version} from ${upstream}`)
|
|
387
|
+
|
|
388
|
+
// Set upstream context
|
|
389
|
+
c.upstream = upstream
|
|
390
|
+
|
|
391
|
+
// Create a mock parameter function
|
|
392
|
+
const originalParam = c.req.param
|
|
393
|
+
c.req.param = (key: string) => {
|
|
394
|
+
if (key === 'scope') return pkg
|
|
395
|
+
if (key === 'pkg') return pkg
|
|
396
|
+
if (key === 'version') return version
|
|
397
|
+
return originalParam.call(c.req, key)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return getPackageManifest(c)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// Handle upstream scoped package tarball requests like /npm/@scope/package/-/package-1.2.3.tgz
|
|
404
|
+
app.get('/:upstream/:scope/:pkg/-/:tarball', async (c: any) => {
|
|
405
|
+
const upstream = c.req.param('upstream')
|
|
406
|
+
const scope = c.req.param('scope')
|
|
407
|
+
const pkg = c.req.param('pkg')
|
|
408
|
+
const tarball = c.req.param('tarball')
|
|
409
|
+
|
|
410
|
+
// Validate upstream name
|
|
411
|
+
if (!isValidUpstreamName(upstream)) {
|
|
412
|
+
return c.json({ error: `Invalid or reserved upstream name: ${upstream}` }, 400)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check if upstream is configured
|
|
416
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
417
|
+
if (!upstreamConfig) {
|
|
418
|
+
return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log(`[UPSTREAM] Scoped tarball request for ${scope}/${pkg}/-/${tarball} from ${upstream}`)
|
|
422
|
+
|
|
423
|
+
// Set upstream context
|
|
424
|
+
c.upstream = upstream
|
|
425
|
+
|
|
426
|
+
// Create a mock parameter function for scoped packages
|
|
427
|
+
const originalParam = c.req.param
|
|
428
|
+
c.req.param = (key: string) => {
|
|
429
|
+
if (key === 'scope') return scope
|
|
430
|
+
if (key === 'pkg') return pkg
|
|
431
|
+
if (key === 'tarball') return tarball
|
|
432
|
+
return originalParam.call(c.req, key)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return getPackageTarball(c)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// Handle upstream tarball requests like /npm/lodash/-/lodash-4.17.21.tgz
|
|
439
|
+
app.get('/:upstream/:pkg/-/:tarball', async (c: any) => {
|
|
440
|
+
const upstream = c.req.param('upstream')
|
|
441
|
+
const pkg = c.req.param('pkg')
|
|
442
|
+
const tarball = c.req.param('tarball')
|
|
443
|
+
|
|
444
|
+
// Validate upstream name
|
|
445
|
+
if (!isValidUpstreamName(upstream)) {
|
|
446
|
+
return c.json({ error: `Invalid or reserved upstream name: ${upstream}` }, 400)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check if upstream is configured
|
|
450
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
451
|
+
if (!upstreamConfig) {
|
|
452
|
+
return c.json({ error: `Unknown upstream: ${upstream}` }, 404)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(`[UPSTREAM] Tarball request for ${pkg}/-/${tarball} from ${upstream}`)
|
|
456
|
+
|
|
457
|
+
// Set upstream context
|
|
458
|
+
c.upstream = upstream
|
|
459
|
+
|
|
460
|
+
// Create a mock parameter function
|
|
461
|
+
const originalParam = c.req.param
|
|
462
|
+
c.req.param = (key: string) => {
|
|
463
|
+
if (key === 'scope') return pkg // For unscoped packages, scope is the package name
|
|
464
|
+
if (key === 'pkg') return undefined // For unscoped packages, pkg parameter should be undefined
|
|
465
|
+
if (key === 'tarball') return tarball
|
|
466
|
+
return originalParam.call(c.req, key)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return getPackageTarball(c)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Handle security audit endpoints
|
|
473
|
+
app.post('/:upstream/-/npm/v1/security/advisories/bulk', async (c: any) => {
|
|
474
|
+
const upstream = c.req.param('upstream')
|
|
475
|
+
|
|
476
|
+
console.log(`[AUDIT] Security audit request for upstream: ${upstream}`)
|
|
477
|
+
|
|
478
|
+
// Return empty audit results - no vulnerabilities found
|
|
479
|
+
// This satisfies npm's security audit without requiring upstream forwarding
|
|
480
|
+
return c.json({})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Handle security audit endpoints without upstream (fall back to default)
|
|
484
|
+
app.post('/-/npm/v1/security/advisories/bulk', async (c: any) => {
|
|
485
|
+
console.log(`[AUDIT] Security audit request (no upstream specified)`)
|
|
486
|
+
|
|
487
|
+
// Return empty audit results - no vulnerabilities found
|
|
488
|
+
return c.json({})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// Handle package publishing
|
|
492
|
+
app.put('/:pkg', async (c: any) => {
|
|
493
|
+
const pkg = decodeURIComponent(c.req.param('pkg'))
|
|
494
|
+
const authHeader = c.req.header('authorization') || c.req.header('Authorization')
|
|
495
|
+
|
|
496
|
+
console.log(`[PUBLISH] Publishing package: ${pkg}`)
|
|
497
|
+
console.log(`[PUBLISH] Auth header: ${authHeader ? 'provided' : 'missing'}`)
|
|
498
|
+
|
|
499
|
+
// Check for authentication
|
|
500
|
+
if (!authHeader) {
|
|
501
|
+
return c.json({
|
|
502
|
+
error: 'Authentication required',
|
|
503
|
+
reason: 'You must be logged in to publish packages. Run "npm adduser" first.'
|
|
504
|
+
}, 401)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const body = await c.req.json()
|
|
509
|
+
console.log(`[PUBLISH] Package data received for ${pkg}, versions: ${Object.keys(body.versions || {}).length}`)
|
|
510
|
+
|
|
511
|
+
// For development, just return success
|
|
512
|
+
return c.json({
|
|
513
|
+
ok: true,
|
|
514
|
+
id: pkg,
|
|
515
|
+
rev: '1-' + Math.random().toString(36).substr(2, 10)
|
|
516
|
+
})
|
|
517
|
+
} catch (err) {
|
|
518
|
+
console.error(`[PUBLISH ERROR] ${(err as Error).message}`)
|
|
519
|
+
return c.json({ error: 'Invalid package data' }, 400)
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// Redirect root-level packages to default upstream (for backward compatibility)
|
|
524
|
+
app.get('/:pkg', async (c: any) => {
|
|
525
|
+
const pkg = decodeURIComponent(c.req.param('pkg'))
|
|
526
|
+
|
|
527
|
+
// Skip if this looks like a static asset or internal route
|
|
528
|
+
if (pkg.includes('.') || pkg.startsWith('-') || pkg.startsWith('_')) {
|
|
529
|
+
return c.next()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const defaultUpstream = getDefaultUpstream()
|
|
533
|
+
console.log(`[REDIRECT] Redirecting ${pkg} to default upstream: ${defaultUpstream}`)
|
|
534
|
+
|
|
535
|
+
return c.redirect(`/${defaultUpstream}/${pkg}`, 302)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
// ---------------------------------------------------------
|
|
539
|
+
// Package Routes
|
|
540
|
+
// ---------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
app.get('/*', handleStaticAssets as any)
|
|
543
|
+
app.put('/*', handlePackageRoute as any)
|
|
544
|
+
app.post('/*', handlePackageRoute as any)
|
|
545
|
+
app.delete('/*', handlePackageRoute as any)
|
|
546
|
+
|
|
547
|
+
// ---------------------------------------------------------
|
|
548
|
+
// Catch-All-The-Things
|
|
549
|
+
// ---------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
// app.all('*', catchAll)
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------
|
|
554
|
+
// Error Handling
|
|
555
|
+
// ---------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
app.onError((err, c) => {
|
|
558
|
+
Sentry.captureException(err);
|
|
559
|
+
if (err instanceof HTTPException) {
|
|
560
|
+
return err.getResponse();
|
|
561
|
+
}
|
|
562
|
+
return c.json({ error: 'Internal server error' }, 500);
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------
|
|
566
|
+
// Wrap with Sentry
|
|
567
|
+
// ---------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
export default Sentry.withSentry(
|
|
570
|
+
(env: Environment) => {
|
|
571
|
+
const { id: versionId } = env.CF_VERSION_METADATA
|
|
572
|
+
return {
|
|
573
|
+
dsn: env.SENTRY.DSN,
|
|
574
|
+
release: versionId,
|
|
575
|
+
tracesSampleRate: 1.0,
|
|
576
|
+
_experiments: { enableLogs: true },
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
app
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Queue consumer for background cache refresh jobs
|
|
584
|
+
*/
|
|
585
|
+
export async function queue(batch: any, env: Environment, ctx: any) {
|
|
586
|
+
console.log(`[QUEUE] Processing batch of ${batch.messages.length} messages`)
|
|
587
|
+
|
|
588
|
+
// Create database operations
|
|
589
|
+
const db = createDatabaseOperations(env.DB)
|
|
590
|
+
|
|
591
|
+
for (const message of batch.messages) {
|
|
592
|
+
try {
|
|
593
|
+
const { type, packageName, spec, upstream, options } = message.body
|
|
594
|
+
console.log(`[QUEUE] Processing ${type} for ${packageName || spec}`)
|
|
595
|
+
|
|
596
|
+
if (type === 'package_refresh') {
|
|
597
|
+
await refreshPackageFromQueue(packageName, upstream, options, env, db, ctx)
|
|
598
|
+
} else if (type === 'version_refresh') {
|
|
599
|
+
await refreshVersionFromQueue(spec, upstream, options, env, db, ctx)
|
|
600
|
+
} else {
|
|
601
|
+
console.error(`[QUEUE] Unknown message type: ${type}`)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Acknowledge successful processing
|
|
605
|
+
message.ack()
|
|
606
|
+
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.error(`[QUEUE ERROR] Failed to process message: ${(error as Error).message}`)
|
|
609
|
+
// Don't ack failed messages so they can be retried
|
|
610
|
+
message.retry()
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Refresh package data from upstream in response to queue message
|
|
617
|
+
*/
|
|
618
|
+
async function refreshPackageFromQueue(packageName: string, upstream: string, options: any, env: Environment, db: any, ctx: any) {
|
|
619
|
+
try {
|
|
620
|
+
console.log(`[QUEUE] Refreshing package data for: ${packageName} from ${upstream}`)
|
|
621
|
+
|
|
622
|
+
// Build upstream URL
|
|
623
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
624
|
+
if (!upstreamConfig) {
|
|
625
|
+
throw new Error(`Unknown upstream: ${upstream}`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Fetch fresh data from upstream
|
|
629
|
+
const upstreamUrl = buildUpstreamUrl(upstreamConfig, packageName)
|
|
630
|
+
|
|
631
|
+
const response = await fetch(upstreamUrl, {
|
|
632
|
+
headers: {
|
|
633
|
+
'Accept': 'application/json',
|
|
634
|
+
'User-Agent': 'vlt-serverless-registry'
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
throw new Error(`Upstream returned ${response.status}`)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const data = await response.json()
|
|
643
|
+
|
|
644
|
+
// Store updated data
|
|
645
|
+
await db.upsertCachedPackage(
|
|
646
|
+
packageName,
|
|
647
|
+
data['dist-tags'] || {},
|
|
648
|
+
upstream,
|
|
649
|
+
data.time?.modified || new Date().toISOString()
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
console.log(`[QUEUE] Successfully refreshed package: ${packageName}`)
|
|
653
|
+
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error(`[QUEUE ERROR] Failed to refresh package ${packageName}: ${(error as Error).message}`)
|
|
656
|
+
throw error
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Refresh version data from upstream in response to queue message
|
|
662
|
+
*/
|
|
663
|
+
async function refreshVersionFromQueue(spec: string, upstream: string, options: any, env: Environment, db: any, ctx: any) {
|
|
664
|
+
try {
|
|
665
|
+
console.log(`[QUEUE] Refreshing version data for: ${spec} from ${upstream}`)
|
|
666
|
+
|
|
667
|
+
// Parse spec to get package name and version
|
|
668
|
+
const [packageName, version] = spec.split('@')
|
|
669
|
+
if (!packageName || !version) {
|
|
670
|
+
throw new Error(`Invalid spec format: ${spec}`)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Build upstream URL
|
|
674
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
675
|
+
if (!upstreamConfig) {
|
|
676
|
+
throw new Error(`Unknown upstream: ${upstream}`)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Fetch fresh data from upstream
|
|
680
|
+
const upstreamUrl = buildUpstreamUrl(upstreamConfig, packageName, version)
|
|
681
|
+
|
|
682
|
+
const response = await fetch(upstreamUrl, {
|
|
683
|
+
headers: {
|
|
684
|
+
'Accept': 'application/json',
|
|
685
|
+
'User-Agent': 'vlt-serverless-registry'
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
throw new Error(`Upstream returned ${response.status}`)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const manifest = await response.json()
|
|
694
|
+
|
|
695
|
+
// Store updated manifest
|
|
696
|
+
await db.upsertCachedVersion(
|
|
697
|
+
spec,
|
|
698
|
+
manifest,
|
|
699
|
+
upstream,
|
|
700
|
+
manifest.time || new Date().toISOString()
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
console.log(`[QUEUE] Successfully refreshed version: ${spec}`)
|
|
704
|
+
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error(`[QUEUE ERROR] Failed to refresh version ${spec}: ${(error as Error).message}`)
|
|
707
|
+
throw error
|
|
708
|
+
}
|
|
709
|
+
}
|