@vltpkg/vsr 0.0.0-27 → 0.0.0-28

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 (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +8 -6
  6. package/dist/index.js +3 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +49 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Cache utilities for efficient upstream package management
3
+ *
4
+ * Strategy:
5
+ * - Return cached data immediately when available (even if stale)
6
+ * - Queue background refresh if cache is stale
7
+ * - Only fetch upstream synchronously if no cache exists
8
+ * - Use Cloudflare Queues for reliable background processing
9
+ */
10
+
11
+ import type {
12
+ HonoContext,
13
+ CacheOptions,
14
+ CacheResult,
15
+ CacheValidation,
16
+ QueueMessage,
17
+ PackageManifest,
18
+ DatabaseOperations,
19
+ ParsedPackage,
20
+ } from '../../types.ts'
21
+
22
+ // Helper function to get typed database from context
23
+ function getDb(c: HonoContext): DatabaseOperations {
24
+ return c.get('db')
25
+ }
26
+
27
+ /**
28
+ * Stale-while-revalidate cache strategy for package data
29
+ * Returns stale cache immediately and refreshes in background via queue
30
+ */
31
+ export async function getCachedPackageWithRefresh<T>(
32
+ c: HonoContext,
33
+ packageName: string,
34
+ fetchUpstreamFn: () => Promise<T>,
35
+ options: CacheOptions = {},
36
+ ): Promise<CacheResult<T>> {
37
+ const {
38
+ packumentTtlMinutes = 5, // Short TTL for packuments (they change frequently)
39
+ staleWhileRevalidateMinutes = 60, // Allow stale data for up to 1 hour while refreshing
40
+ forceRefresh = false,
41
+ upstream = 'npm',
42
+ } = options
43
+
44
+ // If forcing refresh, skip cache entirely
45
+ if (forceRefresh) {
46
+ const upstreamData = await fetchUpstreamFn()
47
+ c.waitUntil?.(
48
+ cachePackageData(c, packageName, upstreamData, options),
49
+ )
50
+ return {
51
+ package: upstreamData,
52
+ fromCache: false,
53
+ }
54
+ }
55
+
56
+ // Get cached data first
57
+ const cachedResult = await getCachedPackageData(
58
+ c,
59
+ packageName,
60
+ packumentTtlMinutes,
61
+ staleWhileRevalidateMinutes,
62
+ )
63
+
64
+ if (cachedResult.data) {
65
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
66
+ const { valid, stale, data } = cachedResult
67
+
68
+ if (valid) {
69
+ // Cache is fresh - return immediately
70
+ return {
71
+ package: data as T,
72
+ fromCache: true,
73
+ stale: false,
74
+ }
75
+ } else if (stale) {
76
+ // Cache is stale but within stale-while-revalidate window
77
+ // Return stale data immediately and queue background refresh
78
+
79
+ // Queue background refresh using Cloudflare Queues
80
+ if (c.env.CACHE_REFRESH_QUEUE) {
81
+ await queuePackageRefresh(
82
+ c,
83
+ packageName,
84
+ upstream,
85
+ fetchUpstreamFn,
86
+ options,
87
+ )
88
+ } else {
89
+ // Fallback to waitUntil if queue not available
90
+ c.waitUntil?.(
91
+ refreshPackageInBackground(
92
+ c,
93
+ packageName,
94
+ fetchUpstreamFn,
95
+ options,
96
+ ),
97
+ )
98
+ }
99
+
100
+ return {
101
+ package: data as T,
102
+ fromCache: true,
103
+ stale: true,
104
+ }
105
+ }
106
+ }
107
+
108
+ // No cache data available - fetch upstream synchronously
109
+ const upstreamData = await fetchUpstreamFn()
110
+
111
+ // Cache the fresh data in background
112
+ c.waitUntil?.(
113
+ cachePackageData(c, packageName, upstreamData, options),
114
+ )
115
+
116
+ return {
117
+ package: upstreamData,
118
+ fromCache: false,
119
+ stale: false,
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Queue a package refresh job using Cloudflare Queues
125
+ * Note: We can't serialize functions, so we'll need to recreate the fetch function in the queue consumer
126
+ */
127
+ async function queuePackageRefresh<T>(
128
+ c: HonoContext,
129
+ packageName: string,
130
+ upstream: string,
131
+ _fetchUpstreamFn: () => Promise<T>,
132
+ options: CacheOptions,
133
+ ): Promise<void> {
134
+ try {
135
+ const message: QueueMessage = {
136
+ type: 'package_refresh',
137
+ packageName,
138
+ upstream,
139
+ timestamp: Date.now(),
140
+ options: {
141
+ packumentTtlMinutes: options.packumentTtlMinutes || 5,
142
+ upstream: options.upstream || 'npm',
143
+ },
144
+ }
145
+
146
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
147
+ await c.env.CACHE_REFRESH_QUEUE.send(message)
148
+ } catch (_error) {
149
+ // Background queue failed, but don't block the response
150
+ // Log to monitoring system instead of console
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Background refresh function (fallback when queue is not available)
156
+ */
157
+ async function refreshPackageInBackground<T>(
158
+ c: HonoContext,
159
+ packageName: string,
160
+ fetchUpstreamFn: () => Promise<T>,
161
+ options: CacheOptions,
162
+ ): Promise<void> {
163
+ try {
164
+ const upstreamData = await fetchUpstreamFn()
165
+ await cachePackageData(c, packageName, upstreamData, options)
166
+ } catch (_error) {
167
+ // Background queue failed, but don't block the response
168
+ // Log to monitoring system instead of console
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Reconstruct a full packument from cached database data
174
+ */
175
+ async function reconstructPackumentFromCache(
176
+ c: HonoContext,
177
+ packageName: string,
178
+ cachedPackage: ParsedPackage,
179
+ ): Promise<any> {
180
+ try {
181
+ // Get all versions for this package
182
+ const versions = await getDb(c).getVersionsByPackage(packageName)
183
+
184
+ // Build the versions object
185
+
186
+ const versionsObject: Record<string, any> = {}
187
+
188
+ const timeObject: Record<string, string> = {
189
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
190
+ modified:
191
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
192
+ (cachedPackage as any).lastUpdated ||
193
+ new Date().toISOString(),
194
+ }
195
+
196
+ versions.forEach(versionData => {
197
+ if (versionData.version) {
198
+ versionsObject[versionData.version] = versionData.manifest
199
+ if (versionData.published_at) {
200
+ timeObject[versionData.version] = versionData.published_at
201
+ }
202
+ }
203
+ })
204
+
205
+ // Parse tags from the cached package
206
+ const distTags =
207
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
208
+ typeof (cachedPackage as any).tags === 'string' ?
209
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
210
+ (JSON.parse((cachedPackage as any).tags || '{}') as Record<
211
+ string,
212
+ string
213
+ >)
214
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
215
+ : ((cachedPackage as any).tags as Record<string, string>)
216
+
217
+ // Construct the packument structure
218
+ return {
219
+ name: packageName,
220
+ 'dist-tags': distTags,
221
+ versions: versionsObject,
222
+ time: timeObject,
223
+ }
224
+ } catch (error) {
225
+ // TODO: Replace with proper logging system
226
+ // eslint-disable-next-line no-console
227
+ console.error(
228
+ 'Failed to reconstruct packument from cache:',
229
+ error,
230
+ )
231
+ return null
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Enhanced cache data retrieval with stale-while-revalidate support
237
+ */
238
+ async function getCachedPackageData(
239
+ c: HonoContext,
240
+ packageName: string,
241
+ ttlMinutes: number,
242
+ staleWhileRevalidateMinutes: number,
243
+ ): Promise<CacheValidation> {
244
+ try {
245
+ const cachedPackage = await getDb(c).getCachedPackage(packageName)
246
+
247
+ if (
248
+ !cachedPackage?.cachedAt ||
249
+ cachedPackage.origin !== 'upstream'
250
+ ) {
251
+ return { valid: false, stale: false, data: null }
252
+ }
253
+
254
+ const cacheTime = new Date(cachedPackage.cachedAt).getTime()
255
+ const now = new Date().getTime()
256
+ const ttlMs = ttlMinutes * 60 * 1000
257
+ const staleMs = staleWhileRevalidateMinutes * 60 * 1000
258
+
259
+ const age = now - cacheTime
260
+ const isValid = age < ttlMs
261
+ const isStale = age < staleMs // Still usable even if stale
262
+
263
+ // Reconstruct the full packument from cached data
264
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
265
+ const reconstructedPackument =
266
+ await reconstructPackumentFromCache(
267
+ c,
268
+ packageName,
269
+ cachedPackage,
270
+ )
271
+
272
+ return {
273
+ valid: isValid,
274
+ stale: isStale && !isValid, // Stale means expired but within stale window
275
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
276
+ data: reconstructedPackument,
277
+ }
278
+ } catch (_error) {
279
+ // Log error to monitoring system instead of console
280
+ return { valid: false, stale: false, data: null }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Stale-while-revalidate cache strategy for version data
286
+ * Returns stale cache immediately and refreshes in background via queue
287
+ */
288
+ export async function getCachedVersionWithRefresh<T>(
289
+ c: HonoContext,
290
+ spec: string,
291
+ fetchUpstreamFn: () => Promise<T>,
292
+ options: CacheOptions = {},
293
+ ): Promise<CacheResult<T>> {
294
+ const {
295
+ manifestTtlMinutes = 525600, // 1 year TTL for manifests
296
+ staleWhileRevalidateMinutes = 1051200, // Allow stale for 2 years (manifests rarely change)
297
+ forceRefresh = false,
298
+ upstream = 'npm',
299
+ } = options
300
+
301
+ // If forcing refresh, skip cache entirely
302
+ if (forceRefresh) {
303
+ const upstreamData = await fetchUpstreamFn()
304
+ c.waitUntil?.(cacheVersionData(c, spec, upstreamData, options))
305
+ return {
306
+ version: upstreamData,
307
+ fromCache: false,
308
+ }
309
+ }
310
+
311
+ // Get cached data first
312
+ const cachedResult = await getCachedVersionData(
313
+ c,
314
+ spec,
315
+ manifestTtlMinutes,
316
+ staleWhileRevalidateMinutes,
317
+ )
318
+
319
+ if (cachedResult.data) {
320
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
321
+ const { valid, stale, data } = cachedResult
322
+
323
+ if (valid) {
324
+ // Cache is fresh - return immediately
325
+ return {
326
+ version: data as T,
327
+ fromCache: true,
328
+ stale: false,
329
+ }
330
+ } else if (stale) {
331
+ // Cache is stale but within stale-while-revalidate window
332
+ // Return stale data immediately and queue background refresh
333
+
334
+ // Queue background refresh using Cloudflare Queues
335
+ if (c.env.CACHE_REFRESH_QUEUE) {
336
+ await queueVersionRefresh(
337
+ c,
338
+ spec,
339
+ upstream,
340
+ fetchUpstreamFn,
341
+ options,
342
+ )
343
+ } else {
344
+ // Fallback to waitUntil if queue not available
345
+ c.waitUntil?.(
346
+ refreshVersionInBackground(
347
+ c,
348
+ spec,
349
+ fetchUpstreamFn,
350
+ options,
351
+ ),
352
+ )
353
+ }
354
+
355
+ return {
356
+ version: data as T,
357
+ fromCache: true,
358
+ stale: true,
359
+ }
360
+ }
361
+ }
362
+
363
+ // No cache data available - fetch upstream synchronously
364
+ const upstreamData = await fetchUpstreamFn()
365
+
366
+ // Cache the fresh data in background
367
+ c.waitUntil?.(cacheVersionData(c, spec, upstreamData, options))
368
+
369
+ return {
370
+ version: upstreamData,
371
+ fromCache: false,
372
+ stale: false,
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Queue a version refresh job using Cloudflare Queues
378
+ */
379
+ async function queueVersionRefresh<T>(
380
+ c: HonoContext,
381
+ spec: string,
382
+ upstream: string,
383
+ _fetchUpstreamFn: () => Promise<T>,
384
+ options: CacheOptions,
385
+ ): Promise<void> {
386
+ try {
387
+ const message: QueueMessage = {
388
+ type: 'version_refresh',
389
+ spec,
390
+ upstream,
391
+ timestamp: Date.now(),
392
+ options: {
393
+ manifestTtlMinutes: options.manifestTtlMinutes || 525600,
394
+ upstream: options.upstream || 'npm',
395
+ },
396
+ }
397
+
398
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
399
+ await c.env.CACHE_REFRESH_QUEUE.send(message)
400
+ } catch (_error) {
401
+ // Background queue failed, but don't block the response
402
+ // Log to monitoring system instead of console
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Background refresh function for versions (fallback when queue is not available)
408
+ */
409
+ async function refreshVersionInBackground<T>(
410
+ c: HonoContext,
411
+ spec: string,
412
+ fetchUpstreamFn: () => Promise<T>,
413
+ options: CacheOptions,
414
+ ): Promise<void> {
415
+ try {
416
+ const upstreamData = await fetchUpstreamFn()
417
+ await cacheVersionData(c, spec, upstreamData, options)
418
+ } catch (_error) {
419
+ // Background queue failed, but don't block the response
420
+ // Log to monitoring system instead of console
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Enhanced cache data retrieval for versions with stale-while-revalidate support
426
+ */
427
+ async function getCachedVersionData(
428
+ c: HonoContext,
429
+ spec: string,
430
+ ttlMinutes: number,
431
+ staleWhileRevalidateMinutes: number,
432
+ ): Promise<CacheValidation> {
433
+ try {
434
+ const cachedVersion = await getDb(c).getCachedVersion(spec)
435
+
436
+ if (
437
+ !cachedVersion?.cachedAt ||
438
+ cachedVersion.origin !== 'upstream'
439
+ ) {
440
+ return { valid: false, stale: false, data: null }
441
+ }
442
+
443
+ const cacheTime = new Date(cachedVersion.cachedAt).getTime()
444
+ const now = new Date().getTime()
445
+ const ttlMs = ttlMinutes * 60 * 1000
446
+ const staleMs = staleWhileRevalidateMinutes * 60 * 1000
447
+
448
+ const age = now - cacheTime
449
+ const isValid = age < ttlMs
450
+ const isStale = age < staleMs // Still usable even if stale
451
+
452
+ return {
453
+ valid: isValid,
454
+ stale: isStale && !isValid, // Stale means expired but within stale window
455
+ data: cachedVersion,
456
+ }
457
+ } catch (_error) {
458
+ // Log error to monitoring system instead of console
459
+ return { valid: false, stale: false, data: null }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Cache package data in the database
465
+ */
466
+ async function cachePackageData(
467
+ c: HonoContext,
468
+ packageName: string,
469
+ packageData: unknown,
470
+ options: CacheOptions,
471
+ ): Promise<void> {
472
+ try {
473
+ if (
474
+ packageData &&
475
+ typeof packageData === 'object' &&
476
+ 'dist-tags' in packageData
477
+ ) {
478
+ const tags =
479
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-unsafe-member-access
480
+ ((packageData as any)['dist-tags'] as Record<
481
+ string,
482
+ string
483
+ >) || {}
484
+ await getDb(c).upsertCachedPackage(
485
+ packageName,
486
+ tags,
487
+ options.upstream ?? 'npm',
488
+ )
489
+ }
490
+ } catch (_error) {
491
+ // Log error to monitoring system instead of console
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Cache version data in the database
497
+ */
498
+ async function cacheVersionData(
499
+ c: HonoContext,
500
+ spec: string,
501
+ versionData: unknown,
502
+ options: CacheOptions,
503
+ ): Promise<void> {
504
+ try {
505
+ if (versionData && typeof versionData === 'object') {
506
+ const manifest = versionData as unknown as PackageManifest
507
+ const publishedAt = new Date().toISOString()
508
+ await getDb(c).upsertCachedVersion(
509
+ spec,
510
+ manifest,
511
+ options.upstream ?? 'npm',
512
+ publishedAt,
513
+ )
514
+ }
515
+ } catch (_error) {
516
+ // Log error to monitoring system instead of console
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Generate storage path for tarball cache
522
+ */
523
+ export function getTarballStoragePath(
524
+ packageName: string,
525
+ version: string,
526
+ origin: 'local' | 'upstream' = 'local',
527
+ upstream: string | null = null,
528
+ ): string {
529
+ const sanitizedName = packageName.replace(/[@/]/g, '_')
530
+ const sanitizedVersion = version.replace(/[^a-zA-Z0-9.-]/g, '_')
531
+
532
+ if (origin === 'upstream' && upstream) {
533
+ return `tarballs/${upstream}/${sanitizedName}/${sanitizedVersion}.tgz`
534
+ }
535
+
536
+ return `tarballs/local/${sanitizedName}/${sanitizedVersion}.tgz`
537
+ }
538
+
539
+ /**
540
+ * Check if tarball is cached
541
+ */
542
+ export async function isTarballCached(
543
+ _c: HonoContext,
544
+ packageName: string,
545
+ version: string,
546
+ origin: 'local' | 'upstream' = 'local',
547
+ upstream: string | null = null,
548
+ ): Promise<boolean> {
549
+ try {
550
+ const _storagePath = getTarballStoragePath(
551
+ packageName,
552
+ version,
553
+ origin,
554
+ upstream,
555
+ )
556
+ // This would need to be implemented based on your storage solution
557
+ // For now, return false as a placeholder
558
+ return false
559
+ } catch (_error) {
560
+ // Log error to monitoring system instead of console
561
+ return false
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Cache tarball data
567
+ */
568
+ export async function cacheTarball(
569
+ _c: HonoContext,
570
+ packageName: string,
571
+ version: string,
572
+ _tarballStream: ReadableStream,
573
+ origin: 'local' | 'upstream' = 'local',
574
+ upstream: string | null = null,
575
+ ): Promise<void> {
576
+ try {
577
+ const _storagePath = getTarballStoragePath(
578
+ packageName,
579
+ version,
580
+ origin,
581
+ upstream,
582
+ )
583
+ // This would need to be implemented based on your storage solution
584
+ } catch (_error) {
585
+ // Log error to monitoring system instead of console
586
+ }
587
+ }
@@ -0,0 +1,50 @@
1
+ import { resolveConfig } from '../middleware/config.ts'
2
+
3
+ /**
4
+ * Global runtime configuration - can be used outside of routes
5
+ * This will be populated when the server starts up with environment variables
6
+ */
7
+ let globalRuntimeConfig: ReturnType<typeof resolveConfig> | null =
8
+ null
9
+
10
+ /**
11
+ * Initialize the global runtime configuration
12
+ * Should be called early in the application lifecycle
13
+ */
14
+ export function initializeGlobalConfig(env?: any) {
15
+ globalRuntimeConfig = resolveConfig(env)
16
+ return globalRuntimeConfig
17
+ }
18
+
19
+ /**
20
+ * Get the current runtime configuration
21
+ * Falls back to resolving with no environment if not initialized
22
+ */
23
+ export function getRuntimeConfig(env?: any) {
24
+ if (globalRuntimeConfig) {
25
+ return globalRuntimeConfig
26
+ }
27
+ // Fallback to resolving with the provided env or defaults
28
+ return resolveConfig(env)
29
+ }
30
+
31
+ /**
32
+ * Check if daemon is enabled at runtime
33
+ */
34
+ export function isDaemonEnabled(env?: any): boolean {
35
+ return getRuntimeConfig(env).DAEMON_ENABLED
36
+ }
37
+
38
+ /**
39
+ * Check if telemetry is enabled at runtime
40
+ */
41
+ export function isTelemetryEnabled(env?: any): boolean {
42
+ return getRuntimeConfig(env).TELEMETRY_ENABLED
43
+ }
44
+
45
+ /**
46
+ * Check if debug is enabled at runtime
47
+ */
48
+ export function isDebugEnabled(env?: any): boolean {
49
+ return getRuntimeConfig(env).DEBUG_ENABLED
50
+ }
@@ -0,0 +1,69 @@
1
+ import { createDatabaseOperations } from '../db/client.ts'
2
+ import type { D1Database } from '@cloudflare/workers-types'
3
+ import type { DatabaseOperations, HonoContext } from '../../types.ts'
4
+
5
+ /**
6
+ * Creates database operations instance from D1 database
7
+ * @param {D1Database} d1 - The D1 database instance
8
+ * @returns {DatabaseOperations} Database operations interface
9
+ */
10
+ export function createDB(d1: D1Database): DatabaseOperations {
11
+ return createDatabaseOperations(d1)
12
+ }
13
+
14
+ /**
15
+ * Type guard to check if database is available
16
+ * @param {unknown} db - The database instance to check
17
+ * @returns {boolean} True if database is available and functional
18
+ */
19
+ export function isDatabaseAvailable(
20
+ db: unknown,
21
+ ): db is DatabaseOperations {
22
+ return (
23
+ typeof db === 'object' &&
24
+ db !== null &&
25
+ 'getPackage' in db &&
26
+ typeof (db as DatabaseOperations).getPackage === 'function'
27
+ )
28
+ }
29
+
30
+ // Cache the database operations to avoid recreating on every request
31
+ let cachedDbOperations: DatabaseOperations | null = null
32
+
33
+ /**
34
+ * Middleware to mount database operations on the context
35
+ * @param {HonoContext} c - The Hono context
36
+ * @param {() => Promise<void>} next - The next middleware function
37
+ */
38
+ export async function mountDatabase(
39
+ c: HonoContext,
40
+ next: () => Promise<void>,
41
+ ): Promise<void> {
42
+ // Check if this is a utility route that doesn't need database
43
+ const path = c.req.path
44
+ const isUtilityRoute =
45
+ path === '/-/ping' ||
46
+ path === '/-/docs' ||
47
+ /^\/[^/]+\/-\/(ping|docs)$/.test(path)
48
+
49
+ // Note: whoami and user endpoints DO need database access for authentication
50
+ // but we need to handle the case where DB is not available in tests
51
+
52
+ if (isUtilityRoute) {
53
+ // Skip database mounting for utility routes
54
+ await next()
55
+ return
56
+ }
57
+
58
+ if (!c.env.DB) {
59
+ throw new Error('Database not found in environment')
60
+ }
61
+
62
+ // Reuse existing database operations if available
63
+ cachedDbOperations ??= createDatabaseOperations(
64
+ c.env.DB,
65
+ ) as DatabaseOperations
66
+
67
+ c.set('db', cachedDbOperations)
68
+ await next()
69
+ }