hostdb 0.29.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/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
- }