d9c345d0eac2

scaffold kepr 3 on bun with strict typescript, biome and validated kepr.toml loading (v3.0.1)

time
2026-09-26 16:09 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
kepr 3 implementation
parent
none, the first save
8 files .kohignore+35 −0biome.json+23 −0bun.lock+204 −0kepr.example.toml+18 −0package.json+24 −0src/server/config.ts+83 −0tests/config.test.ts+53 −0tsconfig.json+19 −0
.kohignore+35 −0
@@ -0,0 +1,35 @@1+# koh ignore rules (.kohignore)2+# Generated by koh init. Edit to fit the project.3+4+# Patterns are matched against repo-relative paths. Suffix `/` restricts5+# to directories. Leading `/` anchors to the repo root. A pattern like6+# `*.log` matches any file with that extension anywhere in the tree.7+#8+# koh already ignores .koh, .koh-wal, .koh-shm, .koh-v2/ and .git/ automatically.9+10+# Build output11+zig-out/12+.zig-cache/13+zig-cache/14+target/15+build/16+dist/17+out/18+19+# Dependencies20+node_modules/21+22+# OS noise23+.DS_Store24+Thumbs.db25+26+# Logs27+*.log28+29+# Koh artifacts30+*.bundle31+*.koh.offer32+33+# Kepr runtime data and local config (holds secrets)34+data/35+kepr.toml

view file

biome.json+23 −0
@@ -0,0 +1,23 @@1+{2+	"$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",3+	"files": {4+		"ignoreUnknown": true,5+		"includes": ["**", "!data", "!node_modules"]6+	},7+	"formatter": {8+		"indentStyle": "tab",9+		"lineWidth": 10010+	},11+	"linter": {12+		"rules": {13+			"preset": "recommended"14+		}15+	},16+	"assist": {17+		"actions": {18+			"source": {19+				"organizeImports": "on"20+			}21+		}22+	}23+}

view file

