@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
@@ -0,0 +1,50 @@
1
+ import type { HonoContext } from '../../types.ts'
2
+
3
+ export async function searchPackages(c: HonoContext) {
4
+ try {
5
+ const query = c.req.query('text') || ''
6
+ const scope = c.req.query('scope')
7
+
8
+ if (!query.trim()) {
9
+ return c.json({ objects: [], total: 0 }, 200)
10
+ }
11
+
12
+ const results = await c.db.searchPackages(query, scope)
13
+
14
+ return c.json({
15
+ objects: results.map(pkg => ({
16
+ package: {
17
+ name: pkg.name,
18
+ scope: pkg.name.startsWith('@') ? pkg.name.split('/')[0] : 'unscoped',
19
+ version: pkg.version || '1.0.0',
20
+ description: pkg.description || '',
21
+ keywords: pkg.keywords || [],
22
+ date: pkg.lastUpdated || new Date().toISOString(),
23
+ links: {
24
+ npm: `https://www.npmjs.com/package/${pkg.name}`,
25
+ homepage: pkg.homepage,
26
+ repository: pkg.repository,
27
+ bugs: pkg.bugs
28
+ },
29
+ author: pkg.author,
30
+ publisher: pkg.publisher,
31
+ maintainers: pkg.maintainers || []
32
+ },
33
+ score: {
34
+ final: 1.0,
35
+ detail: {
36
+ quality: 1.0,
37
+ popularity: 1.0,
38
+ maintenance: 1.0
39
+ }
40
+ },
41
+ searchScore: 1.0
42
+ })),
43
+ total: results.length,
44
+ time: new Date().toISOString()
45
+ })
46
+ } catch (error) {
47
+ console.error('[SEARCH ERROR]', error)
48
+ return c.json({ error: 'Search failed' }, 500)
49
+ }
50
+ }
@@ -0,0 +1,53 @@
1
+ import { serveStatic } from 'hono/cloudflare-workers'
2
+ import type { HonoContext } from '../../types.ts'
3
+
4
+ /**
5
+ * Handles static asset serving
6
+ */
7
+ export const handleStaticAssets = serveStatic({
8
+ root: './src/assets/public'
9
+ } as any)
10
+
11
+ /**
12
+ * Handles favicon requests
13
+ */
14
+ export const handleFavicon = serveStatic({
15
+ path: './src/assets/public/images/favicon/favicon.ico'
16
+ } as any)
17
+
18
+ /**
19
+ * Handles robots.txt requests
20
+ */
21
+ export function handleRobots(c: HonoContext) {
22
+ return c.text(`User-agent: *
23
+ Allow: /
24
+
25
+ Sitemap: ${c.req.url.replace(/\/robots\.txt$/, '/sitemap.xml')}`)
26
+ }
27
+
28
+ /**
29
+ * Handles manifest.json requests for PWA
30
+ */
31
+ export function handleManifest(c: HonoContext) {
32
+ return c.json({
33
+ name: 'VLT Serverless Registry',
34
+ short_name: 'VSR',
35
+ description: 'A serverless npm registry',
36
+ start_url: '/',
37
+ display: 'standalone',
38
+ background_color: '#ffffff',
39
+ theme_color: '#000000',
40
+ icons: [
41
+ {
42
+ src: '/public/images/favicon/web-app-manifest-192x192.png',
43
+ sizes: '192x192',
44
+ type: 'image/png'
45
+ },
46
+ {
47
+ src: '/public/images/favicon/web-app-manifest-512x512.png',
48
+ sizes: '512x512',
49
+ type: 'image/png'
50
+ }
51
+ ]
52
+ })
53
+ }
@@ -0,0 +1,102 @@
1
+ // import { v4 as uuidv4 } from 'uuid' // Removed unused import
2
+ import { getAuthedUser, getTokenFromHeader } from '../utils/auth.ts'
3
+ import type { HonoContext, TokenCreateRequest, TokenScope } from '../../types.ts'
4
+
5
+ export async function getToken(c: HonoContext) {
6
+ const token = c.req.param('token')
7
+ if (!token) {
8
+ return c.json({ error: 'Token parameter required' }, 400)
9
+ }
10
+
11
+ const tokenData = await c.db.getToken(token)
12
+ if (!tokenData) {
13
+ return c.json({ error: 'Token not found' }, 404)
14
+ }
15
+
16
+ return c.json(tokenData)
17
+ }
18
+
19
+ export async function postToken(c: HonoContext) {
20
+ try {
21
+ const body = await c.req.json() as TokenCreateRequest & { token: string }
22
+ const authToken = getTokenFromHeader(c)
23
+
24
+ if (!body.token || !body.uuid || !body.scope) {
25
+ return c.json({ error: 'Missing required fields: token, uuid, scope' }, 400)
26
+ }
27
+
28
+ // Use the enhanced database operation that includes validation
29
+ await c.db.upsertToken(body.token, body.uuid, body.scope, authToken || undefined)
30
+ return c.json({ success: true })
31
+ } catch (error) {
32
+ const err = error as Error
33
+ // Handle validation errors
34
+ if (err.message.includes('Invalid uuid')) {
35
+ return c.json({ error: err.message }, 400)
36
+ } else if (err.message.includes('Unauthorized')) {
37
+ return c.json({ error: 'Unauthorized' }, 401)
38
+ }
39
+
40
+ // Handle other errors
41
+ console.error('Error in postToken:', error)
42
+ return c.json({ error: 'Internal server error' }, 500)
43
+ }
44
+ }
45
+
46
+ // scope is optional (only for privileged tokens) - ex. "read:@scope/pkg" or "read+write:@scope/pkg"
47
+ export async function putToken(c: HonoContext) {
48
+ try {
49
+ const token = c.req.param('token')
50
+ const body = await c.req.json() as { uuid: string; scope: TokenScope[] }
51
+ const authToken = getTokenFromHeader(c)
52
+
53
+ if (!token) {
54
+ return c.json({ error: 'Token parameter required' }, 400)
55
+ }
56
+
57
+ if (!body.uuid || !body.scope) {
58
+ return c.json({ error: 'Missing required fields: uuid, scope' }, 400)
59
+ }
60
+
61
+ // Use the enhanced database operation that includes validation
62
+ await c.db.upsertToken(token, body.uuid, body.scope, authToken || undefined)
63
+ return c.json({ success: true })
64
+ } catch (error) {
65
+ const err = error as Error
66
+ // Handle validation errors
67
+ if (err.message.includes('Invalid uuid')) {
68
+ return c.json({ error: err.message }, 400)
69
+ } else if (err.message.includes('Unauthorized')) {
70
+ return c.json({ error: 'Unauthorized' }, 401)
71
+ }
72
+
73
+ // Handle other errors
74
+ console.error('Error in putToken:', error)
75
+ return c.json({ error: 'Internal server error' }, 500)
76
+ }
77
+ }
78
+
79
+ export async function deleteToken(c: HonoContext) {
80
+ try {
81
+ const token = c.req.param('token')
82
+ const authToken = getTokenFromHeader(c)
83
+
84
+ if (!token) {
85
+ return c.json({ error: 'Token parameter required' }, 400)
86
+ }
87
+
88
+ // Use the enhanced database operation that includes validation
89
+ await c.db.deleteToken(token, authToken || undefined)
90
+ return c.json({ success: true })
91
+ } catch (error) {
92
+ const err = error as Error
93
+ // Handle validation errors
94
+ if (err.message.includes('Unauthorized')) {
95
+ return c.json({ error: 'Unauthorized' }, 401)
96
+ }
97
+
98
+ // Handle other errors
99
+ console.error('Error in deleteToken:', error)
100
+ return c.json({ error: 'Internal server error' }, 500)
101
+ }
102
+ }
@@ -0,0 +1,14 @@
1
+ import { getAuthedUser } from '../utils/auth.ts'
2
+ import type { HonoContext } from '../../types.ts'
3
+
4
+ export async function getUsername(c: HonoContext) {
5
+ const user = await getAuthedUser({ c })
6
+ const uuid = user?.uuid || 'anonymous'
7
+ return c.json({ username: uuid }, 200)
8
+ }
9
+
10
+ export async function getUserProfile(c: HonoContext) {
11
+ const user = await getAuthedUser({ c })
12
+ const uuid = user?.uuid || 'anonymous'
13
+ return c.json({ name: uuid }, 200)
14
+ }
@@ -0,0 +1,145 @@
1
+ import { packageSpec } from './packages.ts'
2
+ import type { HonoContext, TokenScope, TokenAccess, AuthUser } from '../../types.ts'
3
+
4
+ export function getTokenFromHeader(c: HonoContext): string | null {
5
+ const auth = c.req.header('Authorization')
6
+ if (auth && auth.startsWith('Bearer ')) {
7
+ return auth.substring(7).trim()
8
+ }
9
+ return null
10
+ }
11
+
12
+ export function parseTokenAccess({ scope, pkg, uuid }: { scope: TokenScope[]; pkg?: string; uuid: string }): TokenAccess {
13
+ const read = ['get']
14
+ const write = ['put', 'post', 'delete']
15
+ let temp: TokenAccess = {
16
+ anyUser: false,
17
+ specificUser: false,
18
+ anyPackage: false,
19
+ specificPackage: false,
20
+ readAccess: false,
21
+ writeAccess: false,
22
+ methods: []
23
+ }
24
+
25
+ // TODO: add for multiple package access/aliases in scopes
26
+ const alternates: Record<string, string> = {}
27
+
28
+ scope.map(s => {
29
+ if (s.types.pkg) {
30
+ if (s.values.includes('*')) {
31
+ temp.anyPackage = true
32
+ }
33
+ if (pkg && (s.values.includes(pkg) || (alternates[pkg] && s.values.includes(alternates[pkg])))) {
34
+ temp.specificPackage = true
35
+ }
36
+ if ((temp.anyPackage || temp.specificPackage) && s.types.pkg.read) {
37
+ temp.readAccess = true
38
+ }
39
+ if ((temp.anyPackage || temp.specificPackage) && s.types.pkg.write) {
40
+ temp.writeAccess = true
41
+ }
42
+ }
43
+ if (s.types.user) {
44
+ if (s.values.includes('*')) {
45
+ temp.anyUser = true
46
+ }
47
+ if (s.values.includes(`~${uuid}`)) {
48
+ temp.specificUser = true
49
+ }
50
+ if ((temp.anyUser || temp.specificUser) && s.types.user.read) {
51
+ temp.readAccess = true
52
+ }
53
+ if ((temp.anyUser || temp.specificUser) && s.types.user.write) {
54
+ temp.writeAccess = true
55
+ }
56
+ }
57
+ })
58
+
59
+ temp.methods = (temp.readAccess ? read : []).concat(temp.writeAccess ? write : [])
60
+ return temp
61
+ }
62
+
63
+ export function isUserRoute(path: string): boolean {
64
+ const routes = [
65
+ 'ping',
66
+ 'whoami',
67
+ 'vlt/tokens',
68
+ 'npm/v1/user',
69
+ 'npm/v1/tokens',
70
+ 'org/',
71
+ ]
72
+ return !!routes.filter(r => path.startsWith(`/-/${r}`)).length
73
+ }
74
+
75
+ export async function getUserFromToken({ c, token }: { c: HonoContext; token: string }): Promise<AuthUser> {
76
+ const result = await c.db.getToken(token)
77
+ if (!result) return { uuid: null, scope: null, token }
78
+
79
+ // Handle the case when scope is already an object (for tests)
80
+ let scope = result.scope
81
+ if (typeof scope === 'string') {
82
+ try {
83
+ scope = JSON.parse(scope) as TokenScope[]
84
+ } catch (e) {
85
+ console.error('Failed to parse scope JSON:', e)
86
+ return { uuid: null, scope: null, token }
87
+ }
88
+ }
89
+
90
+ return {
91
+ uuid: result.uuid,
92
+ scope: scope,
93
+ token
94
+ }
95
+ }
96
+
97
+ export async function getAuthedUser({ c, token }: { c: HonoContext; token?: string | null }): Promise<AuthUser | null> {
98
+ const authToken = token || getTokenFromHeader(c)
99
+ if (!authToken) {
100
+ return null
101
+ }
102
+ return await getUserFromToken({ c, token: authToken })
103
+ }
104
+
105
+ export async function verifyToken(token: string, c: HonoContext): Promise<boolean> {
106
+ const method = c.req.method ? c.req.method.toLowerCase() : ''
107
+
108
+ if (!token) {
109
+ return false
110
+ }
111
+
112
+ const { uuid, scope } = await getUserFromToken({ c, token })
113
+
114
+ if (!uuid || !scope || !scope.length) {
115
+ return false
116
+ } else {
117
+ const { path } = c.req
118
+ const { pkg } = packageSpec(c)
119
+ const routeType = (isUserRoute(path)) ? 'user' : pkg ? 'pkg' : null
120
+
121
+ // determine access
122
+ const {
123
+ anyUser,
124
+ specificUser,
125
+ anyPackage,
126
+ specificPackage,
127
+ methods
128
+ } = parseTokenAccess({ scope, pkg, uuid })
129
+
130
+ const methodAllowed = methods.includes(method)
131
+
132
+ // if the route is a user route
133
+ if (routeType === 'user') {
134
+ return methodAllowed && (anyUser || specificUser)
135
+ }
136
+
137
+ // handle package routes
138
+ if (routeType === 'pkg') {
139
+ return methodAllowed && (anyPackage || specificPackage)
140
+ }
141
+
142
+ // fallback to false (should be unreachable code path)
143
+ return false
144
+ }
145
+ }