import type { Database } from "bun:sqlite"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Hono } from "hono"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { createApp } from "../src/server/app.ts"; import type { Config } from "../src/server/config.ts"; import { openKeprDb } from "../src/server/db.ts"; const ORIGIN = "https://kepr.example"; export const PASSWORD = "correct horse battery staple"; // The env Bun.serve passes: its requestIP gives the socket peer. const peer = (address: string) => ({ requestIP: () => ({ address }) }); /** A browser against one app: a cookie jar, and the form token of the last admin page read. */ export class Browser { cookies = new Map(); form = ""; constructor( readonly app: Hono, readonly address = "203.0.113.5", ) {} async request(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); headers.set("cookie", [...this.cookies].map(([k, v]) => `${k}=${v}`).join("; ")); const response = await this.app.request(path, { ...init, headers }, peer(this.address)); for (const line of response.headers.getSetCookie()) { const [pair = "", ...attributes] = line.split(";"); const [name = "", value = ""] = pair.split("="); if (attributes.some((a) => /max-age=0/i.test(a.trim())) || value === "") { this.cookies.delete(name); } else this.cookies.set(name, value); } return response; } async get(path: string): Promise { const response = await this.request(path); const text = await response.clone().text(); this.form = /name="form" value="([^"]+)"/.exec(text)?.[1] ?? this.form; return response; } post(path: string, fields: Record | FormData, origin = ORIGIN) { const body = fields instanceof FormData ? fields : new URLSearchParams(fields); if (!(fields instanceof FormData) && !("form" in fields)) body.set("form", this.form); else if (fields instanceof FormData && !fields.has("form")) body.set("form", this.form); return this.request(path, { method: "POST", headers: origin ? { origin } : {}, body }); } async signIn(password = PASSWORD, code = ""): Promise { await this.get("/sign-in"); const response = await this.post("/sign-in", { password, ...(code ? { code } : {}) }); if (response.status === 303) await this.get("/admin"); return response; } } export type Instance = { app: Hono; kepr: Database; dataDir: string; config: Config }; export async function instance(bucketDir: string, dataDir?: string): Promise { const dir = dataDir ?? (await mkdtemp(join(tmpdir(), "kepr-admin-"))); const kepr = openKeprDb(dir); const bucket = { kind: "folder" as const, path: bucketDir }; await syncAll({ bucket: openBucket(bucket), kepr, mirrorDir: join(dir, "mirror"), }); const config: Config = { domain: "kepr.example", dataDir: dir, adminPasswordHash: await Bun.password.hash(PASSWORD, "argon2id"), bucket, }; return { app: createApp(config, kepr), kepr, dataDir: dir, config }; }