bun.lock+204 −0
@@ -0,0 +1,204 @@1+{2+  "lockfileVersion": 1,3+  "configVersion": 1,4+  "workspaces": {5+    "": {6+      "name": "kepr",7+      "dependencies": {8+        "@noble/hashes": "^2.4.0",9+        "hono": "^4.13.9",10+        "markdown-it": "^15.0.2",11+        "shiki": "^4.4.3",12+      },13+      "devDependencies": {14+        "@biomejs/biome": "^2.5.14",15+        "@types/bun": "^1.4.2",16+        "@types/markdown-it": "^14.2.0",17+        "typescript": "^7.0.2",18+      },19+    },20+  },21+  "packages": {22+    "@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="],23+24+    "@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="],25+26+    "@biomejs/cli-darwin-x64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="],27+28+    "@biomejs/cli-linux-arm64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="],29+30+    "@biomejs/cli-linux-arm64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="],31+32+    "@biomejs/cli-linux-x64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="],33+34+    "@biomejs/cli-linux-x64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="],35+36+    "@biomejs/cli-win32-arm64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="],37+38+    "@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="],39+40+    "@noble/hashes": ["@noble/[email protected]", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="],41+42+    "@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],43+44+    "@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],45+46+    "@shikijs/engine-oniguruma": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],47+48+    "@shikijs/langs": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],49+50+    "@shikijs/primitive": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],51+52+    "@shikijs/themes": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],53+54+    "@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],55+56+    "@shikijs/vscode-textmate": ["@shikijs/[email protected]", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],57+58+    "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],59+60+    "@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],61+62+    "@types/linkify-it": ["@types/[email protected]", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],63+64+    "@types/markdown-it": ["@types/[email protected]", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ=="],65+66+    "@types/mdast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],67+68+    "@types/mdurl": ["@types/[email protected]", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],69+70+    "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="],71+72+    "@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],73+74+    "@typescript/typescript-aix-ppc64": ["@typescript/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],75+76+    "@typescript/typescript-darwin-arm64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],77+78+    "@typescript/typescript-darwin-x64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],79+80+    "@typescript/typescript-freebsd-arm64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],81+82+    "@typescript/typescript-freebsd-x64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],83+84+    "@typescript/typescript-linux-arm": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],85+86+    "@typescript/typescript-linux-arm64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],87+88+    "@typescript/typescript-linux-loong64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],89+90+    "@typescript/typescript-linux-mips64el": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],91+92+    "@typescript/typescript-linux-ppc64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],93+94+    "@typescript/typescript-linux-riscv64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],95+96+    "@typescript/typescript-linux-s390x": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],97+98+    "@typescript/typescript-linux-x64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],99+100+    "@typescript/typescript-netbsd-arm64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],101+102+    "@typescript/typescript-netbsd-x64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],103+104+    "@typescript/typescript-openbsd-arm64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],105+106+    "@typescript/typescript-openbsd-x64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],107+108+    "@typescript/typescript-sunos-x64": ["@typescript/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],109+110+    "@typescript/typescript-win32-arm64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],111+112+    "@typescript/typescript-win32-x64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],113+114+    "@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],115+116+    "argparse": ["[email protected]", "", {}, "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ=="],117+118+    "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],119+120+    "ccount": ["[email protected]", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],121+122+    "character-entities-html4": ["[email protected]", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],123+124+    "character-entities-legacy": ["[email protected]", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],125+126+    "comma-separated-tokens": ["[email protected]", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],127+128+    "dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],129+130+    "devlop": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],131+132+    "entities": ["[email protected]", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="],133+134+    "hast-util-to-html": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],135+136+    "hast-util-whitespace": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],137+138+    "hono": ["[email protected]", "", {}, "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg=="],139+140+    "html-void-elements": ["[email protected]", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],141+142+    "linkify-it": ["[email protected]", "", { "dependencies": { "uc.micro": "^3.0.0" } }, "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw=="],143+144+    "markdown-it": ["[email protected]", "", { "dependencies": { "argparse": "^3.0.0", "entities": "^8.0.0", "linkify-it": "^6.0.0", "mdurl": "^2.1.0", "punycode.js": "^2.3.1", "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw=="],145+146+    "mdast-util-to-hast": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],147+148+    "mdurl": ["[email protected]", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="],149+150+    "micromark-util-character": ["[email protected]", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],151+152+    "micromark-util-encode": ["[email protected]", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],153+154+    "micromark-util-sanitize-uri": ["[email protected]", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],155+156+    "micromark-util-symbol": ["[email protected]", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],157+158+    "micromark-util-types": ["[email protected]", "", {}, "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg=="],159+160+    "oniguruma-parser": ["[email protected]", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],161+162+    "oniguruma-to-es": ["[email protected]", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],163+164+    "property-information": ["[email protected]", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],165+166+    "punycode.js": ["[email protected]", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],167+168+    "regex": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],169+170+    "regex-recursion": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],171+172+    "regex-utilities": ["[email protected]", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],173+174+    "shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],175+176+    "space-separated-tokens": ["[email protected]", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],177+178+    "stringify-entities": ["[email protected]", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],179+180+    "trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],181+182+    "typescript": ["[email protected]", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],183+184+    "uc.micro": ["[email protected]", "", {}, "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw=="],185+186+    "undici-types": ["[email protected]", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],187+188+    "unist-util-is": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],189+190+    "unist-util-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],191+192+    "unist-util-stringify-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],193+194+    "unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],195+196+    "unist-util-visit-parents": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],197+198+    "vfile": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],199+200+    "vfile-message": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],201+202+    "zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],203+  }204+}

view file

