@vltpkg/vsr 0.0.0-26

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 (93) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +119 -0
  3. package/README.md +314 -0
  4. package/config.ts +221 -0
  5. package/drizzle.config.js +40 -0
  6. package/info/COMPARISONS.md +37 -0
  7. package/info/CONFIGURATION.md +143 -0
  8. package/info/CONTRIBUTING.md +32 -0
  9. package/info/DATABASE_SETUP.md +108 -0
  10. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  11. package/info/PROJECT_STRUCTURE.md +291 -0
  12. package/info/ROADMAP.md +27 -0
  13. package/info/SUPPORT.md +39 -0
  14. package/info/TESTING.md +301 -0
  15. package/info/USER_SUPPORT.md +31 -0
  16. package/package.json +77 -0
  17. package/scripts/build-assets.js +31 -0
  18. package/scripts/build-bin.js +62 -0
  19. package/scripts/prepack.js +27 -0
  20. package/src/assets/public/images/bg.png +0 -0
  21. package/src/assets/public/images/clients/logo-bun.png +0 -0
  22. package/src/assets/public/images/clients/logo-deno.png +0 -0
  23. package/src/assets/public/images/clients/logo-npm.png +0 -0
  24. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  25. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  26. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  27. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  28. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  29. package/src/assets/public/images/favicon/favicon.ico +0 -0
  30. package/src/assets/public/images/favicon/favicon.svg +3 -0
  31. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  32. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  33. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  34. package/src/assets/public/styles/styles.css +231 -0
  35. package/src/bin/demo/package.json +6 -0
  36. package/src/bin/demo/vlt.json +1 -0
  37. package/src/bin/vsr.ts +484 -0
  38. package/src/db/client.ts +590 -0
  39. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  40. package/src/db/migrations/0000_initial.sql +29 -0
  41. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  42. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  43. package/src/db/migrations/drop.sql +3 -0
  44. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  45. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  46. package/src/db/migrations/meta/_journal.json +20 -0
  47. package/src/db/schema.ts +43 -0
  48. package/src/index.ts +434 -0
  49. package/src/middleware/config.ts +79 -0
  50. package/src/middleware/telemetry.ts +43 -0
  51. package/src/queue/index.ts +97 -0
  52. package/src/routes/access.ts +852 -0
  53. package/src/routes/docs.ts +63 -0
  54. package/src/routes/misc.ts +469 -0
  55. package/src/routes/packages.ts +2823 -0
  56. package/src/routes/ping.ts +39 -0
  57. package/src/routes/search.ts +131 -0
  58. package/src/routes/static.ts +74 -0
  59. package/src/routes/tokens.ts +259 -0
  60. package/src/routes/users.ts +68 -0
  61. package/src/utils/auth.ts +202 -0
  62. package/src/utils/cache.ts +587 -0
  63. package/src/utils/config.ts +50 -0
  64. package/src/utils/database.ts +69 -0
  65. package/src/utils/docs.ts +146 -0
  66. package/src/utils/packages.ts +453 -0
  67. package/src/utils/response.ts +125 -0
  68. package/src/utils/routes.ts +64 -0
  69. package/src/utils/spa.ts +52 -0
  70. package/src/utils/tracing.ts +52 -0
  71. package/src/utils/upstream.ts +172 -0
  72. package/test/access.test.ts +705 -0
  73. package/test/audit.test.ts +828 -0
  74. package/test/dashboard.test.ts +693 -0
  75. package/test/dist-tags.test.ts +678 -0
  76. package/test/manifest.test.ts +436 -0
  77. package/test/packument.test.ts +530 -0
  78. package/test/ping.test.ts +41 -0
  79. package/test/search.test.ts +472 -0
  80. package/test/setup.ts +130 -0
  81. package/test/static.test.ts +646 -0
  82. package/test/tokens.test.ts +389 -0
  83. package/test/utils/auth.test.ts +214 -0
  84. package/test/utils/packages.test.ts +235 -0
  85. package/test/utils/response.test.ts +184 -0
  86. package/test/whoami.test.ts +119 -0
  87. package/tsconfig.json +16 -0
  88. package/tsconfig.worker.json +3 -0
  89. package/typedoc.mjs +2 -0
  90. package/types.ts +598 -0
  91. package/vitest.config.ts +25 -0
  92. package/vlt.json.example +56 -0
  93. package/wrangler.json +65 -0
