From 1e4359aab6aa32a784887434f3e992080346ae87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Sat, 18 Apr 2026 05:21:39 +0200 Subject: [PATCH] publish(task 2): config-loader mit env-validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadConfig() liest 3 pflicht-keys (BUNKER_URL, AUTHOR_PUBKEY_HEX, BOOTSTRAP_RELAY) und 3 optionale mit defaults (CONTENT_ROOT, CLIENT_TAG=leer, MIN_RELAY_ACKS=2). pubkey-hex-format validiert (64 lowercase hex), MIN_RELAY_ACKS als positive integer. 6 tests, alle grün. Co-Authored-By: Claude Opus 4.6 (1M context) --- publish/src/core/config.ts | 47 +++++++++++++++++++++++++++++ publish/tests/config_test.ts | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 publish/src/core/config.ts create mode 100644 publish/tests/config_test.ts diff --git a/publish/src/core/config.ts b/publish/src/core/config.ts new file mode 100644 index 0000000..1439fec --- /dev/null +++ b/publish/src/core/config.ts @@ -0,0 +1,47 @@ +export interface Config { + bunkerUrl: string + authorPubkeyHex: string + bootstrapRelay: string + contentRoot: string + clientTag: string + minRelayAcks: number +} + +type EnvReader = (key: string) => string | undefined + +const REQUIRED = ['BUNKER_URL', 'AUTHOR_PUBKEY_HEX', 'BOOTSTRAP_RELAY'] as const + +const DEFAULTS = { + CONTENT_ROOT: '../content/posts', + CLIENT_TAG: '', + MIN_RELAY_ACKS: '2', +} + +export function loadConfig(read: EnvReader = (k) => Deno.env.get(k)): Config { + const missing: string[] = [] + const values: Record = {} + for (const key of REQUIRED) { + const v = read(key) + if (!v) missing.push(key) + else values[key] = v + } + if (missing.length) { + throw new Error(`Missing env: ${missing.join(', ')}`) + } + if (!/^[0-9a-f]{64}$/.test(values.AUTHOR_PUBKEY_HEX)) { + throw new Error('AUTHOR_PUBKEY_HEX must be 64 lowercase hex characters') + } + const minAcksRaw = read('MIN_RELAY_ACKS') ?? DEFAULTS.MIN_RELAY_ACKS + const minAcks = Number(minAcksRaw) + if (!Number.isInteger(minAcks) || minAcks < 1) { + throw new Error(`MIN_RELAY_ACKS must be a positive integer, got "${minAcksRaw}"`) + } + return { + bunkerUrl: values.BUNKER_URL, + authorPubkeyHex: values.AUTHOR_PUBKEY_HEX, + bootstrapRelay: values.BOOTSTRAP_RELAY, + contentRoot: read('CONTENT_ROOT') ?? DEFAULTS.CONTENT_ROOT, + clientTag: read('CLIENT_TAG') ?? DEFAULTS.CLIENT_TAG, + minRelayAcks: minAcks, + } +} diff --git a/publish/tests/config_test.ts b/publish/tests/config_test.ts new file mode 100644 index 0000000..22ff69d --- /dev/null +++ b/publish/tests/config_test.ts @@ -0,0 +1,57 @@ +import { assertEquals, assertThrows } from '@std/assert' +import { loadConfig } from '../src/core/config.ts' + +const REQUIRED = { + BUNKER_URL: 'bunker://abc?relay=wss://r.example&secret=s', + AUTHOR_PUBKEY_HEX: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41', + BOOTSTRAP_RELAY: 'wss://relay.damus.io', +} + +Deno.test('loadConfig: liest alle pflicht-keys aus env', () => { + const cfg = loadConfig((k) => REQUIRED[k as keyof typeof REQUIRED]) + assertEquals(cfg.bunkerUrl, REQUIRED.BUNKER_URL) + assertEquals(cfg.authorPubkeyHex, REQUIRED.AUTHOR_PUBKEY_HEX) + assertEquals(cfg.bootstrapRelay, REQUIRED.BOOTSTRAP_RELAY) +}) + +Deno.test('loadConfig: liefert defaults für optionale keys', () => { + const cfg = loadConfig((k) => REQUIRED[k as keyof typeof REQUIRED]) + assertEquals(cfg.contentRoot, '../content/posts') + assertEquals(cfg.clientTag, '') + assertEquals(cfg.minRelayAcks, 2) +}) + +Deno.test('loadConfig: optionale keys können überschrieben werden', () => { + const env = { + ...REQUIRED, + CONTENT_ROOT: '../blog', + CLIENT_TAG: 'my-site', + MIN_RELAY_ACKS: '3', + } + const cfg = loadConfig((k) => env[k as keyof typeof env]) + assertEquals(cfg.contentRoot, '../blog') + assertEquals(cfg.clientTag, 'my-site') + assertEquals(cfg.minRelayAcks, 3) +}) + +Deno.test('loadConfig: wirft bei fehlender pflicht-variable', () => { + assertThrows(() => loadConfig(() => undefined), Error, 'BUNKER_URL') +}) + +Deno.test('loadConfig: validiert pubkey-format (64 hex)', () => { + const env = { ...REQUIRED, AUTHOR_PUBKEY_HEX: 'zzz' } + assertThrows( + () => loadConfig((k) => env[k as keyof typeof env]), + Error, + 'AUTHOR_PUBKEY_HEX', + ) +}) + +Deno.test('loadConfig: MIN_RELAY_ACKS muss positiv sein', () => { + const env = { ...REQUIRED, MIN_RELAY_ACKS: '0' } + assertThrows( + () => loadConfig((k) => env[k as keyof typeof env]), + Error, + 'MIN_RELAY_ACKS', + ) +})