@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.
- package/.editorconfig +13 -0
- package/.prettierrc +7 -0
- package/CONTRIBUTING.md +228 -0
- package/LICENSE.md +110 -0
- package/README.md +373 -0
- package/bin/vsr.ts +29 -0
- package/config.ts +124 -0
- package/debug-npm.js +19 -0
- package/drizzle.config.js +33 -0
- package/package.json +80 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/api.ts +2246 -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 +219 -0
- package/src/db/client.ts +544 -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 +41 -0
- package/src/index.ts +709 -0
- package/src/routes/access.ts +263 -0
- package/src/routes/auth.ts +93 -0
- package/src/routes/index.ts +135 -0
- package/src/routes/packages.ts +924 -0
- package/src/routes/search.ts +50 -0
- package/src/routes/static.ts +53 -0
- package/src/routes/tokens.ts +102 -0
- package/src/routes/users.ts +14 -0
- package/src/utils/auth.ts +145 -0
- package/src/utils/cache.ts +466 -0
- package/src/utils/database.ts +44 -0
- package/src/utils/packages.ts +337 -0
- package/src/utils/response.ts +100 -0
- package/src/utils/routes.ts +47 -0
- package/src/utils/spa.ts +14 -0
- package/src/utils/tracing.ts +63 -0
- package/src/utils/upstream.ts +131 -0
- package/test/README.md +91 -0
- package/test/access.test.js +760 -0
- package/test/cloudflare-waituntil.test.js +141 -0
- package/test/db.test.js +447 -0
- package/test/dist-tag.test.js +415 -0
- package/test/e2e.test.js +904 -0
- package/test/hono-context.test.js +250 -0
- package/test/integrity-validation.test.js +183 -0
- package/test/json-response.test.js +76 -0
- package/test/manifest-slimming.test.js +449 -0
- package/test/packument-consistency.test.js +351 -0
- package/test/packument-version-range.test.js +144 -0
- package/test/performance.test.js +162 -0
- package/test/route-with-waituntil.test.js +298 -0
- package/test/run-tests.js +151 -0
- package/test/setup-cache-tests.js +190 -0
- package/test/setup.js +64 -0
- package/test/stale-while-revalidate.test.js +273 -0
- package/test/static-assets.test.js +85 -0
- package/test/upstream-routing.test.js +86 -0
- package/test/utils/test-helpers.js +84 -0
- package/test/waituntil-correct.test.js +208 -0
- package/test/waituntil-demo.test.js +138 -0
- package/test/waituntil-readme.md +113 -0
- package/tsconfig.json +37 -0
- package/types.ts +446 -0
- package/vitest.config.js +95 -0
- 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
|
+
}
|