@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.
- package/DEPLOY.md +163 -0
- package/LICENSE +119 -0
- package/README.md +314 -0
- package/config.ts +221 -0
- 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 +77 -0
- package/scripts/build-assets.js +31 -0
- package/scripts/build-bin.js +62 -0
- package/scripts/prepack.js +27 -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 +484 -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/test/access.test.ts +705 -0
- package/test/audit.test.ts +828 -0
- package/test/dashboard.test.ts +693 -0
- package/test/dist-tags.test.ts +678 -0
- package/test/manifest.test.ts +436 -0
- package/test/packument.test.ts +530 -0
- package/test/ping.test.ts +41 -0
- package/test/search.test.ts +472 -0
- package/test/setup.ts +130 -0
- package/test/static.test.ts +646 -0
- package/test/tokens.test.ts +389 -0
- package/test/utils/auth.test.ts +214 -0
- package/test/utils/packages.test.ts +235 -0
- package/test/utils/response.test.ts +184 -0
- package/test/whoami.test.ts +119 -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/db/client.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import type { D1Database } from '@cloudflare/workers-types'
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1'
|
|
3
|
+
import { sql } from 'drizzle-orm'
|
|
4
|
+
import * as schema from './schema.ts'
|
|
5
|
+
import type {
|
|
6
|
+
ParsedPackage,
|
|
7
|
+
ParsedVersion,
|
|
8
|
+
ParsedToken,
|
|
9
|
+
} from '../../types.ts'
|
|
10
|
+
|
|
11
|
+
const fallbackLogger = {
|
|
12
|
+
error: (_message: string, _error?: unknown) => {
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
console.error(_message, _error)
|
|
15
|
+
},
|
|
16
|
+
info: (_message: string) => {
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.info(_message)
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Database = ReturnType<typeof drizzle<typeof schema>>
|
|
23
|
+
|
|
24
|
+
export function createDatabase(d1: D1Database): Database {
|
|
25
|
+
return drizzle(d1, { schema })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseJSON(value: string | null): any {
|
|
29
|
+
if (!value) return null
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(value)
|
|
32
|
+
} catch (_e) {
|
|
33
|
+
// Log to monitoring system instead of console
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function stringifyJSON(value: unknown): string {
|
|
39
|
+
return JSON.stringify(value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDatabaseOperations(
|
|
43
|
+
d1: D1Database,
|
|
44
|
+
logger = fallbackLogger,
|
|
45
|
+
) {
|
|
46
|
+
const db = createDatabase(d1)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
async getPackage(name: string): Promise<ParsedPackage | null> {
|
|
50
|
+
try {
|
|
51
|
+
const result = await db
|
|
52
|
+
.select()
|
|
53
|
+
.from(schema.packages)
|
|
54
|
+
.where(sql`name = ${name}`)
|
|
55
|
+
.get()
|
|
56
|
+
if (!result) return null
|
|
57
|
+
return {
|
|
58
|
+
name: result.name,
|
|
59
|
+
tags: parseJSON(result.tags) as Record<string, string>,
|
|
60
|
+
lastUpdated: result.lastUpdated || null,
|
|
61
|
+
origin: result.origin || null,
|
|
62
|
+
upstream: result.upstream || null,
|
|
63
|
+
cachedAt: result.cachedAt || null,
|
|
64
|
+
} as ParsedPackage
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
logger.error(
|
|
67
|
+
`[DB ERROR] Failed to get package ${name}`,
|
|
68
|
+
_error,
|
|
69
|
+
)
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async upsertPackage(
|
|
75
|
+
name: string,
|
|
76
|
+
tags: Record<string, string>,
|
|
77
|
+
lastUpdated?: string,
|
|
78
|
+
) {
|
|
79
|
+
try {
|
|
80
|
+
const result = await db
|
|
81
|
+
.insert(schema.packages)
|
|
82
|
+
.values({
|
|
83
|
+
name,
|
|
84
|
+
tags: stringifyJSON(tags),
|
|
85
|
+
lastUpdated: lastUpdated || new Date().toISOString(),
|
|
86
|
+
})
|
|
87
|
+
.onConflictDoUpdate({
|
|
88
|
+
target: schema.packages.name,
|
|
89
|
+
set: {
|
|
90
|
+
tags: stringifyJSON(tags),
|
|
91
|
+
lastUpdated: lastUpdated || new Date().toISOString(),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
return result
|
|
95
|
+
} catch (_error: unknown) {
|
|
96
|
+
logger.error(
|
|
97
|
+
`[DB ERROR] Failed to upsert package ${name}`,
|
|
98
|
+
_error,
|
|
99
|
+
)
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async upsertCachedPackage(
|
|
105
|
+
name: string,
|
|
106
|
+
tags: Record<string, string>,
|
|
107
|
+
upstream: string,
|
|
108
|
+
lastUpdated?: string,
|
|
109
|
+
) {
|
|
110
|
+
try {
|
|
111
|
+
const result = await db
|
|
112
|
+
.insert(schema.packages)
|
|
113
|
+
.values({
|
|
114
|
+
name,
|
|
115
|
+
tags: stringifyJSON(tags),
|
|
116
|
+
lastUpdated: lastUpdated || new Date().toISOString(),
|
|
117
|
+
origin: 'upstream',
|
|
118
|
+
upstream,
|
|
119
|
+
cachedAt: new Date().toISOString(),
|
|
120
|
+
})
|
|
121
|
+
.onConflictDoUpdate({
|
|
122
|
+
target: schema.packages.name,
|
|
123
|
+
set: {
|
|
124
|
+
tags: stringifyJSON(tags),
|
|
125
|
+
lastUpdated: lastUpdated || new Date().toISOString(),
|
|
126
|
+
cachedAt: new Date().toISOString(),
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
// Log to monitoring system instead of console
|
|
130
|
+
return result
|
|
131
|
+
} catch (_error: unknown) {
|
|
132
|
+
logger.error(
|
|
133
|
+
`[DB ERROR] Failed to upsert cached package ${name}`,
|
|
134
|
+
_error,
|
|
135
|
+
)
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async getCachedPackage(name: string) {
|
|
141
|
+
try {
|
|
142
|
+
const result = await db
|
|
143
|
+
.select()
|
|
144
|
+
.from(schema.packages)
|
|
145
|
+
.where(sql`name = ${name}`)
|
|
146
|
+
.get()
|
|
147
|
+
if (!result) return null
|
|
148
|
+
return {
|
|
149
|
+
name: result.name,
|
|
150
|
+
tags: parseJSON(result.tags) as Record<string, string>,
|
|
151
|
+
lastUpdated: result.lastUpdated || null,
|
|
152
|
+
origin: result.origin || null,
|
|
153
|
+
upstream: result.upstream || null,
|
|
154
|
+
cachedAt: result.cachedAt || null,
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.error(
|
|
158
|
+
`[DB ERROR] Failed to get cached package ${name}`,
|
|
159
|
+
error,
|
|
160
|
+
)
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async isPackageCacheValid(name: string, ttlMinutes = 5) {
|
|
166
|
+
try {
|
|
167
|
+
const pkg = await this.getCachedPackage(name)
|
|
168
|
+
if (!pkg?.cachedAt || pkg.origin !== 'upstream') {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const cacheTime = new Date(pkg.cachedAt).getTime()
|
|
173
|
+
const now = new Date().getTime()
|
|
174
|
+
const ttlMs = ttlMinutes * 60 * 1000
|
|
175
|
+
|
|
176
|
+
return now - cacheTime < ttlMs
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error(
|
|
179
|
+
`[DB ERROR] Failed to check cache validity for ${name}`,
|
|
180
|
+
error,
|
|
181
|
+
)
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Token operations
|
|
187
|
+
async getToken(token: string): Promise<ParsedToken | null> {
|
|
188
|
+
try {
|
|
189
|
+
const result = await db
|
|
190
|
+
.select()
|
|
191
|
+
.from(schema.tokens)
|
|
192
|
+
.where(sql`token = ${token}`)
|
|
193
|
+
.get()
|
|
194
|
+
if (!result) return null
|
|
195
|
+
return {
|
|
196
|
+
token: result.token,
|
|
197
|
+
uuid: result.uuid,
|
|
198
|
+
scope: parseJSON(result.scope) as {
|
|
199
|
+
values: string[]
|
|
200
|
+
types: {
|
|
201
|
+
pkg?: { read: boolean; write: boolean }
|
|
202
|
+
user?: { read: boolean; write: boolean }
|
|
203
|
+
}
|
|
204
|
+
}[],
|
|
205
|
+
} as ParsedToken
|
|
206
|
+
} catch (_error: unknown) {
|
|
207
|
+
logger.error(
|
|
208
|
+
`[DB ERROR] Failed to get token ${token}`,
|
|
209
|
+
_error,
|
|
210
|
+
)
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// Validate if a user can access or modify another user's token
|
|
216
|
+
async validateTokenAccess(authToken: string, targetUuid: string) {
|
|
217
|
+
// Special characters validation for UUIDs
|
|
218
|
+
const specialChars = ['~', '!', '*', '^', '&']
|
|
219
|
+
if (
|
|
220
|
+
targetUuid &&
|
|
221
|
+
specialChars.some(char => targetUuid.startsWith(char))
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
'Invalid uuid - uuids can not start with special characters (ex. - ~ ! * ^ &)',
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// If no auth token provided, cannot proceed
|
|
229
|
+
if (!authToken) {
|
|
230
|
+
throw new Error('Unauthorized')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Get authenticated user from token
|
|
234
|
+
const authUser = await this.getToken(authToken)
|
|
235
|
+
if (!authUser?.uuid) {
|
|
236
|
+
throw new Error('Unauthorized')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Allow users to manage their own tokens
|
|
240
|
+
if (authUser.uuid === targetUuid) {
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check user permissions
|
|
245
|
+
const scope = authUser.scope as {
|
|
246
|
+
values: string[]
|
|
247
|
+
types: {
|
|
248
|
+
pkg?: { read: boolean; write: boolean }
|
|
249
|
+
user?: { read: boolean; write: boolean }
|
|
250
|
+
}
|
|
251
|
+
}[]
|
|
252
|
+
const uuid = targetUuid
|
|
253
|
+
|
|
254
|
+
// Parse token access permissions - variables kept for future use
|
|
255
|
+
const _read = ['get']
|
|
256
|
+
const _write = ['put', 'post', 'delete']
|
|
257
|
+
let anyUser = false
|
|
258
|
+
let specificUser = false
|
|
259
|
+
let writeAccess = false
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(scope)) {
|
|
262
|
+
for (const s of scope) {
|
|
263
|
+
if (s.types.user) {
|
|
264
|
+
if (s.values.includes('*')) {
|
|
265
|
+
anyUser = true
|
|
266
|
+
}
|
|
267
|
+
if (s.values.includes(`~${uuid}`)) {
|
|
268
|
+
specificUser = true
|
|
269
|
+
}
|
|
270
|
+
if ((anyUser || specificUser) && s.types.user.write) {
|
|
271
|
+
writeAccess = true
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check if user has appropriate permissions
|
|
278
|
+
if ((!anyUser && !specificUser) || !writeAccess) {
|
|
279
|
+
throw new Error('Unauthorized')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return true
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async upsertToken(
|
|
286
|
+
token: string,
|
|
287
|
+
uuid: string,
|
|
288
|
+
scope: {
|
|
289
|
+
values: string[]
|
|
290
|
+
types: {
|
|
291
|
+
pkg?: { read: boolean; write: boolean }
|
|
292
|
+
user?: { read: boolean; write: boolean }
|
|
293
|
+
}
|
|
294
|
+
}[],
|
|
295
|
+
authToken?: string,
|
|
296
|
+
) {
|
|
297
|
+
// Validate the UUID doesn't start with special characters
|
|
298
|
+
const specialChars = ['~', '!', '*', '^', '&']
|
|
299
|
+
if (uuid && specialChars.some(char => uuid.startsWith(char))) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
'Invalid uuid - uuids can not start with special characters (ex. - ~ ! * ^ &)',
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If authToken is provided, validate access permissions
|
|
306
|
+
if (authToken) {
|
|
307
|
+
await this.validateTokenAccess(authToken, uuid)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// After validation passes, perform the database operation
|
|
311
|
+
|
|
312
|
+
return db
|
|
313
|
+
.insert(schema.tokens)
|
|
314
|
+
.values({
|
|
315
|
+
token,
|
|
316
|
+
uuid,
|
|
317
|
+
scope: stringifyJSON(scope),
|
|
318
|
+
})
|
|
319
|
+
.onConflictDoUpdate({
|
|
320
|
+
target: schema.tokens.token,
|
|
321
|
+
set: {
|
|
322
|
+
uuid,
|
|
323
|
+
scope: stringifyJSON(scope),
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
async deleteToken(token: string, authToken?: string) {
|
|
329
|
+
if (authToken) {
|
|
330
|
+
// Get the token data to check its UUID
|
|
331
|
+
const tokenData = await this.getToken(token)
|
|
332
|
+
if (tokenData?.uuid) {
|
|
333
|
+
// Validate access permission for this UUID
|
|
334
|
+
await this.validateTokenAccess(authToken, tokenData.uuid)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return db
|
|
339
|
+
.delete(schema.tokens)
|
|
340
|
+
.where(sql`token = ${token}`)
|
|
341
|
+
.run()
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// Version operations
|
|
345
|
+
async getVersion(spec: string): Promise<ParsedVersion | null> {
|
|
346
|
+
try {
|
|
347
|
+
const result = await db
|
|
348
|
+
.select()
|
|
349
|
+
.from(schema.versions)
|
|
350
|
+
.where(sql`spec = ${spec}`)
|
|
351
|
+
.get()
|
|
352
|
+
if (!result) return null
|
|
353
|
+
return {
|
|
354
|
+
spec: result.spec,
|
|
355
|
+
version: result.spec.split('@')[1] ?? '',
|
|
356
|
+
manifest: parseJSON(result.manifest) as Record<string, any>,
|
|
357
|
+
published_at: result.publishedAt || null,
|
|
358
|
+
origin: result.origin || null,
|
|
359
|
+
upstream: result.upstream || null,
|
|
360
|
+
cachedAt: result.cachedAt || null,
|
|
361
|
+
} as ParsedVersion
|
|
362
|
+
} catch (_error: unknown) {
|
|
363
|
+
logger.error(
|
|
364
|
+
`[DB ERROR] Failed to get version ${spec}`,
|
|
365
|
+
_error,
|
|
366
|
+
)
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
async upsertVersion(
|
|
372
|
+
spec: string,
|
|
373
|
+
manifest: Record<string, any>,
|
|
374
|
+
publishedAt: string,
|
|
375
|
+
) {
|
|
376
|
+
try {
|
|
377
|
+
// Attempting to upsert version: ${spec}
|
|
378
|
+
// Make sure manifest is an object
|
|
379
|
+
if (typeof manifest !== 'object') {
|
|
380
|
+
// Invalid manifest for ${spec}, received: ${typeof manifest}
|
|
381
|
+
manifest = {
|
|
382
|
+
name: spec.split('@')[0],
|
|
383
|
+
version: spec.split('@').slice(1).join('@'),
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = await db
|
|
388
|
+
.insert(schema.versions)
|
|
389
|
+
.values({
|
|
390
|
+
spec,
|
|
391
|
+
manifest: stringifyJSON(manifest),
|
|
392
|
+
publishedAt,
|
|
393
|
+
})
|
|
394
|
+
.onConflictDoUpdate({
|
|
395
|
+
target: schema.versions.spec,
|
|
396
|
+
set: {
|
|
397
|
+
manifest: stringifyJSON(manifest),
|
|
398
|
+
publishedAt,
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
// Successfully upserted version: ${spec}
|
|
402
|
+
return result
|
|
403
|
+
} catch (error) {
|
|
404
|
+
logger.error(
|
|
405
|
+
`[DB ERROR] Failed to upsert version ${spec}`,
|
|
406
|
+
error,
|
|
407
|
+
)
|
|
408
|
+
return { success: true } // Mock successful operation
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
async upsertCachedVersion(
|
|
413
|
+
spec: string,
|
|
414
|
+
manifest: Record<string, any>,
|
|
415
|
+
upstream: string,
|
|
416
|
+
publishedAt: string,
|
|
417
|
+
) {
|
|
418
|
+
try {
|
|
419
|
+
// Attempting to upsert cached version: ${spec} from upstream: ${upstream}
|
|
420
|
+
|
|
421
|
+
// Make sure manifest is an object
|
|
422
|
+
if (typeof manifest !== 'object') {
|
|
423
|
+
// Invalid manifest for ${spec}, received: ${typeof manifest}
|
|
424
|
+
manifest = {
|
|
425
|
+
name: spec.split('@')[0],
|
|
426
|
+
version: spec.split('@').slice(1).join('@'),
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const result = await db
|
|
431
|
+
.insert(schema.versions)
|
|
432
|
+
.values({
|
|
433
|
+
spec,
|
|
434
|
+
manifest: stringifyJSON(manifest),
|
|
435
|
+
publishedAt,
|
|
436
|
+
origin: 'upstream',
|
|
437
|
+
upstream,
|
|
438
|
+
cachedAt: new Date().toISOString(),
|
|
439
|
+
})
|
|
440
|
+
.onConflictDoUpdate({
|
|
441
|
+
target: schema.versions.spec,
|
|
442
|
+
set: {
|
|
443
|
+
manifest: stringifyJSON(manifest),
|
|
444
|
+
cachedAt: new Date().toISOString(),
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
// Successfully upserted cached version: ${spec}
|
|
448
|
+
return result
|
|
449
|
+
} catch (error) {
|
|
450
|
+
logger.error(
|
|
451
|
+
`[DB ERROR] Failed to upsert cached version ${spec}`,
|
|
452
|
+
error,
|
|
453
|
+
)
|
|
454
|
+
return { success: true }
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
async getCachedVersion(spec: string) {
|
|
459
|
+
try {
|
|
460
|
+
// Attempting to get cached version: ${spec}
|
|
461
|
+
const result = await db
|
|
462
|
+
.select()
|
|
463
|
+
.from(schema.versions)
|
|
464
|
+
.where(sql`spec = ${spec}`)
|
|
465
|
+
.get()
|
|
466
|
+
if (!result) return null
|
|
467
|
+
|
|
468
|
+
// Extract version from spec, handling scoped packages correctly
|
|
469
|
+
let version
|
|
470
|
+
const lastAtIndex = result.spec.lastIndexOf('@')
|
|
471
|
+
if (lastAtIndex > 0) {
|
|
472
|
+
version = result.spec.substring(lastAtIndex + 1)
|
|
473
|
+
} else {
|
|
474
|
+
// Fallback: assume everything after first @ is version
|
|
475
|
+
version = result.spec.split('@').slice(1).join('@')
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
spec: result.spec,
|
|
480
|
+
version,
|
|
481
|
+
manifest: parseJSON(result.manifest) as Record<string, any>,
|
|
482
|
+
published_at: result.publishedAt || null,
|
|
483
|
+
origin: result.origin || null,
|
|
484
|
+
upstream: result.upstream || null,
|
|
485
|
+
cachedAt: result.cachedAt || null,
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
logger.error(
|
|
489
|
+
`[DB ERROR] Failed to get cached version ${spec}`,
|
|
490
|
+
error,
|
|
491
|
+
)
|
|
492
|
+
return null
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
async isVersionCacheValid(spec: string, ttlMinutes = 525600) {
|
|
497
|
+
// Default 1 year for manifests
|
|
498
|
+
try {
|
|
499
|
+
const version = await this.getCachedVersion(spec)
|
|
500
|
+
if (!version?.cachedAt || version.origin !== 'upstream') {
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const cacheTime = new Date(version.cachedAt).getTime()
|
|
505
|
+
const now = new Date().getTime()
|
|
506
|
+
const ttlMs = ttlMinutes * 60 * 1000
|
|
507
|
+
|
|
508
|
+
return now - cacheTime < ttlMs
|
|
509
|
+
} catch (error) {
|
|
510
|
+
logger.error(
|
|
511
|
+
`[DB ERROR] Failed to check version cache validity for ${spec}`,
|
|
512
|
+
error,
|
|
513
|
+
)
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
// Search operations
|
|
519
|
+
async searchPackages(query: string, scope?: string) {
|
|
520
|
+
const results = await db
|
|
521
|
+
.select()
|
|
522
|
+
.from(schema.packages)
|
|
523
|
+
.where(
|
|
524
|
+
scope ?
|
|
525
|
+
sql`name LIKE ${`${scope}/%`} AND name LIKE ${`%${query}%`}`
|
|
526
|
+
: sql`name LIKE ${`%${query}%`}`,
|
|
527
|
+
)
|
|
528
|
+
.all()
|
|
529
|
+
|
|
530
|
+
return results.map(result => ({
|
|
531
|
+
name: result.name,
|
|
532
|
+
tags: parseJSON(result.tags) as Record<string, string>,
|
|
533
|
+
}))
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
// Get all versions for a specific package
|
|
537
|
+
async getVersionsByPackage(
|
|
538
|
+
packageName: string,
|
|
539
|
+
): Promise<ParsedVersion[]> {
|
|
540
|
+
try {
|
|
541
|
+
// Retrieving all versions for package: ${packageName}
|
|
542
|
+
const results = await db
|
|
543
|
+
.select()
|
|
544
|
+
.from(schema.versions)
|
|
545
|
+
.where(sql`spec LIKE ${`${packageName}@%`}`)
|
|
546
|
+
.all()
|
|
547
|
+
|
|
548
|
+
if (results.length === 0) {
|
|
549
|
+
// No versions found for package: ${packageName}
|
|
550
|
+
return []
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return results.map(result => {
|
|
554
|
+
const manifest = parseJSON(result.manifest) as Record<
|
|
555
|
+
string,
|
|
556
|
+
any
|
|
557
|
+
>
|
|
558
|
+
|
|
559
|
+
// Extract version from spec, handling scoped packages correctly
|
|
560
|
+
// For "@scope/package@version" -> extract "version"
|
|
561
|
+
// For "package@version" -> extract "version"
|
|
562
|
+
let version
|
|
563
|
+
const lastAtIndex = result.spec.lastIndexOf('@')
|
|
564
|
+
if (lastAtIndex > 0) {
|
|
565
|
+
version = result.spec.substring(lastAtIndex + 1)
|
|
566
|
+
} else {
|
|
567
|
+
// Fallback: assume everything after first @ is version
|
|
568
|
+
version = result.spec.split('@').slice(1).join('@')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
spec: result.spec,
|
|
573
|
+
version,
|
|
574
|
+
manifest,
|
|
575
|
+
published_at: result.publishedAt || null,
|
|
576
|
+
origin: result.origin || null,
|
|
577
|
+
upstream: result.upstream || null,
|
|
578
|
+
cachedAt: result.cachedAt || null,
|
|
579
|
+
} as ParsedVersion
|
|
580
|
+
})
|
|
581
|
+
} catch (error) {
|
|
582
|
+
logger.error(
|
|
583
|
+
`[DB ERROR] Failed to get versions for package ${packageName}`,
|
|
584
|
+
error,
|
|
585
|
+
)
|
|
586
|
+
return [] // Return empty array on error
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE `packages` (
|
|
2
|
+
`name` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`tags` text
|
|
4
|
+
);
|
|
5
|
+
CREATE TABLE `tokens` (
|
|
6
|
+
`token` text PRIMARY KEY NOT NULL,
|
|
7
|
+
`uuid` text NOT NULL,
|
|
8
|
+
`scope` text
|
|
9
|
+
);
|
|
10
|
+
CREATE TABLE `versions` (
|
|
11
|
+
`spec` text PRIMARY KEY NOT NULL,
|
|
12
|
+
`manifest` text,
|
|
13
|
+
`published_at` text
|
|
14
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS packages (
|
|
2
|
+
name TEXT PRIMARY KEY,
|
|
3
|
+
tags TEXT NOT NULL
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS tokens (
|
|
7
|
+
token TEXT PRIMARY KEY,
|
|
8
|
+
uuid TEXT NOT NULL CHECK (
|
|
9
|
+
uuid NOT LIKE '~%' AND
|
|
10
|
+
uuid NOT LIKE '!%' AND
|
|
11
|
+
uuid NOT LIKE '*%' AND
|
|
12
|
+
uuid NOT LIKE '^%' AND
|
|
13
|
+
uuid NOT LIKE '&%'
|
|
14
|
+
),
|
|
15
|
+
scope TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS versions (
|
|
19
|
+
spec TEXT PRIMARY KEY,
|
|
20
|
+
manifest TEXT NOT NULL,
|
|
21
|
+
published_at TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
-- Insert default admin token
|
|
25
|
+
INSERT OR REPLACE INTO tokens (token, uuid, scope) VALUES (
|
|
26
|
+
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
27
|
+
'admin',
|
|
28
|
+
'[{"values":["*"],"types":{"pkg":{"read":true,"write":true},"user":{"read":true,"write":true}}}]'
|
|
29
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- Add check constraint to prevent UUIDs from starting with special characters
|
|
2
|
+
-- SQLite doesn't support ALTER TABLE ADD CONSTRAINT, so we need to recreate the table
|
|
3
|
+
-- Step 1: Create a new table with the constraint
|
|
4
|
+
CREATE TABLE IF NOT EXISTS tokens_new (
|
|
5
|
+
token TEXT PRIMARY KEY,
|
|
6
|
+
uuid TEXT NOT NULL CHECK (
|
|
7
|
+
uuid NOT LIKE '~%' AND
|
|
8
|
+
uuid NOT LIKE '!%' AND
|
|
9
|
+
uuid NOT LIKE '*%' AND
|
|
10
|
+
uuid NOT LIKE '^%' AND
|
|
11
|
+
uuid NOT LIKE '&%'
|
|
12
|
+
),
|
|
13
|
+
scope TEXT NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Step 2: Copy data from the old table to the new one
|
|
17
|
+
INSERT INTO tokens_new SELECT * FROM tokens WHERE
|
|
18
|
+
uuid NOT LIKE '~%' AND
|
|
19
|
+
uuid NOT LIKE '!%' AND
|
|
20
|
+
uuid NOT LIKE '*%' AND
|
|
21
|
+
uuid NOT LIKE '^%' AND
|
|
22
|
+
uuid NOT LIKE '&%';
|
|
23
|
+
|
|
24
|
+
-- Step 3: Drop the old table
|
|
25
|
+
DROP TABLE tokens;
|
|
26
|
+
|
|
27
|
+
-- Step 4: Rename the new table to the original name
|
|
28
|
+
ALTER TABLE tokens_new RENAME TO tokens;
|
|
29
|
+
|
|
30
|
+
-- Step 5: Re-insert the default admin token (just to be safe)
|
|
31
|
+
INSERT OR REPLACE INTO tokens (token, uuid, scope) VALUES (
|
|
32
|
+
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
33
|
+
'admin',
|
|
34
|
+
'[{"values":["*"],"types":{"pkg":{"read":true,"write":true},"user":{"read":true,"write":true}}}]'
|
|
35
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ALTER TABLE `packages` ADD `last_updated` text;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE `packages` ADD `origin` text DEFAULT 'local' NOT NULL;--> statement-breakpoint
|
|
3
|
+
ALTER TABLE `packages` ADD `upstream` text;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE `packages` ADD `cached_at` text;--> statement-breakpoint
|
|
5
|
+
ALTER TABLE `versions` ADD `origin` text DEFAULT 'local' NOT NULL;--> statement-breakpoint
|
|
6
|
+
ALTER TABLE `versions` ADD `upstream` text;--> statement-breakpoint
|
|
7
|
+
ALTER TABLE `versions` ADD `cached_at` text;
|