kepr.example.toml+18 −0
@@ -0,0 +1,18 @@1+# Copy to kepr.toml (never saved: it holds the admin password hash and the bucket key).2+3+domain = "kepr.example"4+data_dir = "data"5+# bun -e 'console.log(await Bun.password.hash(prompt("password:"), "argon2id"))'6+admin_password_hash = "$argon2id$..."7+8+[bucket]9+endpoint = "https://s3.fr-par.scw.cloud"10+name = "my-koh-bucket"11+region = "fr-par"12+# A read-only key: Kepr never writes to the bucket.13+access_key_id = "SCW..."14+secret_access_key = "..."15+16+# For development and tests, read a Koh 3 folder remote instead of S3.17+# Replace the five S3 keys above with:18+# folder = "/path/to/koh/folder/remote"

view file

package.json+24 −0
@@ -0,0 +1,24 @@1+{2+	"name": "kepr",3+	"version": "3.0.1",4+	"private": true,5+	"type": "module",6+	"scripts": {7+		"start": "bun run src/main.ts",8+		"dev": "bun --watch run src/main.ts",9+		"test": "bun test",10+		"budget": "bun run scripts/budget.ts"11+	},12+	"dependencies": {13+		"@noble/hashes": "^2.4.0",14+		"hono": "^4.13.9",15+		"markdown-it": "^15.0.2",16+		"shiki": "^4.4.3"17+	},18+	"devDependencies": {19+		"@biomejs/biome": "^2.5.14",20+		"@types/bun": "^1.4.2",21+		"@types/markdown-it": "^14.2.0",22+		"typescript": "^7.0.2"23+	}24+}

view file

src/server/config.ts+83 −0
@@ -0,0 +1,83 @@1+export type BucketSource =2+	| {3+			kind: "s3";4+			endpoint: string;5+			name: string;6+			region: string;7+			accessKeyId: string;8+			secretAccessKey: string;9+	  }10+	// A directory laid out exactly like the bucket (Koh 3's folder remote), for development and tests.11+	| { kind: "folder"; path: string };12+13+export type Config = {14+	domain: string;15+	dataDir: string;16+	adminPasswordHash: string;17+	bucket: BucketSource;18+};19+20+export class ConfigError extends Error {}21+22+export function parseConfig(text: string): Config {23+	let raw: unknown;24+	try {25+		raw = Bun.TOML.parse(text);26+	} catch (cause) {27+		throw new ConfigError(`kepr.toml is not valid TOML: ${(cause as Error).message}`);28+	}29+	const top = asTable(raw, "kepr.toml");30+	const bucket = asTable(top.bucket, "[bucket]");31+	const adminPasswordHash = requireString(top, "admin_password_hash");32+	if (!adminPasswordHash.startsWith("$argon2id$")) {33+		throw new ConfigError("admin_password_hash must be an Argon2id hash ($argon2id$...)");34+	}35+	return {36+		domain: requireString(top, "domain"),37+		dataDir: requireString(top, "data_dir"),38+		adminPasswordHash,39+		bucket: parseBucket(bucket),40+	};41+}42+43+export async function loadConfig(path: string): Promise<Config> {44+	const file = Bun.file(path);45+	if (!(await file.exists())) throw new ConfigError(`${path} not found`);46+	return parseConfig(await file.text());47+}48+49+function parseBucket(bucket: Record<string, unknown>): BucketSource {50+	if (bucket.folder !== undefined) {51+		const s3Keys = ["endpoint", "name", "region", "access_key_id", "secret_access_key"];52+		const mixed = s3Keys.filter((key) => bucket[key] !== undefined);53+		if (mixed.length > 0) {54+			throw new ConfigError(`[bucket] folder cannot be combined with ${mixed.join(", ")}`);55+		}56+		return { kind: "folder", path: requireString(bucket, "folder", "[bucket] ") };57+	}58+	const endpoint = requireString(bucket, "endpoint", "[bucket] ");59+	if (!URL.canParse(endpoint)) throw new ConfigError("[bucket] endpoint must be a URL");60+	return {61+		kind: "s3",62+		endpoint,63+		name: requireString(bucket, "name", "[bucket] "),64+		region: requireString(bucket, "region", "[bucket] "),65+		accessKeyId: requireString(bucket, "access_key_id", "[bucket] "),66+		secretAccessKey: requireString(bucket, "secret_access_key", "[bucket] "),67+	};68+}69+70+function asTable(value: unknown, name: string): Record<string, unknown> {71+	if (typeof value !== "object" || value === null || Array.isArray(value)) {72+		throw new ConfigError(`${name} is missing or not a table`);73+	}74+	return value as Record<string, unknown>;75+}76+77+function requireString(table: Record<string, unknown>, key: string, section = ""): string {78+	const value = table[key];79+	if (typeof value !== "string" || value.trim() === "") {80+		throw new ConfigError(`${section}${key} is required and must be a non-empty string`);81+	}82+	return value;83+}

