hostdb 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -6
- package/databases.json +220 -27
- package/dist/checksums.d.ts +16 -0
- package/dist/checksums.d.ts.map +1 -0
- package/dist/checksums.js +74 -0
- package/dist/checksums.js.map +1 -0
- package/dist/databases.d.ts +125 -0
- package/dist/databases.d.ts.map +1 -0
- package/dist/databases.js +217 -0
- package/dist/databases.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/r2.d.ts +60 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +127 -0
- package/dist/r2.js.map +1 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +15 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolver.d.ts +142 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +306 -0
- package/dist/resolver.js.map +1 -0
- package/package.json +20 -4
- package/releases.json +581 -8
- package/lib/checksums.ts +0 -98
- package/lib/databases.ts +0 -341
- package/lib/r2.ts +0 -208
- package/lib/registry.ts +0 -17
package/lib/checksums.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared checksum utilities for fetching and parsing checksums.txt files
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
type GitHubAsset = {
|
|
6
|
-
id: number
|
|
7
|
-
name: string
|
|
8
|
-
browser_download_url: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type GitHubRelease = {
|
|
12
|
-
assets: GitHubAsset[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse checksums.txt content into a record of filename -> hash
|
|
17
|
-
*/
|
|
18
|
-
export function parseChecksums(content: string): Record<string, string> {
|
|
19
|
-
const checksums: Record<string, string> = {}
|
|
20
|
-
|
|
21
|
-
for (const line of content.split('\n')) {
|
|
22
|
-
// Format: "hash filename" or "hash *filename" (binary mode)
|
|
23
|
-
const match = line.match(/^([a-f0-9]{64})\s+\*?(.+)$/)
|
|
24
|
-
if (match) {
|
|
25
|
-
checksums[match[2]] = match[1]
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return checksums
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Fetch checksums.txt from a GitHub release using the API (avoids CDN caching issues)
|
|
34
|
-
*
|
|
35
|
-
* @param repo - Repository in "owner/repo" format
|
|
36
|
-
* @param tag - Release tag name
|
|
37
|
-
* @returns Record of filename -> SHA256 hash, or empty object if not found
|
|
38
|
-
*/
|
|
39
|
-
export async function fetchChecksums(
|
|
40
|
-
repo: string,
|
|
41
|
-
tag: string,
|
|
42
|
-
): Promise<Record<string, string>> {
|
|
43
|
-
// First try to get the asset URL from the API
|
|
44
|
-
const apiUrl = `https://api.github.com/repos/${repo}/releases/tags/${tag}`
|
|
45
|
-
const apiResponse = await fetch(apiUrl, {
|
|
46
|
-
headers: {
|
|
47
|
-
Accept: 'application/vnd.github.v3+json',
|
|
48
|
-
'User-Agent': 'hostdb-checksums',
|
|
49
|
-
...(process.env.GITHUB_TOKEN
|
|
50
|
-
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
|
51
|
-
: {}),
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
if (!apiResponse.ok) {
|
|
56
|
-
return {}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const release = (await apiResponse.json()) as GitHubRelease
|
|
60
|
-
const checksumAsset = release.assets.find((a) => a.name === 'checksums.txt')
|
|
61
|
-
|
|
62
|
-
if (!checksumAsset) {
|
|
63
|
-
return {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Use API asset download (works for both private and public repos)
|
|
67
|
-
const assetApiUrl = `https://api.github.com/repos/${repo}/releases/assets/${checksumAsset.id}`
|
|
68
|
-
const assetApiResponse = await fetch(assetApiUrl, {
|
|
69
|
-
headers: {
|
|
70
|
-
Accept: 'application/octet-stream',
|
|
71
|
-
'User-Agent': 'hostdb-checksums',
|
|
72
|
-
...(process.env.GITHUB_TOKEN
|
|
73
|
-
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
|
74
|
-
: {}),
|
|
75
|
-
},
|
|
76
|
-
redirect: 'follow',
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
if (assetApiResponse.ok) {
|
|
80
|
-
const content = await assetApiResponse.text()
|
|
81
|
-
return parseChecksums(content)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Fallback: try browser_download_url (only works for public repos)
|
|
85
|
-
if (!process.env.GITHUB_TOKEN) {
|
|
86
|
-
const browserResponse = await fetch(checksumAsset.browser_download_url, {
|
|
87
|
-
headers: { 'User-Agent': 'hostdb-checksums' },
|
|
88
|
-
redirect: 'follow',
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
if (browserResponse.ok) {
|
|
92
|
-
const content = await browserResponse.text()
|
|
93
|
-
return parseChecksums(content)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {}
|
|
98
|
-
}
|
package/lib/databases.ts
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import { parse as parseYaml } from 'yaml'
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const ROOT = join(__dirname, '..')
|
|
8
|
-
|
|
9
|
-
// Canonical platform type
|
|
10
|
-
export type Platform =
|
|
11
|
-
| 'linux-x64'
|
|
12
|
-
| 'linux-arm64'
|
|
13
|
-
| 'darwin-x64'
|
|
14
|
-
| 'darwin-arm64'
|
|
15
|
-
| 'win32-x64'
|
|
16
|
-
|
|
17
|
-
// All supported platforms
|
|
18
|
-
export const ALL_PLATFORMS: Platform[] = [
|
|
19
|
-
'linux-x64',
|
|
20
|
-
'linux-arm64',
|
|
21
|
-
'darwin-x64',
|
|
22
|
-
'darwin-arm64',
|
|
23
|
-
'win32-x64',
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
// Runtime dependency on another database engine
|
|
27
|
-
export type Dependency = {
|
|
28
|
-
database: string
|
|
29
|
-
cascadeDelete: boolean
|
|
30
|
-
note?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// CLI tools configuration
|
|
34
|
-
export type CliTools = {
|
|
35
|
-
server: string | null
|
|
36
|
-
client: string | null
|
|
37
|
-
utilities: string[]
|
|
38
|
-
enhanced: string[]
|
|
39
|
-
note?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Per-platform overrides within a version
|
|
43
|
-
export type PlatformConfig = {
|
|
44
|
-
dependencies?: Dependency[]
|
|
45
|
-
cliTools?: CliTools
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// A platform entry in the version platforms map: true (inherit) or config overrides
|
|
49
|
-
export type PlatformEntry = true | PlatformConfig
|
|
50
|
-
|
|
51
|
-
// Version config with overrides (when version entry is an object)
|
|
52
|
-
export type VersionConfig = {
|
|
53
|
-
enabled?: boolean
|
|
54
|
-
deprecated?: boolean
|
|
55
|
-
note?: string
|
|
56
|
-
dependencies?: Dependency[]
|
|
57
|
-
platforms?: Platform[] | Record<string, PlatformEntry>
|
|
58
|
-
cliTools?: CliTools
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// A version entry is either a simple boolean or a config object
|
|
62
|
-
export type VersionEntry = boolean | VersionConfig
|
|
63
|
-
|
|
64
|
-
// Database entry from databases.json
|
|
65
|
-
export type DatabaseEntry = {
|
|
66
|
-
displayName: string
|
|
67
|
-
description: string
|
|
68
|
-
type: string
|
|
69
|
-
sourceRepo: string
|
|
70
|
-
license: string
|
|
71
|
-
commercialUse: boolean
|
|
72
|
-
hostedServiceAllowed: boolean
|
|
73
|
-
protocol: string | null
|
|
74
|
-
note?: string
|
|
75
|
-
dependencies?: Dependency[]
|
|
76
|
-
spindbStatus: 'completed' | 'in-progress'
|
|
77
|
-
versions: Record<string, VersionEntry>
|
|
78
|
-
platforms: Platform[]
|
|
79
|
-
cliTools: CliTools
|
|
80
|
-
connection: {
|
|
81
|
-
runtime: 'server' | 'embedded'
|
|
82
|
-
defaultPort: number | null
|
|
83
|
-
scheme: string | null
|
|
84
|
-
defaultDatabase: string | null
|
|
85
|
-
defaultUser: string | null
|
|
86
|
-
queryLanguage: string
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// databases.json structure
|
|
91
|
-
export type DatabasesJson = {
|
|
92
|
-
$schema?: string
|
|
93
|
-
_generated?: string
|
|
94
|
-
databases: Record<string, DatabaseEntry>
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Platform asset from releases.json
|
|
98
|
-
export type PlatformAsset = {
|
|
99
|
-
url: string
|
|
100
|
-
sha256: string
|
|
101
|
-
size: number
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Version release from releases.json
|
|
105
|
-
export type VersionRelease = {
|
|
106
|
-
version: string
|
|
107
|
-
releaseTag: string
|
|
108
|
-
releasedAt: string
|
|
109
|
-
deprecated?: boolean
|
|
110
|
-
platforms: Partial<Record<Platform, PlatformAsset>>
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// releases.json structure
|
|
114
|
-
export type ReleasesJson = {
|
|
115
|
-
$schema?: string
|
|
116
|
-
repository: string
|
|
117
|
-
databases: Record<string, Record<string, VersionRelease>>
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function loadDatabasesJson(): DatabasesJson {
|
|
121
|
-
const filePath = join(ROOT, 'databases.json')
|
|
122
|
-
return JSON.parse(readFileSync(filePath, 'utf-8')) as DatabasesJson
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function loadReleasesJson(): ReleasesJson {
|
|
126
|
-
const filePath = join(ROOT, 'releases.json')
|
|
127
|
-
return JSON.parse(readFileSync(filePath, 'utf-8')) as ReleasesJson
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --- Internal helpers ---
|
|
131
|
-
|
|
132
|
-
/** Get the version's platforms field, handling both array and object forms */
|
|
133
|
-
function getVersionPlatformsRaw(versionEntry: VersionConfig): {
|
|
134
|
-
list: Platform[]
|
|
135
|
-
map: Record<string, PlatformEntry> | null
|
|
136
|
-
} {
|
|
137
|
-
if (!versionEntry.platforms) {
|
|
138
|
-
return { list: [], map: null }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (Array.isArray(versionEntry.platforms)) {
|
|
142
|
-
return { list: versionEntry.platforms as Platform[], map: null }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Object form: keys are platforms
|
|
146
|
-
const map = versionEntry.platforms as Record<string, PlatformEntry>
|
|
147
|
-
return { list: Object.keys(map) as Platform[], map }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// --- Resolver helpers ---
|
|
151
|
-
|
|
152
|
-
/** Check if a version entry is enabled */
|
|
153
|
-
export function isVersionEnabled(entry: VersionEntry): boolean {
|
|
154
|
-
if (typeof entry === 'boolean') return entry
|
|
155
|
-
return entry.enabled !== false
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Check if a version entry is deprecated */
|
|
159
|
-
export function isVersionDeprecated(entry: VersionEntry): boolean {
|
|
160
|
-
if (typeof entry === 'boolean') return false
|
|
161
|
-
return entry.deprecated === true
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Get set of enabled version strings for a database */
|
|
165
|
-
export function getEnabledVersions(database: string): Set<string> {
|
|
166
|
-
try {
|
|
167
|
-
const data = loadDatabasesJson()
|
|
168
|
-
const dbEntry = data.databases[database]
|
|
169
|
-
if (!dbEntry) return new Set()
|
|
170
|
-
|
|
171
|
-
return new Set(
|
|
172
|
-
Object.entries(dbEntry.versions)
|
|
173
|
-
.filter(([, entry]) => isVersionEnabled(entry))
|
|
174
|
-
.map(([version]) => version),
|
|
175
|
-
)
|
|
176
|
-
} catch {
|
|
177
|
-
return new Set()
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Get effective platforms for a version (version overrides or engine defaults) */
|
|
182
|
-
export function getVersionPlatforms(
|
|
183
|
-
engine: DatabaseEntry,
|
|
184
|
-
version: string,
|
|
185
|
-
): Platform[] {
|
|
186
|
-
const versionEntry = engine.versions[version]
|
|
187
|
-
if (!versionEntry || typeof versionEntry === 'boolean') {
|
|
188
|
-
return [...engine.platforms]
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const { list } = getVersionPlatformsRaw(versionEntry)
|
|
192
|
-
return list.length > 0 ? [...list] : [...engine.platforms]
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Get effective dependencies for a version+platform.
|
|
197
|
-
*
|
|
198
|
-
* Resolution order (each level fully replaces):
|
|
199
|
-
* 1. Engine-level dependencies
|
|
200
|
-
* 2. Version-level dependencies (if specified)
|
|
201
|
-
* 3. Platform-level dependencies within version (if platforms is object form and platform entry has dependencies)
|
|
202
|
-
*/
|
|
203
|
-
export function getVersionDependencies(
|
|
204
|
-
engine: DatabaseEntry,
|
|
205
|
-
version: string,
|
|
206
|
-
platform?: Platform,
|
|
207
|
-
): Dependency[] {
|
|
208
|
-
const versionEntry = engine.versions[version]
|
|
209
|
-
if (!versionEntry || typeof versionEntry === 'boolean') {
|
|
210
|
-
return engine.dependencies ?? []
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Version-level replaces engine-level
|
|
214
|
-
let resolved = versionEntry.dependencies ?? engine.dependencies ?? []
|
|
215
|
-
|
|
216
|
-
// Platform-level replaces version-level
|
|
217
|
-
if (platform) {
|
|
218
|
-
const { map } = getVersionPlatformsRaw(versionEntry)
|
|
219
|
-
if (map) {
|
|
220
|
-
const platformEntry = map[platform]
|
|
221
|
-
if (
|
|
222
|
-
platformEntry &&
|
|
223
|
-
typeof platformEntry === 'object' &&
|
|
224
|
-
platformEntry.dependencies
|
|
225
|
-
) {
|
|
226
|
-
resolved = platformEntry.dependencies
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return resolved
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Get effective CLI tools for a version+platform.
|
|
236
|
-
*
|
|
237
|
-
* Resolution order (each level fully replaces):
|
|
238
|
-
* 1. Engine-level cliTools
|
|
239
|
-
* 2. Version-level cliTools (if specified)
|
|
240
|
-
* 3. Platform-level cliTools within version (if platforms is object form and platform entry has cliTools)
|
|
241
|
-
*/
|
|
242
|
-
export function getVersionCliTools(
|
|
243
|
-
engine: DatabaseEntry,
|
|
244
|
-
version: string,
|
|
245
|
-
platform?: Platform,
|
|
246
|
-
): CliTools {
|
|
247
|
-
const versionEntry = engine.versions[version]
|
|
248
|
-
if (!versionEntry || typeof versionEntry === 'boolean') {
|
|
249
|
-
return engine.cliTools
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Version-level replaces engine-level
|
|
253
|
-
let resolved = versionEntry.cliTools ?? engine.cliTools
|
|
254
|
-
|
|
255
|
-
// Platform-level replaces version-level
|
|
256
|
-
if (platform) {
|
|
257
|
-
const { map } = getVersionPlatformsRaw(versionEntry)
|
|
258
|
-
if (map) {
|
|
259
|
-
const platformEntry = map[platform]
|
|
260
|
-
if (
|
|
261
|
-
platformEntry &&
|
|
262
|
-
typeof platformEntry === 'object' &&
|
|
263
|
-
platformEntry.cliTools
|
|
264
|
-
) {
|
|
265
|
-
resolved = platformEntry.cliTools
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return resolved
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// --- databases.json generation from databases.yml ---
|
|
274
|
-
|
|
275
|
-
/** Convert a snake_case string to camelCase */
|
|
276
|
-
function snakeToCamel(str: string): string {
|
|
277
|
-
return str.replace(/_([a-z0-9])/g, (_, char: string) => char.toUpperCase())
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Recursively convert all object keys from snake_case to camelCase */
|
|
281
|
-
function transformKeys(obj: unknown): unknown {
|
|
282
|
-
if (Array.isArray(obj)) {
|
|
283
|
-
return obj.map(transformKeys)
|
|
284
|
-
}
|
|
285
|
-
if (obj !== null && typeof obj === 'object') {
|
|
286
|
-
const result: Record<string, unknown> = {}
|
|
287
|
-
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
288
|
-
result[snakeToCamel(key)] = transformKeys(value)
|
|
289
|
-
}
|
|
290
|
-
return result
|
|
291
|
-
}
|
|
292
|
-
return obj
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Generate databases.json from databases.yml
|
|
297
|
-
*
|
|
298
|
-
* @returns true if the file was changed/created (or needs updating in check mode), false if already up-to-date
|
|
299
|
-
*/
|
|
300
|
-
export function generateDatabasesJson(options?: {
|
|
301
|
-
checkOnly?: boolean
|
|
302
|
-
rootDir?: string
|
|
303
|
-
}): boolean {
|
|
304
|
-
const { checkOnly = false, rootDir = ROOT } = options ?? {}
|
|
305
|
-
const yamlPath = join(rootDir, 'databases.yml')
|
|
306
|
-
const jsonPath = join(rootDir, 'databases.json')
|
|
307
|
-
|
|
308
|
-
if (!existsSync(yamlPath)) {
|
|
309
|
-
return false
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const yamlContent = readFileSync(yamlPath, 'utf-8')
|
|
313
|
-
const parsed = parseYaml(yamlContent) as Record<string, unknown>
|
|
314
|
-
|
|
315
|
-
const transformed = transformKeys(parsed) as Record<string, unknown>
|
|
316
|
-
|
|
317
|
-
const output = {
|
|
318
|
-
_generated:
|
|
319
|
-
'DO NOT EDIT. Generated from databases.yml by pnpm prep. Edit databases.yml instead.',
|
|
320
|
-
$schema: './schemas/databases.schema.json',
|
|
321
|
-
...transformed,
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const newJson = JSON.stringify(output, null, 2) + '\n'
|
|
325
|
-
|
|
326
|
-
let currentJson = ''
|
|
327
|
-
if (existsSync(jsonPath)) {
|
|
328
|
-
currentJson = readFileSync(jsonPath, 'utf-8')
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (currentJson === newJson) {
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (checkOnly) {
|
|
336
|
-
return true
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
writeFileSync(jsonPath, newJson)
|
|
340
|
-
return true
|
|
341
|
-
}
|
package/lib/r2.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Cloudflare R2 client utilities.
|
|
3
|
-
*
|
|
4
|
-
* R2 is S3-compatible, so we use the AWS SDK with a custom endpoint.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
S3Client,
|
|
9
|
-
PutObjectCommand,
|
|
10
|
-
HeadObjectCommand,
|
|
11
|
-
DeleteObjectCommand,
|
|
12
|
-
ListObjectsV2Command,
|
|
13
|
-
} from '@aws-sdk/client-s3'
|
|
14
|
-
import { readFileSync } from 'node:fs'
|
|
15
|
-
import { resolve } from 'node:path'
|
|
16
|
-
|
|
17
|
-
export type R2Config = {
|
|
18
|
-
accountId: string
|
|
19
|
-
accessKeyId: string
|
|
20
|
-
secretAccessKey: string
|
|
21
|
-
bucket: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function loadR2Config(): R2Config {
|
|
25
|
-
const accountId = process.env.R2_ACCOUNT_ID
|
|
26
|
-
const accessKeyId = process.env.R2_ACCESS_KEY_ID
|
|
27
|
-
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY
|
|
28
|
-
const bucket = process.env.R2_BUCKET_NAME
|
|
29
|
-
|
|
30
|
-
if (!accountId || !accessKeyId || !secretAccessKey || !bucket) {
|
|
31
|
-
const missing = [
|
|
32
|
-
!accountId && 'R2_ACCOUNT_ID',
|
|
33
|
-
!accessKeyId && 'R2_ACCESS_KEY_ID',
|
|
34
|
-
!secretAccessKey && 'R2_SECRET_ACCESS_KEY',
|
|
35
|
-
!bucket && 'R2_BUCKET_NAME',
|
|
36
|
-
].filter(Boolean)
|
|
37
|
-
throw new Error(`Missing required R2 env vars: ${missing.join(', ')}`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { accountId, accessKeyId, secretAccessKey, bucket }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function createR2Client(config: R2Config): S3Client {
|
|
44
|
-
return new S3Client({
|
|
45
|
-
region: 'auto',
|
|
46
|
-
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
47
|
-
credentials: {
|
|
48
|
-
accessKeyId: config.accessKeyId,
|
|
49
|
-
secretAccessKey: config.secretAccessKey,
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function uploadToR2(options: {
|
|
55
|
-
client: S3Client
|
|
56
|
-
bucket: string
|
|
57
|
-
key: string
|
|
58
|
-
body: Buffer | Uint8Array | ReadableStream
|
|
59
|
-
contentType?: string
|
|
60
|
-
cacheControl?: string
|
|
61
|
-
}): Promise<void> {
|
|
62
|
-
const { client, bucket, key, body, contentType, cacheControl } = options
|
|
63
|
-
|
|
64
|
-
await client.send(
|
|
65
|
-
new PutObjectCommand({
|
|
66
|
-
Bucket: bucket,
|
|
67
|
-
Key: key,
|
|
68
|
-
Body: body,
|
|
69
|
-
ContentType: contentType ?? 'application/octet-stream',
|
|
70
|
-
CacheControl: cacheControl ?? 'public, max-age=31536000, immutable',
|
|
71
|
-
}),
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function objectExists(options: {
|
|
76
|
-
client: S3Client
|
|
77
|
-
bucket: string
|
|
78
|
-
key: string
|
|
79
|
-
}): Promise<boolean> {
|
|
80
|
-
const { client, bucket, key } = options
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
await client.send(
|
|
84
|
-
new HeadObjectCommand({
|
|
85
|
-
Bucket: bucket,
|
|
86
|
-
Key: key,
|
|
87
|
-
}),
|
|
88
|
-
)
|
|
89
|
-
return true
|
|
90
|
-
} catch (error: unknown) {
|
|
91
|
-
const code =
|
|
92
|
-
error instanceof Error && 'name' in error ? error.name : undefined
|
|
93
|
-
if (code === 'NotFound' || code === '404') {
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
throw error
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function deleteFromR2(options: {
|
|
101
|
-
client: S3Client
|
|
102
|
-
bucket: string
|
|
103
|
-
key: string
|
|
104
|
-
}): Promise<void> {
|
|
105
|
-
const { client, bucket, key } = options
|
|
106
|
-
|
|
107
|
-
await client.send(
|
|
108
|
-
new DeleteObjectCommand({
|
|
109
|
-
Bucket: bucket,
|
|
110
|
-
Key: key,
|
|
111
|
-
}),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function listR2Objects(options: {
|
|
116
|
-
client: S3Client
|
|
117
|
-
bucket: string
|
|
118
|
-
prefix: string
|
|
119
|
-
}): Promise<string[]> {
|
|
120
|
-
const { client, bucket, prefix } = options
|
|
121
|
-
|
|
122
|
-
const result = await client.send(
|
|
123
|
-
new ListObjectsV2Command({
|
|
124
|
-
Bucket: bucket,
|
|
125
|
-
Prefix: prefix,
|
|
126
|
-
}),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
return (result.Contents ?? [])
|
|
130
|
-
.map((obj) => obj.Key)
|
|
131
|
-
.filter((key): key is string => key !== undefined)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export async function publishJsonToR2(options: {
|
|
135
|
-
client: S3Client
|
|
136
|
-
bucket: string
|
|
137
|
-
rootDir: string
|
|
138
|
-
filename: string
|
|
139
|
-
cacheControl?: string
|
|
140
|
-
}): Promise<void> {
|
|
141
|
-
const { client, bucket, rootDir, filename, cacheControl } = options
|
|
142
|
-
const body = readFileSync(resolve(rootDir, filename))
|
|
143
|
-
|
|
144
|
-
await uploadToR2({
|
|
145
|
-
client,
|
|
146
|
-
bucket,
|
|
147
|
-
key: filename,
|
|
148
|
-
body,
|
|
149
|
-
contentType: 'application/json',
|
|
150
|
-
cacheControl: cacheControl ?? 'public, max-age=60',
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function publishReleasesJson(options: {
|
|
155
|
-
client: S3Client
|
|
156
|
-
bucket: string
|
|
157
|
-
rootDir: string
|
|
158
|
-
}): Promise<void> {
|
|
159
|
-
await publishJsonToR2({ ...options, filename: 'releases.json' })
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Purge Cloudflare CDN cache for specific URLs.
|
|
164
|
-
*
|
|
165
|
-
* Required env vars: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID
|
|
166
|
-
* If not set, logs a warning and returns (no-op).
|
|
167
|
-
*/
|
|
168
|
-
export async function purgeCloudflareCache(
|
|
169
|
-
urls: string[],
|
|
170
|
-
): Promise<{ purged: boolean; count: number }> {
|
|
171
|
-
const apiToken = process.env.CLOUDFLARE_API_TOKEN
|
|
172
|
-
const zoneId = process.env.CLOUDFLARE_ZONE_ID
|
|
173
|
-
|
|
174
|
-
if (!apiToken || !zoneId) {
|
|
175
|
-
return { purged: false, count: 0 }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Cloudflare allows max 30 URLs per purge request
|
|
179
|
-
const BATCH_SIZE = 30
|
|
180
|
-
let totalPurged = 0
|
|
181
|
-
|
|
182
|
-
for (let i = 0; i < urls.length; i += BATCH_SIZE) {
|
|
183
|
-
const batch = urls.slice(i, i + BATCH_SIZE)
|
|
184
|
-
|
|
185
|
-
const response = await fetch(
|
|
186
|
-
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
|
187
|
-
{
|
|
188
|
-
method: 'POST',
|
|
189
|
-
headers: {
|
|
190
|
-
Authorization: `Bearer ${apiToken}`,
|
|
191
|
-
'Content-Type': 'application/json',
|
|
192
|
-
},
|
|
193
|
-
body: JSON.stringify({ files: batch }),
|
|
194
|
-
},
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
if (!response.ok) {
|
|
198
|
-
const body = await response.text()
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Cloudflare cache purge failed: ${response.status} ${body}`,
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
totalPurged += batch.length
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return { purged: true, count: totalPurged }
|
|
208
|
-
}
|
package/lib/registry.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configurable registry base URL for binary downloads.
|
|
3
|
-
*
|
|
4
|
-
* Switch between providers by changing REGISTRY_BASE_URL:
|
|
5
|
-
* R2: https://registry.layerbase.host
|
|
6
|
-
* GitHub: https://github.com/robertjbass/hostdb/releases/download
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const REGISTRY_BASE_URL = 'https://registry.layerbase.host'
|
|
10
|
-
|
|
11
|
-
export function getDownloadUrl(tag: string, filename: string): string {
|
|
12
|
-
return `${REGISTRY_BASE_URL}/${tag}/${filename}`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getRegistryBaseUrl(): string {
|
|
16
|
-
return REGISTRY_BASE_URL
|
|
17
|
-
}
|