@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.
Files changed (83) hide show
  1. package/.editorconfig +13 -0
  2. package/.prettierrc +7 -0
  3. package/CONTRIBUTING.md +228 -0
  4. package/LICENSE.md +110 -0
  5. package/README.md +373 -0
  6. package/bin/vsr.ts +29 -0
  7. package/config.ts +124 -0
  8. package/debug-npm.js +19 -0
  9. package/drizzle.config.js +33 -0
  10. package/package.json +80 -0
  11. package/pnpm-workspace.yaml +5 -0
  12. package/src/api.ts +2246 -0
  13. package/src/assets/public/images/bg.png +0 -0
  14. package/src/assets/public/images/clients/logo-bun.png +0 -0
  15. package/src/assets/public/images/clients/logo-deno.png +0 -0
  16. package/src/assets/public/images/clients/logo-npm.png +0 -0
  17. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  18. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  19. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  20. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  21. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  22. package/src/assets/public/images/favicon/favicon.ico +0 -0
  23. package/src/assets/public/images/favicon/favicon.svg +3 -0
  24. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  25. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  26. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  27. package/src/assets/public/styles/styles.css +219 -0
  28. package/src/db/client.ts +544 -0
  29. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  30. package/src/db/migrations/0000_initial.sql +29 -0
  31. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  32. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  33. package/src/db/migrations/drop.sql +3 -0
  34. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  35. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  36. package/src/db/migrations/meta/_journal.json +20 -0
  37. package/src/db/schema.ts +41 -0
  38. package/src/index.ts +709 -0
  39. package/src/routes/access.ts +263 -0
  40. package/src/routes/auth.ts +93 -0
  41. package/src/routes/index.ts +135 -0
  42. package/src/routes/packages.ts +924 -0
  43. package/src/routes/search.ts +50 -0
  44. package/src/routes/static.ts +53 -0
  45. package/src/routes/tokens.ts +102 -0
  46. package/src/routes/users.ts +14 -0
  47. package/src/utils/auth.ts +145 -0
  48. package/src/utils/cache.ts +466 -0
  49. package/src/utils/database.ts +44 -0
  50. package/src/utils/packages.ts +337 -0
  51. package/src/utils/response.ts +100 -0
  52. package/src/utils/routes.ts +47 -0
  53. package/src/utils/spa.ts +14 -0
  54. package/src/utils/tracing.ts +63 -0
  55. package/src/utils/upstream.ts +131 -0
  56. package/test/README.md +91 -0
  57. package/test/access.test.js +760 -0
  58. package/test/cloudflare-waituntil.test.js +141 -0
  59. package/test/db.test.js +447 -0
  60. package/test/dist-tag.test.js +415 -0
  61. package/test/e2e.test.js +904 -0
  62. package/test/hono-context.test.js +250 -0
  63. package/test/integrity-validation.test.js +183 -0
  64. package/test/json-response.test.js +76 -0
  65. package/test/manifest-slimming.test.js +449 -0
  66. package/test/packument-consistency.test.js +351 -0
  67. package/test/packument-version-range.test.js +144 -0
  68. package/test/performance.test.js +162 -0
  69. package/test/route-with-waituntil.test.js +298 -0
  70. package/test/run-tests.js +151 -0
  71. package/test/setup-cache-tests.js +190 -0
  72. package/test/setup.js +64 -0
  73. package/test/stale-while-revalidate.test.js +273 -0
  74. package/test/static-assets.test.js +85 -0
  75. package/test/upstream-routing.test.js +86 -0
  76. package/test/utils/test-helpers.js +84 -0
  77. package/test/waituntil-correct.test.js +208 -0
  78. package/test/waituntil-demo.test.js +138 -0
  79. package/test/waituntil-readme.md +113 -0
  80. package/tsconfig.json +37 -0
  81. package/types.ts +446 -0
  82. package/vitest.config.js +95 -0
  83. 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
+ }