view file

tests/config.test.ts+53 −0
@@ -0,0 +1,53 @@1+import { expect, test } from "bun:test";2+import { parseConfig } from "../src/server/config.ts";3+4+const hash = "$argon2id$v=19$m=65536,t=2,p=1$c2FsdA$aGFzaA";5+const s3 = `6+domain = "kepr.example"7+data_dir = "data"8+admin_password_hash = "${hash}"9+10+[bucket]11+endpoint = "https://s3.fr-par.scw.cloud"12+name = "koh"13+region = "fr-par"14+access_key_id = "SCWREAD"15+secret_access_key = "secret"16+`;17+18+test("parses the §19 keys with an S3 bucket", () => {19+	expect(parseConfig(s3)).toEqual({20+		domain: "kepr.example",21+		dataDir: "data",22+		adminPasswordHash: hash,23+		bucket: {24+			kind: "s3",25+			endpoint: "https://s3.fr-par.scw.cloud",26+			name: "koh",27+			region: "fr-par",28+			accessKeyId: "SCWREAD",29+			secretAccessKey: "secret",30+		},31+	});32+});33+34+test("parses a folder source", () => {35+	const text = `domain = "d"\ndata_dir = "data"\nadmin_password_hash = "${hash}"\n[bucket]\nfolder = "/tmp/b"\n`;36+	expect(parseConfig(text).bucket).toEqual({ kind: "folder", path: "/tmp/b" });37+});38+39+test("fails loudly, naming the problem", () => {40+	expect(() => parseConfig("domain = ")).toThrow("not valid TOML");41+	expect(() => parseConfig(s3.replace('domain = "kepr.example"', ""))).toThrow(42+		"domain is required",43+	);44+	expect(() => parseConfig(s3.replace(/\[bucket\][\s\S]*/, ""))).toThrow("[bucket] is missing");45+	expect(() => parseConfig(s3.replace('region = "fr-par"', ""))).toThrow(46+		"[bucket] region is required",47+	);48+	expect(() => parseConfig(s3.replace(hash, "plaintext"))).toThrow("Argon2id");49+	expect(() => parseConfig(`${s3}folder = "/tmp/b"\n`)).toThrow("cannot be combined");50+	expect(() => parseConfig(s3.replace("https://s3.fr-par.scw.cloud", "nope"))).toThrow(51+		"must be a URL",52+	);53+});

view file

tsconfig.json+19 −0
@@ -0,0 +1,19 @@1+{2+	"compilerOptions": {3+		"lib": ["ESNext"],4+		"target": "ESNext",5+		"module": "ESNext",6+		"moduleResolution": "bundler",7+		"moduleDetection": "force",8+		"types": ["bun"],9+		"strict": true,10+		"noUnusedLocals": true,11+		"noUnusedParameters": true,12+		"noFallthroughCasesInSwitch": true,13+		"verbatimModuleSyntax": true,14+		"allowImportingTsExtensions": true,15+		"skipLibCheck": true,16+		"noEmit": true17+	},18+	"include": ["src", "scripts", "tests"]19+}

view file