1import type { Database } from "bun:sqlite";2import { mkdtemp } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import type { Hono } from "hono";6import { openBucket } from "../src/bucket/reader.ts";7import { syncAll } from "../src/bucket/sync.ts";8import { createApp } from "../src/server/app.ts";9import type { Config } from "../src/server/config.ts";10import { openKeprDb } from "../src/server/db.ts";1112const ORIGIN = "https://kepr.example";13export const PASSWORD = "correct horse battery staple";1415const peer = (address: string) => ({ requestIP: () => ({ address }) });161718export class Browser {19 cookies = new Map<string, string>();20 form = "";21 constructor(22 readonly app: Hono,23 readonly address = "203.0.113.5",24 ) {}2526 async request(path: string, init: RequestInit = {}): Promise<Response> {27 const headers = new Headers(init.headers);28 headers.set("cookie", [...this.cookies].map(([k, v]) => `${k}=${v}`).join("; "));29 const response = await this.app.request(path, { ...init, headers }, peer(this.address));30 for (const line of response.headers.getSetCookie()) {31 const [pair = "", ...attributes] = line.split(";");32 const [name = "", value = ""] = pair.split("=");33 if (attributes.some((a) => /max-age=0/i.test(a.trim())) || value === "") {34 this.cookies.delete(name);35 } else this.cookies.set(name, value);36 }37 return response;38 }3940 async get(path: string): Promise<Response> {41 const response = await this.request(path);42 const text = await response.clone().text();43 this.form = /name="form" value="([^"]+)"/.exec(text)?.[1] ?? this.form;44 return response;45 }4647 post(path: string, fields: Record<string, string> | FormData, origin = ORIGIN) {48 const body = fields instanceof FormData ? fields : new URLSearchParams(fields);49 if (!(fields instanceof FormData) && !("form" in fields)) body.set("form", this.form);50 else if (fields instanceof FormData && !fields.has("form")) body.set("form", this.form);51 return this.request(path, { method: "POST", headers: origin ? { origin } : {}, body });52 }5354 async signIn(password = PASSWORD): Promise<Response> {55 await this.get("/sign-in");56 const response = await this.post("/sign-in", { password });57 if (response.status === 303) await this.get("/admin");58 return response;59 }60}6162export type Instance = { app: Hono; kepr: Database; dataDir: string; config: Config };6364export async function instance(bucketDir: string, dataDir?: string): Promise<Instance> {65 const dir = dataDir ?? (await mkdtemp(join(tmpdir(), "kepr-admin-")));66 const kepr = openKeprDb(dir);67 const bucket = { kind: "folder" as const, path: bucketDir };68 await syncAll({69 bucket: openBucket(bucket),70 kepr,71 mirrorDir: join(dir, "mirror"),72 });73 const config: Config = {74 domain: "kepr.example",75 dataDir: dir,76 adminPasswordHash: await Bun.password.hash(PASSWORD, "argon2id"),77 bucket,78 };79 return { app: createApp(config, kepr), kepr, dataDir: dir, config };80}