@vltpkg/vsr 0.0.0-27 → 0.0.0-29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +163 -0
- package/LICENSE +114 -10
- package/config.ts +221 -0
- package/dist/README.md +1 -1
- package/dist/bin/vsr.js +9 -6
- package/dist/index.js +4 -6
- package/dist/index.js.map +2 -2
- package/drizzle.config.js +40 -0
- package/info/COMPARISONS.md +37 -0
- package/info/CONFIGURATION.md +143 -0
- package/info/CONTRIBUTING.md +32 -0
- package/info/DATABASE_SETUP.md +108 -0
- package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
- package/info/PROJECT_STRUCTURE.md +291 -0
- package/info/ROADMAP.md +27 -0
- package/info/SUPPORT.md +39 -0
- package/info/TESTING.md +301 -0
- package/info/USER_SUPPORT.md +31 -0
- package/package.json +50 -6
- package/scripts/build-assets.js +31 -0
- package/scripts/build-bin.js +63 -0
- package/src/assets/public/images/bg.png +0 -0
- package/src/assets/public/images/clients/logo-bun.png +0 -0
- package/src/assets/public/images/clients/logo-deno.png +0 -0
- package/src/assets/public/images/clients/logo-npm.png +0 -0
- package/src/assets/public/images/clients/logo-pnpm.png +0 -0
- package/src/assets/public/images/clients/logo-vlt.png +0 -0
- package/src/assets/public/images/clients/logo-yarn.png +0 -0
- package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
- package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
- package/src/assets/public/images/favicon/favicon.ico +0 -0
- package/src/assets/public/images/favicon/favicon.svg +3 -0
- package/src/assets/public/images/favicon/site.webmanifest +21 -0
- package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
- package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
- package/src/assets/public/styles/styles.css +231 -0
- package/src/bin/demo/package.json +6 -0
- package/src/bin/demo/vlt.json +1 -0
- package/src/bin/vsr.ts +496 -0
- package/src/db/client.ts +590 -0
- package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
- package/src/db/migrations/0000_initial.sql +29 -0
- package/src/db/migrations/0001_uuid_validation.sql +35 -0
- package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
- package/src/db/migrations/drop.sql +3 -0
- package/src/db/migrations/meta/0000_snapshot.json +104 -0
- package/src/db/migrations/meta/0001_snapshot.json +155 -0
- package/src/db/migrations/meta/_journal.json +20 -0
- package/src/db/schema.ts +43 -0
- package/src/index.ts +434 -0
- package/src/middleware/config.ts +79 -0
- package/src/middleware/telemetry.ts +43 -0
- package/src/queue/index.ts +97 -0
- package/src/routes/access.ts +852 -0
- package/src/routes/docs.ts +63 -0
- package/src/routes/misc.ts +469 -0
- package/src/routes/packages.ts +2823 -0
- package/src/routes/ping.ts +39 -0
- package/src/routes/search.ts +131 -0
- package/src/routes/static.ts +74 -0
- package/src/routes/tokens.ts +259 -0
- package/src/routes/users.ts +68 -0
- package/src/utils/auth.ts +202 -0
- package/src/utils/cache.ts +587 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/database.ts +69 -0
- package/src/utils/docs.ts +146 -0
- package/src/utils/packages.ts +453 -0
- package/src/utils/response.ts +125 -0
- package/src/utils/routes.ts +64 -0
- package/src/utils/spa.ts +52 -0
- package/src/utils/tracing.ts +52 -0
- package/src/utils/upstream.ts +172 -0
- package/tsconfig.json +16 -0
- package/tsconfig.worker.json +3 -0
- package/typedoc.mjs +2 -0
- package/types.ts +598 -0
- package/vitest.config.ts +25 -0
- package/vlt.json.example +56 -0
- package/wrangler.json +65 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
import { OPEN_API_CONFIG } from '../config.ts'
|
|
3
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
4
|
+
import { requestId } from 'hono/request-id'
|
|
5
|
+
import { bearerAuth } from 'hono/bearer-auth'
|
|
6
|
+
import { except } from 'hono/combine'
|
|
7
|
+
import { logger } from 'hono/logger'
|
|
8
|
+
import { secureHeaders } from 'hono/secure-headers'
|
|
9
|
+
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
10
|
+
import { telemetryMiddleware } from './middleware/telemetry.ts'
|
|
11
|
+
import { configMiddleware } from './middleware/config.ts'
|
|
12
|
+
import { getApp } from './utils/spa.ts'
|
|
13
|
+
import { verifyToken } from './utils/auth.ts'
|
|
14
|
+
import { mountDatabase } from './utils/database.ts'
|
|
15
|
+
import { jsonResponseHandler } from './utils/response.ts'
|
|
16
|
+
import { requiresToken } from './utils/routes.ts'
|
|
17
|
+
import {
|
|
18
|
+
getUsername,
|
|
19
|
+
getUserProfile,
|
|
20
|
+
userProfileRoute,
|
|
21
|
+
whoamiRoute,
|
|
22
|
+
} from './routes/users.ts'
|
|
23
|
+
import { pingRoute, handlePing } from './routes/ping.ts'
|
|
24
|
+
import { getDocs } from './routes/docs.ts'
|
|
25
|
+
import {
|
|
26
|
+
getToken,
|
|
27
|
+
putToken,
|
|
28
|
+
postToken,
|
|
29
|
+
deleteToken,
|
|
30
|
+
getTokensRoute,
|
|
31
|
+
createTokenRoute,
|
|
32
|
+
updateTokenRoute,
|
|
33
|
+
deleteTokenRoute,
|
|
34
|
+
} from './routes/tokens.ts'
|
|
35
|
+
import {
|
|
36
|
+
getPackageDistTags,
|
|
37
|
+
putPackageDistTag,
|
|
38
|
+
deletePackageDistTag,
|
|
39
|
+
handleRootPackageRoute,
|
|
40
|
+
handlePackagePublish,
|
|
41
|
+
handlePackageVersion,
|
|
42
|
+
handlePackageTarball,
|
|
43
|
+
handleUpstreamPackage,
|
|
44
|
+
handleUpstreamScopedTarball,
|
|
45
|
+
handleUpstreamScopedVersion,
|
|
46
|
+
handleUpstreamEncodedScoped,
|
|
47
|
+
handleUpstreamUnified,
|
|
48
|
+
handleUpstreamTarball,
|
|
49
|
+
// Local package route definitions
|
|
50
|
+
getPackageRoute,
|
|
51
|
+
getPackageVersionRoute,
|
|
52
|
+
getPackageTarballRoute,
|
|
53
|
+
publishPackageRoute,
|
|
54
|
+
getPackageDistTagsRoute,
|
|
55
|
+
putPackageDistTagRoute,
|
|
56
|
+
deletePackageDistTagRoute,
|
|
57
|
+
// Upstream package route definitions
|
|
58
|
+
getUpstreamPackageRoute,
|
|
59
|
+
getUpstreamScopedPackageVersionRoute,
|
|
60
|
+
getUpstreamPackageTarballRoute,
|
|
61
|
+
getUpstreamScopedPackageTarballRoute,
|
|
62
|
+
getUpstreamEncodedScopedPackageRoute,
|
|
63
|
+
getUpstreamUnifiedRoute,
|
|
64
|
+
} from './routes/packages.ts'
|
|
65
|
+
import {
|
|
66
|
+
listPackagesAccess,
|
|
67
|
+
getPackageAccessStatus,
|
|
68
|
+
setPackageAccessStatus,
|
|
69
|
+
grantPackageAccess,
|
|
70
|
+
revokePackageAccess,
|
|
71
|
+
// OpenAPI route definitions
|
|
72
|
+
getPackageAccessRoute,
|
|
73
|
+
setPackageAccessRoute,
|
|
74
|
+
getScopedPackageAccessRoute,
|
|
75
|
+
setScopedPackageAccessRoute,
|
|
76
|
+
listPackagesAccessRoute,
|
|
77
|
+
grantPackageAccessRoute,
|
|
78
|
+
revokePackageAccessRoute,
|
|
79
|
+
grantScopedPackageAccessRoute,
|
|
80
|
+
revokeScopedPackageAccessRoute,
|
|
81
|
+
} from './routes/access.ts'
|
|
82
|
+
import {
|
|
83
|
+
searchPackages,
|
|
84
|
+
searchPackagesRoute,
|
|
85
|
+
} from './routes/search.ts'
|
|
86
|
+
import {
|
|
87
|
+
auditRoute,
|
|
88
|
+
auditQuickRoute,
|
|
89
|
+
advisoriesBulkRoute,
|
|
90
|
+
dashboardDataRoute,
|
|
91
|
+
appDataRoute,
|
|
92
|
+
handleDashboardData,
|
|
93
|
+
handleAppData,
|
|
94
|
+
handleSecurityAudit,
|
|
95
|
+
// Compatibility redirect routes
|
|
96
|
+
searchRedirectRoute,
|
|
97
|
+
userRedirectRoute,
|
|
98
|
+
tokensRedirectRoute,
|
|
99
|
+
createTokenRedirectRoute,
|
|
100
|
+
updateTokenRedirectRoute,
|
|
101
|
+
deleteTokenRedirectRoute,
|
|
102
|
+
} from './routes/misc.ts'
|
|
103
|
+
|
|
104
|
+
import { sessionMonitor } from './utils/tracing.ts'
|
|
105
|
+
import type { createDatabaseOperations } from './db/client.ts'
|
|
106
|
+
import {
|
|
107
|
+
handleStaticAssets,
|
|
108
|
+
handleFavicon,
|
|
109
|
+
handleRobots,
|
|
110
|
+
handleManifest,
|
|
111
|
+
} from './routes/static.ts'
|
|
112
|
+
import type { Environment } from '../types.ts'
|
|
113
|
+
import type { Context } from 'hono'
|
|
114
|
+
|
|
115
|
+
// Import queue handler from dedicated module
|
|
116
|
+
import { queue } from './queue/index.ts'
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------
|
|
119
|
+
// App Initialization
|
|
120
|
+
// ("strict mode" is turned off to ensure that routes like
|
|
121
|
+
// `/hello` & `/hello/` are handled the same way - ref.
|
|
122
|
+
// https://hono.dev/docs/api/hono#strict-mode)
|
|
123
|
+
// ---------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
const app = new OpenAPIHono<{
|
|
126
|
+
Bindings: Environment
|
|
127
|
+
Variables: {
|
|
128
|
+
db: ReturnType<typeof createDatabaseOperations>
|
|
129
|
+
}
|
|
130
|
+
}>({ strict: false })
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------
|
|
133
|
+
// Middleware
|
|
134
|
+
// ---------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
app.use(trimTrailingSlash())
|
|
137
|
+
app.use('*', requestId())
|
|
138
|
+
app.use('*', logger())
|
|
139
|
+
app.use('*', configMiddleware)
|
|
140
|
+
app.use('*', telemetryMiddleware)
|
|
141
|
+
app.use('*', secureHeaders())
|
|
142
|
+
app.use('*', jsonResponseHandler())
|
|
143
|
+
app.use('*', except(['/-/ping', '/-/docs'], mountDatabase as any))
|
|
144
|
+
app.use('*', sessionMonitor)
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------
|
|
147
|
+
// Home
|
|
148
|
+
// (Single Page Application or API Docs)
|
|
149
|
+
// ---------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
app.get('/', async c => {
|
|
152
|
+
// If daemon is disabled and API docs are enabled, redirect to docs
|
|
153
|
+
// This provides a better user experience when the daemon isn't available
|
|
154
|
+
if (!c.env.DAEMON_ENABLED && c.env.API_DOCS_ENABLED) {
|
|
155
|
+
return c.redirect('/-/docs', 302)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If daemon is enabled, show the GUI (projects will be available)
|
|
159
|
+
if (c.env.DAEMON_ENABLED) {
|
|
160
|
+
return c.html(await getApp(c.env.ASSETS))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fallback: show docs if available, otherwise show GUI
|
|
164
|
+
if (c.env.API_DOCS_ENABLED) {
|
|
165
|
+
return c.redirect('/-/docs', 302)
|
|
166
|
+
} else {
|
|
167
|
+
return c.html(await getApp(c.env.ASSETS))
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------
|
|
172
|
+
// Documentation
|
|
173
|
+
// ---------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
// Mount API documentation routes
|
|
176
|
+
app.doc('/-/api', OPEN_API_CONFIG)
|
|
177
|
+
app.get('/-/docs', getDocs)
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------
|
|
180
|
+
// Health Check
|
|
181
|
+
// ---------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
// Pattern: /-/ping
|
|
184
|
+
app.openapi(pingRoute, handlePing as any)
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------
|
|
187
|
+
// Authorization Verification Middleware
|
|
188
|
+
// ---------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
// Custom auth middleware that checks if token is required
|
|
191
|
+
app.use('*', async (c, next) => {
|
|
192
|
+
if (requiresToken(c)) {
|
|
193
|
+
// Token is required, apply bearer auth
|
|
194
|
+
return bearerAuth({ verifyToken: verifyToken as any })(c, next)
|
|
195
|
+
} else {
|
|
196
|
+
// Token not required, skip auth
|
|
197
|
+
await next()
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------
|
|
202
|
+
// User Routes
|
|
203
|
+
// ---------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
// Pattern: /-/whoami
|
|
206
|
+
app.openapi(whoamiRoute, getUsername)
|
|
207
|
+
// Pattern: /-/user
|
|
208
|
+
app.openapi(userProfileRoute, getUserProfile)
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------
|
|
211
|
+
// Daemon Project Routes - only local use
|
|
212
|
+
// ---------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
// Pattern: /dashboard.json
|
|
215
|
+
app.openapi(dashboardDataRoute, handleDashboardData as any)
|
|
216
|
+
|
|
217
|
+
// Pattern: /app-data.json
|
|
218
|
+
app.openapi(appDataRoute, handleAppData as any)
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------
|
|
221
|
+
// Token Routes
|
|
222
|
+
// ---------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
// Pattern: /-/tokens
|
|
225
|
+
app.openapi(getTokensRoute, getToken as any)
|
|
226
|
+
// Pattern: /-/tokens
|
|
227
|
+
app.openapi(createTokenRoute, postToken as any)
|
|
228
|
+
// Pattern: /-/tokens
|
|
229
|
+
app.openapi(updateTokenRoute, putToken as any)
|
|
230
|
+
// Pattern: /-/tokens/{token}
|
|
231
|
+
app.openapi(deleteTokenRoute, deleteToken as any)
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------
|
|
234
|
+
// Dist-tag Routes
|
|
235
|
+
// ---------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
// Pattern: /-/package/{pkg}/dist-tags
|
|
238
|
+
app.openapi(getPackageDistTagsRoute, getPackageDistTags as any)
|
|
239
|
+
// Pattern: /-/package/{pkg}/dist-tags/{tag}
|
|
240
|
+
app.openapi(putPackageDistTagRoute, putPackageDistTag as any)
|
|
241
|
+
// Pattern: /-/package/{pkg}/dist-tags/{tag}
|
|
242
|
+
app.openapi(deletePackageDistTagRoute, deletePackageDistTag as any)
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------
|
|
245
|
+
// Access Control Routes
|
|
246
|
+
// ---------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
// Pattern: /-/package/{pkg}/access (unscoped packages)
|
|
249
|
+
app.openapi(getPackageAccessRoute, getPackageAccessStatus as any)
|
|
250
|
+
app.openapi(setPackageAccessRoute, setPackageAccessStatus as any)
|
|
251
|
+
|
|
252
|
+
// Pattern: /-/package/{scope}%2f{pkg}/access (scoped packages)
|
|
253
|
+
app.openapi(
|
|
254
|
+
getScopedPackageAccessRoute,
|
|
255
|
+
getPackageAccessStatus as any,
|
|
256
|
+
)
|
|
257
|
+
app.openapi(
|
|
258
|
+
setScopedPackageAccessRoute,
|
|
259
|
+
setPackageAccessStatus as any,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
// Pattern: /-/package/list
|
|
263
|
+
app.openapi(listPackagesAccessRoute, listPackagesAccess as any)
|
|
264
|
+
|
|
265
|
+
// Pattern: /-/package/{pkg}/collaborators/{username} (unscoped packages)
|
|
266
|
+
app.openapi(grantPackageAccessRoute, grantPackageAccess as any)
|
|
267
|
+
app.openapi(revokePackageAccessRoute, revokePackageAccess as any)
|
|
268
|
+
|
|
269
|
+
// Pattern: /-/package/{scope}%2f{pkg}/collaborators/{username} (scoped packages)
|
|
270
|
+
app.openapi(grantScopedPackageAccessRoute, grantPackageAccess as any)
|
|
271
|
+
app.openapi(
|
|
272
|
+
revokeScopedPackageAccessRoute,
|
|
273
|
+
revokePackageAccess as any,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------
|
|
277
|
+
// Search Packages
|
|
278
|
+
// ---------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
// Pattern: /-/search
|
|
281
|
+
app.openapi(searchPackagesRoute, searchPackages as any)
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------
|
|
284
|
+
// Handle Audit Requests
|
|
285
|
+
// ---------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
// Pattern: /-/npm/audit (security audit - not implemented)
|
|
288
|
+
app.openapi(auditRoute, handleSecurityAudit as any)
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------
|
|
291
|
+
// NPM Compatibility Routes (Legacy API Redirects)
|
|
292
|
+
// (maximizes backwards compatibility with npm clients)
|
|
293
|
+
// ---------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
// Pattern: /-/v1/search → /-/search
|
|
296
|
+
app.openapi(searchRedirectRoute, (c: Context) =>
|
|
297
|
+
c.redirect('/-/search', 308),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
// Pattern: /-/npm/v1/user → /-/user
|
|
301
|
+
app.openapi(userRedirectRoute, (c: Context) =>
|
|
302
|
+
c.redirect('/-/user', 308),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Pattern: /-/npm/v1/tokens → /-/tokens (GET)
|
|
306
|
+
app.openapi(tokensRedirectRoute, (c: Context) =>
|
|
307
|
+
c.redirect('/-/tokens', 308),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Pattern: /-/npm/v1/tokens → /-/tokens (POST)
|
|
311
|
+
app.openapi(createTokenRedirectRoute, (c: Context) =>
|
|
312
|
+
c.redirect('/-/tokens', 308),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// Pattern: /-/npm/v1/tokens → /-/tokens (PUT)
|
|
316
|
+
app.openapi(updateTokenRedirectRoute, (c: Context) =>
|
|
317
|
+
c.redirect('/-/tokens', 308),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
// Pattern: /-/npm/v1/tokens/token/{token} → /-/tokens/{token} (DELETE)
|
|
321
|
+
app.openapi(deleteTokenRedirectRoute, (c: Context) => {
|
|
322
|
+
return c.redirect(`/-/tokens/${c.req.param('token')}`, 308)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Pattern: /-/npm/v1/security/audits/quick → /-/npm/audit
|
|
326
|
+
app.openapi(auditQuickRoute, (c: Context) => {
|
|
327
|
+
return c.redirect('/-/npm/audit', 308)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Pattern: /-/npm/v1/security/advisories/bulk → /-/npm/audit
|
|
331
|
+
app.openapi(advisoriesBulkRoute, (c: Context) => {
|
|
332
|
+
return c.redirect('/-/npm/audit', 308)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------
|
|
336
|
+
// Upstream Utility Routes
|
|
337
|
+
// (Registry utility endpoints for upstream registries)
|
|
338
|
+
// (MUST come before upstream package routes to avoid conflicts)
|
|
339
|
+
// ---------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
// Pattern: /{upstream}/-/ping (upstream registry ping)
|
|
342
|
+
app.get('/:upstream/-/ping', handlePing)
|
|
343
|
+
|
|
344
|
+
// Pattern: /{upstream}/-/docs (upstream registry docs)
|
|
345
|
+
app.get('/:upstream/-/docs', getDocs)
|
|
346
|
+
|
|
347
|
+
// Pattern: /{upstream}/-/whoami (upstream registry whoami)
|
|
348
|
+
app.get('/:upstream/-/whoami', getUsername)
|
|
349
|
+
|
|
350
|
+
// Pattern: /{upstream}/-/user (upstream registry user profile)
|
|
351
|
+
app.get('/:upstream/-/user', getUserProfile)
|
|
352
|
+
|
|
353
|
+
// Pattern: /{upstream}/-/tokens (upstream registry token management)
|
|
354
|
+
app.get('/:upstream/-/tokens', getToken)
|
|
355
|
+
app.post('/:upstream/-/tokens', postToken)
|
|
356
|
+
app.put('/:upstream/-/tokens', putToken)
|
|
357
|
+
app.delete('/:upstream/-/tokens/:token', deleteToken)
|
|
358
|
+
|
|
359
|
+
// Pattern: /{upstream}/-/search (upstream registry search)
|
|
360
|
+
app.get('/:upstream/-/search', searchPackages)
|
|
361
|
+
|
|
362
|
+
// Pattern: /{upstream}/-/npm/audit (upstream registry audit)
|
|
363
|
+
app.post('/:upstream/-/npm/audit', handleSecurityAudit)
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------
|
|
366
|
+
// Upstream Package Routes
|
|
367
|
+
// (must come before catch-all package routes)
|
|
368
|
+
// ---------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
// Pattern: /{upstream}/@{scope}/{pkg}/-/{tarball} (scoped package tarball)
|
|
371
|
+
app.openapi(
|
|
372
|
+
getUpstreamScopedPackageTarballRoute,
|
|
373
|
+
handleUpstreamScopedTarball as any,
|
|
374
|
+
)
|
|
375
|
+
// Pattern: /{upstream}/@{scope}/{pkg}/{version} (scoped package version)
|
|
376
|
+
app.openapi(
|
|
377
|
+
getUpstreamScopedPackageVersionRoute,
|
|
378
|
+
handleUpstreamScopedVersion as any,
|
|
379
|
+
)
|
|
380
|
+
// Pattern: /{upstream}/{pkg}/-/{tarball} (unscoped package tarball)
|
|
381
|
+
app.openapi(
|
|
382
|
+
getUpstreamPackageTarballRoute,
|
|
383
|
+
handleUpstreamTarball as any,
|
|
384
|
+
)
|
|
385
|
+
// Pattern: /{upstream}/@{scope}%2f{pkg} (URL-encoded scoped package)
|
|
386
|
+
app.openapi(
|
|
387
|
+
getUpstreamEncodedScopedPackageRoute,
|
|
388
|
+
handleUpstreamEncodedScoped as any,
|
|
389
|
+
)
|
|
390
|
+
// Pattern: /{upstream}/{param2}/{param3} (unified handler for ambiguous 3-segment paths)
|
|
391
|
+
app.openapi(getUpstreamUnifiedRoute, handleUpstreamUnified as any)
|
|
392
|
+
// Pattern: /{upstream}/{pkg} (unscoped package manifest)
|
|
393
|
+
app.openapi(getUpstreamPackageRoute, handleUpstreamPackage as any)
|
|
394
|
+
|
|
395
|
+
// Pattern: /-/npm/v1/security/advisories/bulk → /-/npm/audit
|
|
396
|
+
app.openapi(advisoriesBulkRoute, async (c: Context) => {
|
|
397
|
+
return c.redirect('/-/npm/audit', 308)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// ---------------------------------------------------------
|
|
401
|
+
// Local Package Routes
|
|
402
|
+
// (catch-all patterns, must come after upstream routes)
|
|
403
|
+
// ---------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
// Pattern: /{pkg} (package publishing via PUT)
|
|
406
|
+
app.openapi(publishPackageRoute, handlePackagePublish as any)
|
|
407
|
+
// Pattern: /{pkg}/-/{tarball} (package tarball download)
|
|
408
|
+
app.openapi(getPackageTarballRoute, handlePackageTarball as any)
|
|
409
|
+
// Pattern: /{pkg}/{version} (specific package version)
|
|
410
|
+
app.openapi(getPackageVersionRoute, handlePackageVersion as any)
|
|
411
|
+
// Pattern: /{pkg} (package manifest/packument)
|
|
412
|
+
app.openapi(getPackageRoute, handleRootPackageRoute as any)
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------
|
|
415
|
+
// Handle Static Assets
|
|
416
|
+
// ---------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
// Pattern: /public/* (static assets from public directory)
|
|
419
|
+
app.get('/public/*', handleStaticAssets)
|
|
420
|
+
// Pattern: /favicon.ico (browser favicon)
|
|
421
|
+
app.get('/favicon.ico', handleFavicon)
|
|
422
|
+
// Pattern: /robots.txt (web crawler instructions)
|
|
423
|
+
app.get('/robots.txt', handleRobots)
|
|
424
|
+
// Pattern: /manifest.json (PWA web app manifest)
|
|
425
|
+
app.get('/manifest.json', handleManifest)
|
|
426
|
+
// Pattern: /* (catch-all for any other static assets)
|
|
427
|
+
app.get('/*', handleStaticAssets)
|
|
428
|
+
|
|
429
|
+
export { app }
|
|
430
|
+
|
|
431
|
+
export default {
|
|
432
|
+
fetch: app.fetch,
|
|
433
|
+
queue,
|
|
434
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
import {
|
|
3
|
+
API_DOCS_ENABLED,
|
|
4
|
+
DAEMON_ENABLED,
|
|
5
|
+
DAEMON_PORT,
|
|
6
|
+
DAEMON_URL,
|
|
7
|
+
DEBUG_ENABLED,
|
|
8
|
+
TELEMETRY_ENABLED,
|
|
9
|
+
SENTRY_CONFIG,
|
|
10
|
+
PORT,
|
|
11
|
+
VERSION,
|
|
12
|
+
URL,
|
|
13
|
+
} from '../../config.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runtime configuration resolver - can be used outside of routes
|
|
17
|
+
* Checks environment variables and falls back to defaults
|
|
18
|
+
*/
|
|
19
|
+
export function resolveConfig(env?: any) {
|
|
20
|
+
const getBooleanFromEnv = (
|
|
21
|
+
key: string,
|
|
22
|
+
defaultValue: boolean,
|
|
23
|
+
): boolean => {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
25
|
+
const value = env?.[key]
|
|
26
|
+
return typeof value === 'string' ?
|
|
27
|
+
value.toLowerCase() === 'true'
|
|
28
|
+
: defaultValue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
// Daemon configuration
|
|
33
|
+
DAEMON_ENABLED: getBooleanFromEnv('ARG_DAEMON', DAEMON_ENABLED),
|
|
34
|
+
|
|
35
|
+
// Telemetry configuration
|
|
36
|
+
TELEMETRY_ENABLED: getBooleanFromEnv(
|
|
37
|
+
'ARG_TELEMETRY',
|
|
38
|
+
TELEMETRY_ENABLED,
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
// Debug configuration
|
|
42
|
+
DEBUG_ENABLED: getBooleanFromEnv('ARG_DEBUG', DEBUG_ENABLED),
|
|
43
|
+
|
|
44
|
+
// Static config values (pass through)
|
|
45
|
+
API_DOCS_ENABLED,
|
|
46
|
+
DAEMON_PORT,
|
|
47
|
+
DAEMON_URL,
|
|
48
|
+
SENTRY_CONFIG,
|
|
49
|
+
PORT,
|
|
50
|
+
VERSION,
|
|
51
|
+
URL,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configuration middleware that enriches the context with computed config values
|
|
57
|
+
* Merges compile-time defaults with runtime environment variables
|
|
58
|
+
*/
|
|
59
|
+
export async function configMiddleware(
|
|
60
|
+
c: Context,
|
|
61
|
+
next: () => Promise<void>,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
// Use the resolver to get computed config
|
|
64
|
+
const runtimeConfig = resolveConfig(c.env)
|
|
65
|
+
|
|
66
|
+
// Initialize global config if not already done
|
|
67
|
+
const { initializeGlobalConfig } = await import(
|
|
68
|
+
'../utils/config.ts'
|
|
69
|
+
)
|
|
70
|
+
initializeGlobalConfig(c.env)
|
|
71
|
+
|
|
72
|
+
// Enrich the context environment with computed values
|
|
73
|
+
// Ensure c.env exists (it might be undefined in test environments)
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
75
|
+
c.env = c.env || {}
|
|
76
|
+
Object.assign(c.env, runtimeConfig)
|
|
77
|
+
|
|
78
|
+
await next()
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sentry } from '@hono/sentry'
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
import type { HonoContext } from '../../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Telemetry middleware that conditionally applies Sentry based on configuration
|
|
7
|
+
* Relies on configMiddleware to have already enriched c.env with TELEMETRY_ENABLED
|
|
8
|
+
*/
|
|
9
|
+
export async function telemetryMiddleware(
|
|
10
|
+
c: HonoContext,
|
|
11
|
+
next: () => Promise<void>,
|
|
12
|
+
) {
|
|
13
|
+
// Check if telemetry is enabled via the enriched config
|
|
14
|
+
if (c.env.TELEMETRY_ENABLED) {
|
|
15
|
+
// Apply Sentry middleware dynamically
|
|
16
|
+
const sentryMiddleware = sentry({
|
|
17
|
+
dsn: c.env.SENTRY?.dsn ?? c.env.SENTRY_CONFIG?.dsn ?? '',
|
|
18
|
+
environment:
|
|
19
|
+
c.env.SENTRY?.environment ??
|
|
20
|
+
c.env.SENTRY_CONFIG?.environment ??
|
|
21
|
+
'development',
|
|
22
|
+
sendDefaultPii: c.env.SENTRY_CONFIG?.sendDefaultPii ?? true,
|
|
23
|
+
sampleRate: c.env.SENTRY_CONFIG?.sampleRate ?? 1.0,
|
|
24
|
+
tracesSampleRate: c.env.SENTRY_CONFIG?.tracesSampleRate ?? 0.1,
|
|
25
|
+
beforeSend(event, _hint) {
|
|
26
|
+
// Filter out expected errors to reduce noise
|
|
27
|
+
if (
|
|
28
|
+
event.exception?.values?.[0]?.value?.includes('404') ||
|
|
29
|
+
event.exception?.values?.[0]?.value?.includes('not found')
|
|
30
|
+
) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
return event
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
38
|
+
return sentryMiddleware(c as Context, next)
|
|
39
|
+
} else {
|
|
40
|
+
// Skip Sentry when telemetry is disabled
|
|
41
|
+
return next()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { QueueBatch, Environment } from '../../types.ts'
|
|
2
|
+
import { createDatabaseOperations } from '../db/client.ts'
|
|
3
|
+
import {
|
|
4
|
+
getUpstreamConfig,
|
|
5
|
+
buildUpstreamUrl,
|
|
6
|
+
} from '../utils/upstream.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Queue handler for background cache refresh jobs
|
|
10
|
+
*
|
|
11
|
+
* Processes queue messages to refresh package and version cache data
|
|
12
|
+
* from upstream registries. This runs in the background to keep
|
|
13
|
+
* cached data fresh without blocking user requests.
|
|
14
|
+
*/
|
|
15
|
+
export async function queue(batch: QueueBatch, env: Environment) {
|
|
16
|
+
if (!env.DB) {
|
|
17
|
+
throw new Error('Database not available')
|
|
18
|
+
}
|
|
19
|
+
const db = createDatabaseOperations(env.DB)
|
|
20
|
+
|
|
21
|
+
for (const message of batch.messages) {
|
|
22
|
+
try {
|
|
23
|
+
const {
|
|
24
|
+
type,
|
|
25
|
+
packageName,
|
|
26
|
+
spec,
|
|
27
|
+
upstream,
|
|
28
|
+
options: _options,
|
|
29
|
+
} = message.body
|
|
30
|
+
|
|
31
|
+
if (type === 'package_refresh' && packageName) {
|
|
32
|
+
// Handle package refresh - refetch from upstream and cache
|
|
33
|
+
const upstreamConfig = getUpstreamConfig(upstream)
|
|
34
|
+
if (upstreamConfig) {
|
|
35
|
+
const upstreamUrl = buildUpstreamUrl(
|
|
36
|
+
upstreamConfig,
|
|
37
|
+
packageName,
|
|
38
|
+
)
|
|
39
|
+
const response = await fetch(upstreamUrl, {
|
|
40
|
+
headers: { Accept: 'application/json' },
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
45
|
+
const upstreamData = (await response.json()) as Record<
|
|
46
|
+
string,
|
|
47
|
+
any
|
|
48
|
+
>
|
|
49
|
+
|
|
50
|
+
// Cache the package metadata
|
|
51
|
+
if (upstreamData['dist-tags']) {
|
|
52
|
+
await db.upsertCachedPackage(
|
|
53
|
+
packageName,
|
|
54
|
+
upstreamData['dist-tags'] as Record<string, string>,
|
|
55
|
+
upstream,
|
|
56
|
+
new Date().toISOString(),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Cache all versions
|
|
61
|
+
if (upstreamData.versions) {
|
|
62
|
+
const versionPromises = Object.entries(
|
|
63
|
+
upstreamData.versions as Record<string, any>,
|
|
64
|
+
).map(async ([version, manifest]) => {
|
|
65
|
+
try {
|
|
66
|
+
await db.upsertCachedVersion(
|
|
67
|
+
`${packageName}@${version}`,
|
|
68
|
+
manifest as Record<string, any>,
|
|
69
|
+
upstream,
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
71
|
+
(manifest.publishedAt as string) ||
|
|
72
|
+
new Date().toISOString(),
|
|
73
|
+
)
|
|
74
|
+
} catch (_error) {
|
|
75
|
+
// Silently fail individual versions
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
await Promise.allSettled(versionPromises)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else if (type === 'version_refresh' && spec) {
|
|
83
|
+
// Handle version refresh - similar logic for individual versions
|
|
84
|
+
// (This would be implemented if needed)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Acknowledge successful processing
|
|
88
|
+
message.ack()
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Retry failed messages
|
|
91
|
+
// TODO: Replace with proper logging system
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.error('Queue processing error:', error)
|
|
94
|
+
message.retry()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|