@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
|
@@ -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
|
+
}
|