@vltpkg/vsr 0.0.0-27 → 0.0.0-28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +8 -6
  6. package/dist/index.js +3 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +49 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
@@ -0,0 +1,125 @@
1
+ import type { HonoContext, ApiError } from '../../types.ts'
2
+
3
+ /**
4
+ * JSON response handler middleware that formats JSON based on Accept headers
5
+ * @returns {Function} Hono middleware function
6
+ */
7
+ export function jsonResponseHandler() {
8
+ return async (c: any, next: any) => {
9
+ // Override the json method to handle formatting
10
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
11
+ const originalJson = c.json.bind(c)
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ c.json = (data: any, status?: number) => {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
16
+ const acceptHeader = c.req.header('accept') || ''
17
+
18
+ // If the client accepts the npm install format, return minimal JSON
19
+
20
+ if (
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
22
+ acceptHeader.includes('application/vnd.npm.install-v1+json')
23
+ ) {
24
+ // Use original json method for minimal output
25
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
26
+ return originalJson(data, status)
27
+ }
28
+
29
+ // For other requests, return pretty-printed JSON
30
+ // Preserve existing headers that were set via c.header()
31
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
32
+ const existingHeaders = new Headers(c.res?.headers || {})
33
+ existingHeaders.set(
34
+ 'Content-Type',
35
+ 'application/json; charset=UTF-8',
36
+ )
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
39
+ c.res = new Response(JSON.stringify(data, null, 2), {
40
+ status: status || 200,
41
+ headers: existingHeaders,
42
+ })
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
44
+ return c.res
45
+ }
46
+
47
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
48
+ await next()
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Creates a standardized JSON error response
54
+ * @param {HonoContext} c - The Hono context
55
+ * @param {string | ApiError} error - Error message or object
56
+ * @param {number} [status] - HTTP status code
57
+ * @returns {any} JSON error response
58
+ */
59
+ export function jsonError(
60
+ c: HonoContext,
61
+ error: string | ApiError,
62
+ status = 400,
63
+ ) {
64
+ const errorObj: ApiError =
65
+ typeof error === 'string' ? { error } : error
66
+
67
+ return c.json(errorObj, status as any)
68
+ }
69
+
70
+ /**
71
+ * Creates a standardized JSON success response
72
+ * @param {HonoContext} c - The Hono context
73
+ * @param {any} data - Response data
74
+ * @param {number} [status] - HTTP status code
75
+ * @returns {any} JSON success response
76
+ */
77
+ export function jsonSuccess(c: HonoContext, data: any, status = 200) {
78
+ return c.json(data, status as any)
79
+ }
80
+
81
+ /**
82
+ * Creates a 404 Not Found response
83
+ * @param {HonoContext} c - The Hono context
84
+ * @param {string} [message] - Optional custom message
85
+ * @returns {any} 404 JSON response
86
+ */
87
+ export function notFound(c: HonoContext, message = 'Not Found') {
88
+ return jsonError(c, { error: message }, 404)
89
+ }
90
+
91
+ /**
92
+ * Creates a 401 Unauthorized response
93
+ * @param {HonoContext} c - The Hono context
94
+ * @param {string} [message] - Optional custom message
95
+ * @returns {any} 401 JSON response
96
+ */
97
+ export function unauthorized(
98
+ c: HonoContext,
99
+ message = 'Unauthorized',
100
+ ) {
101
+ return jsonError(c, { error: message }, 401)
102
+ }
103
+
104
+ /**
105
+ * Creates a 403 Forbidden response
106
+ * @param {HonoContext} c - The Hono context
107
+ * @param {string} [message] - Optional custom message
108
+ * @returns {any} 403 JSON response
109
+ */
110
+ export function forbidden(c: HonoContext, message = 'Forbidden') {
111
+ return jsonError(c, { error: message }, 403)
112
+ }
113
+
114
+ /**
115
+ * Creates a 500 Internal Server Error response
116
+ * @param {HonoContext} c - The Hono context
117
+ * @param {string} [message] - Optional custom message
118
+ * @returns {any} 500 JSON response
119
+ */
120
+ export function internalServerError(
121
+ c: HonoContext,
122
+ message = 'Internal Server Error',
123
+ ) {
124
+ return jsonError(c, { error: message }, 500)
125
+ }
@@ -0,0 +1,64 @@
1
+ import type { HonoContext } from '../../types.ts'
2
+
3
+ // Public / Static Assets
4
+ export function requiresToken(c: HonoContext): boolean {
5
+ const { path } = c.req
6
+ const publicRoutes = [
7
+ '/images',
8
+ '/styles',
9
+ '/-/auth/*',
10
+ '/-/ping',
11
+ '/-/docs',
12
+ '/',
13
+ ]
14
+
15
+ // Check for upstream utility routes that are public (ping and docs only)
16
+ const upstreamPublicPattern = /^\/[^/]+\/-\/(ping|docs)$/
17
+ if (upstreamPublicPattern.test(path)) {
18
+ return false // These are public, no token required
19
+ }
20
+
21
+ // Check standard public routes
22
+ const isStandardPublicRoute = publicRoutes.some(route => {
23
+ if (route.endsWith('/*')) {
24
+ return path.startsWith(route.slice(0, -2))
25
+ }
26
+ return path === route || path.startsWith(route)
27
+ })
28
+
29
+ if (isStandardPublicRoute) {
30
+ return false // No token required
31
+ }
32
+
33
+ // Package routes should be public for downloads
34
+ // This includes hash-based routes, upstream routes, and legacy redirects
35
+ const isPackageRoute =
36
+ path.startsWith('/*/') || // Hash-based routes (literal asterisk)
37
+ /^\/[^-/][^/]*\/[^/]/.test(path) || // Upstream or package routes
38
+ /^\/[^-/][^/]*$/.test(path) || // Root package routes
39
+ /^\/@[^/]+\/[^/]+/.test(path) // Scoped packages
40
+
41
+ // Exclude PUT requests (publishing) from being public
42
+ const isPutRequest = c.req.method === 'PUT'
43
+ const isPublicPackageRoute = isPackageRoute && !isPutRequest
44
+
45
+ if (isPublicPackageRoute) {
46
+ return false // No token required
47
+ }
48
+
49
+ // All other routes require authentication
50
+ return true
51
+ }
52
+
53
+ // Catch-all for non-GET methods
54
+ export function catchAll(c: HonoContext) {
55
+ return c.json({ error: 'Method not allowed' }, 405)
56
+ }
57
+
58
+ export function notFound(c: HonoContext) {
59
+ return c.json({ error: 'Not found' }, 404)
60
+ }
61
+
62
+ export function isOK(c: HonoContext) {
63
+ return c.json({}, 200)
64
+ }
@@ -0,0 +1,52 @@
1
+ // This function now takes the ASSETS environment binding as a parameter
2
+ // to avoid the circular dependency issue
3
+ export const getApp = async (
4
+ assetsBinding?: any,
5
+ ): Promise<string> => {
6
+ if (assetsBinding) {
7
+ try {
8
+ // Use the ASSETS binding to fetch the index.html file
9
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
10
+ const response = (await assetsBinding.fetch(
11
+ new Request('http://localhost/public/index.html'),
12
+ )) as Response
13
+ if (response.ok) {
14
+ const html = await response.text()
15
+ return changeSourceReferences(html)
16
+ }
17
+ } catch (error) {
18
+ // eslint-disable-next-line no-console
19
+ console.error('Failed to load index.html from assets:', error)
20
+ }
21
+ }
22
+
23
+ // Fallback: provide a simple HTML page
24
+ return `<!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="utf-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29
+ <title>vlt | Explorer</title>
30
+ <style>
31
+ body { font-family: system-ui, sans-serif; padding: 2rem; text-align: center; }
32
+ .error { color: #dc2626; margin: 1rem 0; }
33
+ </style>
34
+ </head>
35
+ <body>
36
+ <h1>vlt | Explorer</h1>
37
+ <div class="error">Unable to load the application assets.</div>
38
+ <p>Please check that the server is running correctly and assets are available.</p>
39
+ </body>
40
+ </html>`
41
+ }
42
+
43
+ export const changeSourceReferences = (html: string): string => {
44
+ html = html.replace('href="/main.css', 'href="/public/main.css')
45
+ html = html.replace(
46
+ 'href="/favicon.ico"',
47
+ 'href="/public/favicon.ico"',
48
+ )
49
+ html = html.replace('href="/fonts/', 'href="/public/fonts/')
50
+ html = html.replace('src="/index.js"', 'src="/public/index.js"')
51
+ return html
52
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Simple tracing utility for debugging and monitoring
3
+ */
4
+
5
+ /**
6
+ * Logs a trace message with timestamp
7
+ * @param {string} _message - The message to log
8
+ * @param {any} _data - Optional additional data to log
9
+ */
10
+ export function trace(_message: string, _data?: any): void {
11
+ // Tracing disabled for production
12
+ }
13
+
14
+ /**
15
+ * Measures execution time of a function
16
+ * @param {string} name - Name of the operation being measured
17
+ * @param {() => Promise<any>} fn - Function to measure
18
+ * @returns {Promise<any>} Result of the function
19
+ */
20
+ export async function measureTime<T>(
21
+ name: string,
22
+ fn: () => Promise<T>,
23
+ ): Promise<T> {
24
+ const start = performance.now()
25
+ try {
26
+ const result = await fn()
27
+ const duration = performance.now() - start
28
+ trace(`${name} completed in ${duration.toFixed(2)}ms`)
29
+ return result
30
+ } catch (error) {
31
+ const duration = performance.now() - start
32
+ trace(`${name} failed after ${duration.toFixed(2)}ms`, error)
33
+ throw error
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Session monitoring middleware for tracking requests
39
+ * @param {any} _c - The Hono context
40
+ * @param {() => Promise<void>} next - The next middleware function
41
+ * @returns {Promise<void>} Result of next middleware
42
+ */
43
+ export function sessionMonitor(
44
+ _c: any,
45
+ next: () => Promise<void>,
46
+ ): Promise<void> {
47
+ // Session monitoring is currently disabled
48
+ // This middleware can be extended to add session tracking functionality
49
+ trace('Session monitoring middleware called')
50
+
51
+ return next()
52
+ }
@@ -0,0 +1,172 @@
1
+ import { ORIGIN_CONFIG, RESERVED_ROUTES } from '../../config.ts'
2
+ import type {
3
+ UpstreamConfig,
4
+ ParsedPackageInfo,
5
+ } from '../../types.ts'
6
+
7
+ /**
8
+ * Validates if an upstream name is allowed (not reserved)
9
+ * @param {string} upstreamName - The upstream name to validate
10
+ * @returns {boolean} True if valid, false if reserved
11
+ */
12
+ export function isValidUpstreamName(upstreamName: string): boolean {
13
+ return !RESERVED_ROUTES.includes(upstreamName)
14
+ }
15
+
16
+ /**
17
+ * Gets the upstream configuration by name
18
+ * @param {string} upstreamName - The upstream name
19
+ * @returns {UpstreamConfig | null} The upstream config or null if not found
20
+ */
21
+ export function getUpstreamConfig(
22
+ upstreamName: string,
23
+ ): UpstreamConfig | null {
24
+ return ORIGIN_CONFIG.upstreams[upstreamName] ?? null
25
+ }
26
+
27
+ /**
28
+ * Gets the default upstream name
29
+ * @returns {string} The default upstream name
30
+ */
31
+ export function getDefaultUpstream(): string {
32
+ return ORIGIN_CONFIG.default
33
+ }
34
+
35
+ /**
36
+ * Generates a cache key for upstream package data
37
+ * @param {string} upstreamName - The upstream name
38
+ * @param {string} packageName - The package name
39
+ * @param {string} [version] - The package version (optional)
40
+ * @returns {string} A deterministic hash ID
41
+ */
42
+ export function generateCacheKey(
43
+ upstreamName: string,
44
+ packageName: string,
45
+ version?: string,
46
+ ): string {
47
+ const key =
48
+ version ?
49
+ `${upstreamName}:${packageName}:${version}`
50
+ : `${upstreamName}:${packageName}`
51
+
52
+ // Use TextEncoder for cross-platform compatibility
53
+ const encoder = new TextEncoder()
54
+ const data = encoder.encode(key)
55
+
56
+ // Convert to base64 using btoa
57
+ const base64 = btoa(String.fromCharCode(...data))
58
+
59
+ // Convert base64 to base64url format (replace + with -, / with _, remove =)
60
+ return base64
61
+ .replace(/\+/g, '-')
62
+ .replace(/\//g, '_')
63
+ .replace(/=/g, '')
64
+ }
65
+
66
+ /**
67
+ * Parses a request path to extract package information
68
+ * @param {string} path - The request path
69
+ * @returns {ParsedPackageInfo} Parsed package info
70
+ */
71
+ export function parsePackageSpec(path: string): ParsedPackageInfo {
72
+ // Remove leading slash and split by '/'
73
+ const segments = path.replace(/^\/+/, '').split('/')
74
+
75
+ // Handle different path patterns
76
+ if (segments.length === 0) {
77
+ return { packageName: '', segments }
78
+ }
79
+
80
+ // Check if first segment is an upstream name
81
+ const firstSegment = segments[0]
82
+ if (firstSegment && ORIGIN_CONFIG.upstreams[firstSegment]) {
83
+ // Path starts with upstream name: /upstream/package/version
84
+ const upstream = firstSegment
85
+ const packageSegments = segments.slice(1)
86
+
87
+ if (packageSegments.length === 0) {
88
+ return { upstream, packageName: '', segments: packageSegments }
89
+ }
90
+
91
+ // Handle scoped packages: @scope/package
92
+ if (
93
+ packageSegments[0]?.startsWith('@') &&
94
+ packageSegments.length > 1
95
+ ) {
96
+ const packageName = `${packageSegments[0]}/${packageSegments[1]}`
97
+ const version = packageSegments[2]
98
+ const remainingSegments = packageSegments.slice(2)
99
+ return {
100
+ upstream,
101
+ packageName,
102
+ version,
103
+ segments: remainingSegments,
104
+ }
105
+ }
106
+
107
+ // Handle regular packages
108
+ const packageName = packageSegments[0] || ''
109
+ const version = packageSegments[1]
110
+ const remainingSegments = packageSegments.slice(1)
111
+ return {
112
+ upstream,
113
+ packageName,
114
+ version,
115
+ segments: remainingSegments,
116
+ }
117
+ }
118
+
119
+ // No upstream in path, treat as package name
120
+ if (firstSegment?.startsWith('@') && segments.length > 1) {
121
+ // Scoped package: @scope/package/version
122
+ const packageName = `${segments[0]}/${segments[1] || ''}`
123
+ const version = segments[2]
124
+ const remainingSegments = segments.slice(2)
125
+ return { packageName, version, segments: remainingSegments }
126
+ }
127
+
128
+ // Regular package: package/version
129
+ const packageName = segments[0] || ''
130
+ const version = segments[1]
131
+ const remainingSegments = segments.slice(1)
132
+ return { packageName, version, segments: remainingSegments }
133
+ }
134
+
135
+ /**
136
+ * Constructs the upstream URL for a package request
137
+ * @param {UpstreamConfig} upstreamConfig - The upstream configuration
138
+ * @param {string} packageName - The package name
139
+ * @param {string} [path] - Additional path segments
140
+ * @returns {string} The full upstream URL
141
+ */
142
+ export function buildUpstreamUrl(
143
+ upstreamConfig: UpstreamConfig,
144
+ packageName: string,
145
+ path = '',
146
+ ): string {
147
+ const baseUrl = upstreamConfig.url.replace(/\/$/, '')
148
+ const encodedPackage = encodeURIComponent(packageName)
149
+
150
+ switch (upstreamConfig.type) {
151
+ case 'npm':
152
+ case 'vsr':
153
+ return `${baseUrl}/${encodedPackage}${path ? `/${path}` : ''}`
154
+ case 'jsr':
155
+ // JSR has a different URL structure
156
+ return `${baseUrl}/${encodedPackage}${path ? `/${path}` : ''}`
157
+ case 'local':
158
+ return `${baseUrl}/${encodedPackage}${path ? `/${path}` : ''}`
159
+ default:
160
+ return `${baseUrl}/${encodedPackage}${path ? `/${path}` : ''}`
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Checks if proxying is enabled for an upstream
166
+ * @param {string} upstreamName - The upstream name
167
+ * @returns {boolean} True if proxying is enabled
168
+ */
169
+ export function isProxyEnabled(upstreamName: string): boolean {
170
+ const config = getUpstreamConfig(upstreamName)
171
+ return config !== null && config.type !== 'local'
172
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "es2022",
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "esModuleInterop": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "resolveJsonModule": true,
10
+ "declaration": false,
11
+ "declarationMap": false,
12
+ "sourceMap": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src/**/*", "config.ts", "types.ts"]
16
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./tsconfig.json"
3
+ }
package/typedoc.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import config from '../../www/docs/typedoc.workspace.mjs'
2
+ export default config(import.meta.dirname)