@@ -0,0 +1,39 @@
1
+ import { URL } from '../../config.ts'
2
+ import { createRoute, z } from '@hono/zod-openapi'
3
+ import type { Context } from 'hono'
4
+
5
+ export const pingRoute = createRoute({
6
+ method: 'get',
7
+ path: '/-/ping',
8
+ tags: ['Health Check'],
9
+ summary: 'Ping',
10
+ description: `Check if the server is alive
11
+ \`\`\`bash
12
+ $ npm ping
13
+ npm notice PING ${URL}
14
+ npm notice PONG 13ms
15
+ \`\`\``,
16
+ request: {},
17
+ responses: {
18
+ 200: {
19
+ content: {
20
+ 'application/json': {
21
+ schema: z.object({}),
22
+ },
23
+ },
24
+ headers: z.object({
25
+ 'npm-notice': z.string().openapi({
26
+ description: 'Contains "PONG" for npm client compatibility',
27
+ example: 'PONG',
28
+ }),
29
+ }),
30
+ description: 'Server is alive',
31
+ },
32
+ },
33
+ })
34
+
35
+ // Ping route handler
36
+ export function handlePing(c: Context): Response {
37
+ c.header('npm-notice', 'PONG')
38
+ return c.json({}, 200)
39
+ }
@@ -0,0 +1,131 @@
1
+ import type { HonoContext } from '../../types.ts'
2
+ import { createRoute, z } from '@hono/zod-openapi'
3
+
4
+ /**
5
+ * Search for packages by text query
6
+ */
7
+ export async function searchPackages(c: HonoContext) {
8
+ try {
9
+ const text = c.req.query('text')
10
+
11
+ if (!text) {
12
+ return c.json(
13
+ {
14
+ error: 'Missing required parameter "text"',
15
+ },
16
+ 400,
17
+ )
18
+ }
19
+
20
+ // Use the existing database search function
21
+ const results = await c.get('db').searchPackages(text)
22
+
23
+ return c.json(results)
24
+ } catch (error) {
25
+ // TODO: Replace with proper logging system
26
+ // eslint-disable-next-line no-console
27
+ console.error('Search error:', error)
28
+ return c.json(
29
+ {
30
+ error: 'Internal server error',
31
+ },
32
+ 500,
33
+ )
34
+ }
35
+ }
36
+
37
+ // Route definition for OpenAPI documentation
38
+ export const searchPackagesRoute = createRoute({
39
+ method: 'get',
40
+ path: '/-/search',
41
+ tags: ['Search'],
42
+ summary: 'Search Packages',
43
+ description: `Search for packages by text query
44
+ \`\`\`bash
45
+ $ npm search react
46
+ \`\`\``,
47
+ request: {
48
+ query: z.object({
49
+ text: z.string().describe('Search query string'),
50
+ size: z
51
+ .string()
52
+ .optional()
53
+ .describe('Number of results to return (default: 20)'),
54
+ from: z
55
+ .string()
56
+ .optional()
57
+ .describe('Offset for pagination (default: 0)'),
58
+ }),
59
+ },
60
+ responses: {
61
+ 200: {
62
+ content: {
63
+ 'application/json': {
64
+ schema: z.object({
65
+ objects: z.array(
66
+ z.object({
67
+ package: z.object({
68
+ name: z.string(),
69
+ version: z.string(),
70
+ description: z.string().optional(),
71
+ keywords: z.array(z.string()).optional(),
72
+ date: z.string().optional(),
73
+ links: z
74
+ .object({
75
+ npm: z.string().optional(),
76
+ homepage: z.string().optional(),
77
+ repository: z.string().optional(),
78
+ bugs: z.string().optional(),
79
+ })
80
+ .optional(),
81
+ author: z
82
+ .object({
83
+ name: z.string(),
84
+ email: z.string().optional(),
85
+ })
86
+ .optional(),
87
+ publisher: z
88
+ .object({
89
+ username: z.string(),
90
+ email: z.string().optional(),
91
+ })
92
+ .optional(),
93
+ maintainers: z
94
+ .array(
95
+ z.object({
96
+ username: z.string(),
97
+ email: z.string().optional(),
98
+ }),
99
+ )
100
+ .optional(),
101
+ }),
102
+ score: z.object({
103
+ final: z.number(),
104
+ detail: z.object({
105
+ quality: z.number(),
106
+ popularity: z.number(),
107
+ maintenance: z.number(),
108
+ }),
109
+ }),
110
+ searchScore: z.number(),
111
+ }),
112
+ ),
113
+ total: z.number(),
114
+ time: z.string(),
115
+ }),
116
+ },
117
+ },
118
+ description: 'Search results',
119
+ },
120
+ 400: {
121
+ content: {
122
+ 'application/json': {
123
+ schema: z.object({
124
+ error: z.string(),
125
+ }),
126
+ },
127
+ },
128
+ description: 'Bad request - missing text parameter',
129
+ },
130
+ },
131
+ })
@@ -0,0 +1,74 @@
1
+ import type { HonoContext } from '../../types.ts'
2
+
3
+ /**
4
+ * Handles static asset serving
5
+ */
6
+ export const handleStaticAssets = async (c: HonoContext) => {
7
+ try {
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
9
+ const response = (await c.env.ASSETS.fetch(c.req.raw)) as Response
10
+
11
+ // If the ASSETS binding returns a 404, return a proper 404 response
12
+ // with mutable headers to avoid the secure headers middleware conflict
13
+ if (response.status === 404) {
14
+ return await c.notFound()
15
+ }
16
+
17
+ // For other non-200 responses, create a new response to avoid header conflicts
18
+ if (!response.ok) {
19
+ return c.text('Asset not available', response.status as any)
20
+ }
21
+
22
+ return response
23
+ } catch (error) {
24
+ // TODO: Replace with proper logging system
25
+ // eslint-disable-next-line no-console
26
+ console.error('Error serving static asset:', error)
27
+ return c.text('Internal Server Error', 500)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Handles favicon requests
33
+ */
34
+ export const handleFavicon = async (c: HonoContext) => {
35
+ // Redirect to the correct favicon path
36
+ return c.redirect('/public/images/favicon/favicon.ico', 301)
37
+ }
38
+
39
+ /**
40
+ * Handles robots.txt requests
41
+ */
42
+ export function handleRobots(c: HonoContext) {
43
+ return c.text(`User-agent: *
44
+ Allow: /
45
+
46
+ Sitemap: ${c.req.url.replace(/\/robots\.txt$/, '/sitemap.xml')}`)
47
+ }
48
+
49
+ /**
50
+ * Handles manifest.json requests for PWA
51
+ */
52
+ export function handleManifest(c: HonoContext) {
53
+ return c.json({
54
+ name: 'VLT Serverless Registry',
55
+ short_name: 'VSR',
56
+ description: 'A serverless npm registry',
57
+ start_url: '/',
58
+ display: 'standalone',
59
+ background_color: '#ffffff',
60
+ theme_color: '#000000',
61
+ icons: [
62
+ {
63
+ src: '/public/images/favicon/web-app-manifest-192x192.png',
64
+ sizes: '192x192',
65
+ type: 'image/png',
66
+ },
67
+ {
68
+ src: '/public/images/favicon/web-app-manifest-512x512.png',
69
+ sizes: '512x512',
70
+ type: 'image/png',
71
+ },
72
+ ],
73
+ })
74
+ }
@@ -0,0 +1,259 @@
1
+ // import { v4 as uuidv4 } from 'uuid' // Removed unused import
2
+ import { getTokenFromHeader } from '../utils/auth.ts'
3
+ import type {
4
+ HonoContext,
5
+ DatabaseOperations,
6
+ TokenScope,
7
+ } from '../../types.ts'
8
+ import { createRoute, z } from '@hono/zod-openapi'
9
+
10
+ // Helper function to get typed database from context
11
+ function getDb(c: HonoContext): DatabaseOperations {
12
+ return c.get('db')
13
+ }
14
+
15
+ export async function getToken(c: HonoContext) {
16
+ const token = c.req.param('token')
17
+ if (!token) {
18
+ return c.json({ error: 'Token parameter required' }, 400)
19
+ }
20
+
21
+ const tokenData = await getDb(c).getToken(token)
22
+ if (!tokenData) {
23
+ return c.json({ error: 'Token not found' }, 404)
24
+ }
25
+
26
+ return c.json(tokenData)
27
+ }
28
+
29
+ export async function postToken(c: HonoContext) {
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32
+ const body = await c.req.json()
33
+ const authToken = getTokenFromHeader(c)
34
+
35
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
36
+ if (!body.token || !body.uuid || !body.scope) {
37
+ return c.json(
38
+ { error: 'Missing required fields: token, uuid, scope' },
39
+ 400,
40
+ )
41
+ }
42
+
43
+ // Use the enhanced database operation that includes validation
44
+ await getDb(c).upsertToken(
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
46
+ body.token as string,
47
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
48
+ body.uuid as string,
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
50
+ body.scope as TokenScope[],
51
+ authToken ?? undefined,
52
+ )
53
+ return c.json({ success: true })
54
+ } catch (error) {
55
+ const err = error as Error
56
+ // Handle validation errors
57
+ if (err.message.includes('Invalid uuid')) {
58
+ return c.json({ error: err.message }, 400)
59
+ } else if (err.message.includes('Unauthorized')) {
60
+ return c.json({ error: 'Unauthorized' }, 401)
61
+ }
62
+
63
+ // Handle other errors - log to monitoring system instead of console
64
+ return c.json({ error: 'Internal server error' }, 500)
65
+ }
66
+ }
67
+
68
+ // scope is optional (only for privileged tokens) - ex. "read:@scope/pkg" or "read+write:@scope/pkg"
69
+ export async function putToken(c: HonoContext) {
70
+ try {
71
+ const token = c.req.param('token')
72
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
73
+ const body = await c.req.json()
74
+ const authToken = getTokenFromHeader(c)
75
+
76
+ if (!token) {
77
+ return c.json({ error: 'Token parameter required' }, 400)
78
+ }
79
+
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
81
+ if (!body.uuid || !body.scope) {
82
+ return c.json(
83
+ { error: 'Missing required fields: uuid, scope' },
84
+ 400,
85
+ )
86
+ }
87
+
88
+ // Use the enhanced database operation that includes validation
89
+ await getDb(c).upsertToken(
90
+ token,
91
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
92
+ body.uuid as string,
93
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
94
+ body.scope as TokenScope[],
95
+ authToken ?? undefined,
96
+ )
97
+ return c.json({ success: true })
98
+ } catch (error) {
99
+ const err = error as Error
100
+ // Handle validation errors
101
+ if (err.message.includes('Invalid uuid')) {
102
+ return c.json({ error: err.message }, 400)
103
+ } else if (err.message.includes('Unauthorized')) {
104
+ return c.json({ error: 'Unauthorized' }, 401)
105
+ }
106
+
107
+ // Handle other errors - log to monitoring system instead of console
108
+ return c.json({ error: 'Internal server error' }, 500)
109
+ }
110
+ }
111
+
112
+ export async function deleteToken(c: HonoContext) {
113
+ try {
114
+ const token = c.req.param('token')
115
+ const authToken = getTokenFromHeader(c)
116
+
117
+ if (!token) {
118
+ return c.json({ error: 'Token parameter required' }, 400)
119
+ }
120
+
121
+ // Use the enhanced database operation that includes validation
122
+ await getDb(c).deleteToken(token, authToken ?? undefined)
123
+ return c.json({ success: true })
124
+ } catch (error) {
125
+ const err = error as Error
126
+ // Handle validation errors
127
+ if (err.message.includes('Unauthorized')) {
128
+ return c.json({ error: 'Unauthorized' }, 401)
129
+ }
130
+
131
+ // Handle other errors - log to monitoring system instead of console
132
+ return c.json({ error: 'Internal server error' }, 500)
133
+ }
134
+ }
135
+
136
+ // Route definitions for OpenAPI documentation
137
+ export const getTokensRoute = createRoute({
138
+ method: 'get',
139
+ path: '/-/tokens',
140
+ tags: ['Authentication'],
141
+ summary: 'List Tokens',
142
+ description: `List all authentication tokens for the authenticated user
143
+ \`\`\`bash
144
+ $ npm token list
145
+ \`\`\``,
146
+ request: {},
147
+ responses: {
148
+ 200: {
149
+ content: {
150
+ 'application/json': {
151
+ schema: z.array(
152
+ z.object({
153
+ token: z.string(),
154
+ key: z.string(),
155
+ cidr_whitelist: z.array(z.string()).optional(),
156
+ readonly: z.boolean(),
157
+ automation: z.boolean(),
158
+ created: z.string(),
159
+ updated: z.string(),
160
+ }),
161
+ ),
162
+ },
163
+ },
164
+ description: 'List of tokens',
165
+ },
166
+ },
167
+ })
168
+
169
+ export const createTokenRoute = createRoute({
170
+ method: 'post',
171
+ path: '/-/tokens',
172
+ tags: ['Authentication'],
173
+ summary: 'Create Token',
174
+ description: `Create a new authentication token
175
+ \`\`\`bash
176
+ $ npm token create
177
+ \`\`\``,
178
+ request: {
179
+ body: {
180
+ content: {
181
+ 'application/json': {
182
+ schema: z.object({
183
+ token: z.string(),
184
+ uuid: z.string(),
185
+ scope: z.string(),
186
+ }),
187
+ },
188
+ },
189
+ },
190
+ },
191
+ responses: {
192
+ 201: {
193
+ content: {
194
+ 'application/json': {
195
+ schema: z.object({
196
+ token: z.string(),
197
+ key: z.string(),
198
+ }),
199
+ },
200
+ },
201
+ description: 'Token created successfully',
202
+ },
203
+ },
204
+ })
205
+
206
+ export const updateTokenRoute = createRoute({
207
+ method: 'put',
208
+ path: '/-/tokens',
209
+ tags: ['Authentication'],
210
+ summary: 'Update Token',
211
+ description: `Update an existing authentication token`,
212
+ request: {
213
+ body: {
214
+ content: {
215
+ 'application/json': {
216
+ schema: z.object({
217
+ token: z.string(),
218
+ uuid: z.string(),
219
+ scope: z.string(),
220
+ }),
221
+ },
222
+ },
223
+ },
224
+ },
225
+ responses: {
226
+ 200: {
227
+ content: {
228
+ 'application/json': {
229
+ schema: z.object({
230
+ token: z.string(),
231
+ key: z.string(),
232
+ }),
233
+ },
234
+ },
235
+ description: 'Token updated successfully',
236
+ },
237
+ },
238
+ })
239
+
240
+ export const deleteTokenRoute = createRoute({
241
+ method: 'delete',
242
+ path: '/-/tokens/token/{token}',
243
+ tags: ['Authentication'],
244
+ summary: 'Delete Token',
245
+ description: `Delete an authentication token
246
+ \`\`\`bash
247
+ $ npm token revoke <token>
248
+ \`\`\``,
249
+ request: {
250
+ params: z.object({
251
+ token: z.string(),
252
+ }),
253
+ },
254
+ responses: {
255
+ 204: {
256
+ description: 'Token deleted successfully',
257
+ },
258
+ },
259
+ })
@@ -0,0 +1,68 @@
1
+ import { getAuthedUser } from '../utils/auth.ts'
2
+ import type { HonoContext } from '../../types.ts'
3
+ import { createRoute, z } from '@hono/zod-openapi'
4
+
5
+ export async function getUsername(c: HonoContext) {
6
+ const user = await getAuthedUser({ c })
7
+ const uuid = user?.uuid || 'anonymous'
8
+ return c.json({ username: uuid }, 200)
9
+ }
10
+
11
+ export async function getUserProfile(c: HonoContext) {
12
+ const user = await getAuthedUser({ c })
13
+ const uuid = user?.uuid || 'anonymous'
14
+ return c.json({ name: uuid }, 200)
15
+ }
16
+
17
+ // Route definitions for OpenAPI documentation
18
+ export const userProfileRoute = createRoute({
19
+ method: 'get',
20
+ path: '/-/user',
21
+ tags: ['Authentication'],
22
+ summary: 'Get User Profile',
23
+ description: `Returns profile object associated with auth token
24
+ \`\`\`bash
25
+ $ npm profile
26
+ name: johnsmith
27
+ created: 2015-02-26T01:26:01.124Z
28
+ updated: 2023-01-10T21:55:32.118Z
29
+ \`\`\``,
30
+ request: {},
31
+ responses: {
32
+ 200: {
33
+ content: {
34
+ 'application/json': {
35
+ schema: z.object({
36
+ name: z.string(),
37
+ }),
38
+ },
39
+ },
40
+ description: 'User Profile',
41
+ },
42
+ },
43
+ })
44
+
45
+ export const whoamiRoute = createRoute({
46
+ method: 'get',
47
+ path: '/-/whoami',
48
+ tags: ['Authentication'],
49
+ summary: 'Who Am I',
50
+ description: `Returns the username of the authenticated user
51
+ \`\`\`bash
52
+ $ npm whoami
53
+ johnsmith
54
+ \`\`\``,
55
+ request: {},
56
+ responses: {
57
+ 200: {
58
+ content: {
59
+ 'application/json': {
60
+ schema: z.object({
61
+ username: z.string(),
62
+ }),
63
+ },
64
+ },
65
+ description: 'Username',
66
+ },
67
+ },
68
+ })