@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,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;
@@ -0,0 +1,3 @@
1
+ DROP TABLE IF EXISTS packages;
2
+ DROP TABLE IF EXISTS tokens;
3
+ DROP TABLE IF EXISTS versions;