feat: (BREAKING!) migrate hono #19
					 23 changed files with 1042 additions and 1113 deletions
				
			
		| 
						 | 
					@ -17,7 +17,7 @@
 | 
				
			||||||
    "typescript": "^5.4.3"
 | 
					    "typescript": "^5.4.3"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "type": "module",
 | 
					  "type": "module",
 | 
				
			||||||
  "dependencies": {
 | 
					  "peerDependencies": {
 | 
				
			||||||
    "zod": "^3.23.8"
 | 
					    "zod": "^4.1.12"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,12 @@
 | 
				
			||||||
import { z } from "zod";
 | 
					import { z } from "zod";
 | 
				
			||||||
export { ZodError } from "zod";
 | 
					export { ZodError } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PERMISSIONS = ["ModifyTag", "QueryContent", "ModifyTagDesc"] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PermissionNameSchema = z.enum(PERMISSIONS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type PermissionName = z.infer<typeof PermissionNameSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DocumentBodySchema = z.object({
 | 
					export const DocumentBodySchema = z.object({
 | 
				
			||||||
	title: z.string(),
 | 
						title: z.string(),
 | 
				
			||||||
	content_type: z.string(),
 | 
						content_type: z.string(),
 | 
				
			||||||
| 
						 | 
					@ -8,7 +14,7 @@ export const DocumentBodySchema = z.object({
 | 
				
			||||||
	filename: z.string(),
 | 
						filename: z.string(),
 | 
				
			||||||
	modified_at: z.number(),
 | 
						modified_at: z.number(),
 | 
				
			||||||
	content_hash: z.string(),
 | 
						content_hash: z.string(),
 | 
				
			||||||
	additional: z.record(z.unknown()),
 | 
						additional: z.record(z.string(), z.unknown()),
 | 
				
			||||||
	tags: z.array(z.string()),
 | 
						tags: z.array(z.string()),
 | 
				
			||||||
	pagenum: z.number().int(),
 | 
						pagenum: z.number().int(),
 | 
				
			||||||
	gid: z.number().nullable(),
 | 
						gid: z.number().nullable(),
 | 
				
			||||||
| 
						 | 
					@ -40,7 +46,7 @@ export type TagRelation = z.infer<typeof TagRelationSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PermissionSchema = z.object({
 | 
					export const PermissionSchema = z.object({
 | 
				
			||||||
	username: z.string(),
 | 
						username: z.string(),
 | 
				
			||||||
	name: z.string(),
 | 
						name: PermissionNameSchema,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Permission = z.infer<typeof PermissionSchema>;
 | 
					export type Permission = z.infer<typeof PermissionSchema>;
 | 
				
			||||||
| 
						 | 
					@ -103,7 +109,7 @@ export const ServerPersistedSettingSchema = z.object({
 | 
				
			||||||
	secure: z.boolean(),
 | 
						secure: z.boolean(),
 | 
				
			||||||
	cli: z.boolean(),
 | 
						cli: z.boolean(),
 | 
				
			||||||
	forbid_remote_admin_login: z.boolean(),
 | 
						forbid_remote_admin_login: z.boolean(),
 | 
				
			||||||
	guest: z.array(z.string()),
 | 
						guest: z.array(PermissionNameSchema),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ServerPersistedSetting = z.infer<typeof ServerPersistedSettingSchema>;
 | 
					export type ServerPersistedSetting = z.infer<typeof ServerPersistedSettingSchema>;
 | 
				
			||||||
| 
						 | 
					@ -115,7 +121,7 @@ export const ServerSettingResponseSchema = z.object({
 | 
				
			||||||
		mode: z.enum(["development", "production"]),
 | 
							mode: z.enum(["development", "production"]),
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
	persisted: ServerPersistedSettingSchema,
 | 
						persisted: ServerPersistedSettingSchema,
 | 
				
			||||||
	permissions: z.array(z.string()),
 | 
						permissions: z.array(PermissionNameSchema),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ServerSettingResponse = z.infer<typeof ServerSettingResponseSchema>;
 | 
					export type ServerSettingResponse = z.infer<typeof ServerSettingResponseSchema>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,21 +14,22 @@
 | 
				
			||||||
	"author": "",
 | 
						"author": "",
 | 
				
			||||||
	"license": "ISC",
 | 
						"license": "ISC",
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
		"@elysiajs/cors": "^1.3.3",
 | 
							"@hono/node-server": "^1.19.6",
 | 
				
			||||||
		"@elysiajs/html": "^1.3.1",
 | 
							"@hono/standard-validator": "^0.1.5",
 | 
				
			||||||
		"@elysiajs/node": "^1.4.1",
 | 
							"@hono/zod-openapi": "^1.1.4",
 | 
				
			||||||
		"@elysiajs/openapi": "^1.4.11",
 | 
							"@hono/zod-validator": "^0.7.4",
 | 
				
			||||||
		"@std/async": "npm:@jsr/std__async@^1.0.13",
 | 
							"@std/async": "npm:@jsr/std__async@^1.0.13",
 | 
				
			||||||
		"@zip.js/zip.js": "^2.7.62",
 | 
							"@zip.js/zip.js": "^2.7.62",
 | 
				
			||||||
		"better-sqlite3": "^9.6.0",
 | 
							"better-sqlite3": "^9.6.0",
 | 
				
			||||||
		"chokidar": "^3.6.0",
 | 
							"chokidar": "^3.6.0",
 | 
				
			||||||
		"dbtype": "workspace:dbtype",
 | 
							"dbtype": "workspace:dbtype",
 | 
				
			||||||
		"dotenv": "^16.5.0",
 | 
							"dotenv": "^16.5.0",
 | 
				
			||||||
		"elysia": "^1.4.9",
 | 
							"hono": "^4.10.4",
 | 
				
			||||||
		"jose": "^5.10.0",
 | 
							"jose": "^5.10.0",
 | 
				
			||||||
		"kysely": "^0.27.6",
 | 
							"kysely": "^0.27.6",
 | 
				
			||||||
		"natural-orderby": "^2.0.3",
 | 
							"natural-orderby": "^2.0.3",
 | 
				
			||||||
		"tiny-async-pool": "^1.3.0"
 | 
							"tiny-async-pool": "^1.3.0",
 | 
				
			||||||
 | 
							"zod": "^4.1.12"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"devDependencies": {
 | 
						"devDependencies": {
 | 
				
			||||||
		"@types/better-sqlite3": "^7.6.13",
 | 
							"@types/better-sqlite3": "^7.6.13",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import { existsSync, readFileSync } from "node:fs";
 | 
					import { existsSync, readFileSync } from "node:fs";
 | 
				
			||||||
import type { Kysely } from "kysely";
 | 
					import type { Kysely } from "kysely";
 | 
				
			||||||
import type { db } from "dbtype";
 | 
					import type { db } from "dbtype";
 | 
				
			||||||
import { Permission } from "./permission/permission.ts";
 | 
					import { type Permission, normalizePermissions } from "./permission/permission.ts";
 | 
				
			||||||
import { getAppConfig, upsertAppConfig } from "./db/config.ts";
 | 
					import { getAppConfig, upsertAppConfig } from "./db/config.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SettingConfig {
 | 
					export interface SettingConfig {
 | 
				
			||||||
| 
						 | 
					@ -130,14 +130,8 @@ const loadPersistedSetting = async (db: Kysely<db.DB>): Promise<PersistedSetting
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mergePersisted = (input: Partial<PersistedSetting>): PersistedSetting => {
 | 
					const mergePersisted = (input: Partial<PersistedSetting>): PersistedSetting => {
 | 
				
			||||||
	const validPermissions = new Set<Permission>(Object.values(Permission));
 | 
						const guestSource = Array.isArray(input.guest) ? input.guest : persistedDefault.guest;
 | 
				
			||||||
	const guest = Array.isArray(input.guest)
 | 
						const guest = normalizePermissions(guestSource);
 | 
				
			||||||
		? Array.from(
 | 
					 | 
				
			||||||
			new Set(
 | 
					 | 
				
			||||||
				input.guest.filter((value): value is Permission => validPermissions.has(value as Permission)),
 | 
					 | 
				
			||||||
			),
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		: persistedDefault.guest;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		secure: input.secure ?? persistedDefault.secure,
 | 
							secure: input.secure ?? persistedDefault.secure,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,3 @@
 | 
				
			||||||
import Elysia from "elysia";
 | 
					 | 
				
			||||||
import { connectDB } from "./database.ts";
 | 
					import { connectDB } from "./database.ts";
 | 
				
			||||||
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
 | 
					import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import { getKysely } from "./kysely.ts";
 | 
					import { getKysely } from "./kysely.ts";
 | 
				
			||||||
import { type IUser, IUserSettings, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
 | 
					import { type IUser, IUserSettings, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
 | 
				
			||||||
 | 
					import { normalizePermissions } from "../permission/permission.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SqliteUser implements IUser {
 | 
					class SqliteUser implements IUser {
 | 
				
			||||||
	readonly username: string;
 | 
						readonly username: string;
 | 
				
			||||||
| 
						 | 
					@ -23,7 +24,7 @@ class SqliteUser implements IUser {
 | 
				
			||||||
			.selectAll()
 | 
								.selectAll()
 | 
				
			||||||
			.where("username", "=", this.username)
 | 
								.where("username", "=", this.username)
 | 
				
			||||||
			.execute();
 | 
								.execute();
 | 
				
			||||||
		return permissions.map((x) => x.name);
 | 
							return normalizePermissions(permissions.map((x) => x.name));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	async add(name: string) {
 | 
						async add(name: string) {
 | 
				
			||||||
		const result = await this.kysely
 | 
							const result = await this.kysely
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,57 +1,63 @@
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { sValidator } from "@hono/standard-validator";
 | 
				
			||||||
import type { DiffManager } from "./diff.ts";
 | 
					import type { DiffManager } from "./diff.ts";
 | 
				
			||||||
import type { ContentFile } from "../content/mod.ts";
 | 
					import type { ContentFile } from "../content/mod.ts";
 | 
				
			||||||
 | 
					import type { AppEnv } from "../login.ts";
 | 
				
			||||||
import { AdminOnly } from "../permission/permission.ts";
 | 
					import { AdminOnly } from "../permission/permission.ts";
 | 
				
			||||||
import { sendError } from "../route/error_handler.ts";
 | 
					import { sendError } from "../route/error_handler.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const toSerializableContent = (file: ContentFile) => ({ path: file.path, type: file.type });
 | 
					const toSerializableContent = (file: ContentFile) => ({ path: file.path, type: file.type });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CommitEntrySchema = t.Array(t.Object({
 | 
					const commitEntrySchema = z.array(z.object({
 | 
				
			||||||
	type: t.String(),
 | 
						type: z.string(),
 | 
				
			||||||
	path: t.String(),
 | 
						path: z.string(),
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CommitAllSchema = t.Object({
 | 
					const commitAllSchema = z.object({
 | 
				
			||||||
	type: t.String(),
 | 
						type: z.string(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createDiffRouter = (diffmgr: DiffManager) =>
 | 
					
 | 
				
			||||||
	new Elysia({ name: "diff-router" })
 | 
					export const createDiffRouter = (diffmgr: DiffManager) => {
 | 
				
			||||||
		.group("/diff", (app) =>
 | 
						const router = new Hono<AppEnv>();
 | 
				
			||||||
			app
 | 
					
 | 
				
			||||||
				.get("/list", () => {
 | 
						router.get("/list", AdminOnly, (c) =>
 | 
				
			||||||
					return diffmgr.getAdded().map((entry) => ({
 | 
							c.json(
 | 
				
			||||||
 | 
								diffmgr.getAdded().map((entry) => ({
 | 
				
			||||||
				type: entry.type,
 | 
									type: entry.type,
 | 
				
			||||||
				value: entry.value.map(toSerializableContent),
 | 
									value: entry.value.map(toSerializableContent),
 | 
				
			||||||
					}));
 | 
								})),
 | 
				
			||||||
				}, {
 | 
							),
 | 
				
			||||||
					beforeHandle: AdminOnly,
 | 
						);
 | 
				
			||||||
				})
 | 
					
 | 
				
			||||||
				.post("/commit", async ({ body }) => {
 | 
						router.post(
 | 
				
			||||||
					if (body.length === 0) {
 | 
							"/commit",
 | 
				
			||||||
						return { ok: true, docs: [] as number[] };
 | 
							AdminOnly,
 | 
				
			||||||
 | 
							sValidator("json", commitEntrySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const entries = c.req.valid("json");
 | 
				
			||||||
 | 
								if (entries.length === 0) {
 | 
				
			||||||
 | 
									return c.json({ ok: true, docs: [] as number[] });
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
					const results = await Promise.all(body.map(({ type, path }) => diffmgr.commit(type, path)));
 | 
								const results = await Promise.all(entries.map(({ type, path }) => diffmgr.commit(type, path)));
 | 
				
			||||||
					return {
 | 
								return c.json({ ok: true, docs: results });
 | 
				
			||||||
						ok: true,
 | 
							},
 | 
				
			||||||
						docs: results,
 | 
						);
 | 
				
			||||||
					};
 | 
					
 | 
				
			||||||
				}, {
 | 
						router.post(
 | 
				
			||||||
					beforeHandle: AdminOnly,
 | 
							"/commitall",
 | 
				
			||||||
					body: CommitEntrySchema,
 | 
							AdminOnly,
 | 
				
			||||||
				})
 | 
							sValidator("json", commitAllSchema),
 | 
				
			||||||
				.post("/commitall", async ({ body }) => {
 | 
							async (c) => {
 | 
				
			||||||
					const { type } = body;
 | 
								const { type } = c.req.valid("json");
 | 
				
			||||||
			if (!type) {
 | 
								if (!type) {
 | 
				
			||||||
				sendError(400, 'format exception: there is no "type"');
 | 
									sendError(400, 'format exception: there is no "type"');
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			await diffmgr.commitAll(type);
 | 
								await diffmgr.commitAll(type);
 | 
				
			||||||
					return { ok: true };
 | 
								return c.json({ ok: true });
 | 
				
			||||||
				}, {
 | 
							},
 | 
				
			||||||
					beforeHandle: AdminOnly,
 | 
					 | 
				
			||||||
					body: CommitAllSchema,
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				.get("/*", () => {
 | 
					 | 
				
			||||||
					sendError(404);
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return router;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,35 @@
 | 
				
			||||||
import { Elysia, t, type Context } from "elysia";
 | 
					import { getCookie, setCookie, deleteCookie } from "hono/cookie";
 | 
				
			||||||
 | 
					import { Hono, type Context, type MiddlewareHandler } from "hono";
 | 
				
			||||||
import { SignJWT, jwtVerify, errors } from "jose";
 | 
					import { SignJWT, jwtVerify, errors } from "jose";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { sValidator } from "@hono/standard-validator";
 | 
				
			||||||
import type { IUser, UserAccessor } from "./model/mod.ts";
 | 
					import type { IUser, UserAccessor } from "./model/mod.ts";
 | 
				
			||||||
import { ClientRequestError } from "./route/error_handler.ts";
 | 
					import { ClientRequestError } from "./route/error_handler.ts";
 | 
				
			||||||
import { get_setting } from "./SettingConfig.ts";
 | 
					import { get_setting } from "./SettingConfig.ts";
 | 
				
			||||||
 | 
					import { normalizePermissions } from "./permission/permission.ts";
 | 
				
			||||||
 | 
					import type { Permission } from "./permission/permission.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PayloadInfo = {
 | 
					export type PayloadInfo = {
 | 
				
			||||||
	username: string;
 | 
						username: string;
 | 
				
			||||||
	permission: string[];
 | 
						permission: Permission[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type UserState = {
 | 
					export type UserState = {
 | 
				
			||||||
	user: PayloadInfo;
 | 
						user: PayloadInfo;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AuthStore = {
 | 
					export type AuthStore = {
 | 
				
			||||||
	user: PayloadInfo;
 | 
						user: PayloadInfo;
 | 
				
			||||||
	refreshed: boolean;
 | 
						refreshed: boolean;
 | 
				
			||||||
	authenticated: boolean;
 | 
						authenticated: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AppEnv = {
 | 
				
			||||||
 | 
						Variables: {
 | 
				
			||||||
 | 
							auth: AuthStore;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LoginResponse = {
 | 
					type LoginResponse = {
 | 
				
			||||||
	accessExpired: number;
 | 
						accessExpired: number;
 | 
				
			||||||
} & PayloadInfo;
 | 
					} & PayloadInfo;
 | 
				
			||||||
| 
						 | 
					@ -30,20 +41,18 @@ type RefreshResponse = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RefreshPayloadInfo = { username: string };
 | 
					type RefreshPayloadInfo = { username: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CookieJar = Context["cookie"];
 | 
					const LoginBodySchema = z.object({
 | 
				
			||||||
 | 
						username: z.string(),
 | 
				
			||||||
const LoginBodySchema = t.Object({
 | 
						password: z.string(),
 | 
				
			||||||
	username: t.String(),
 | 
					 | 
				
			||||||
	password: t.String(),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ResetBodySchema = t.Object({
 | 
					const ResetBodySchema = z.object({
 | 
				
			||||||
	username: t.String(),
 | 
						username: z.string(),
 | 
				
			||||||
	oldpassword: t.String(),
 | 
						oldpassword: z.string(),
 | 
				
			||||||
	newpassword: t.String(),
 | 
						newpassword: z.string(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SettingsBodySchema = t.Record(t.String(), t.Unknown());
 | 
					const SettingsBodySchema = z.record(z.string(), z.unknown());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const accessExpiredTime = 60 * 60 * 2 * 1000; // 2 hours
 | 
					const accessExpiredTime = 60 * 60 * 2 * 1000; // 2 hours
 | 
				
			||||||
const refreshExpiredTime = 60 * 60 * 24 * 14 * 1000; // 14 days
 | 
					const refreshExpiredTime = 60 * 60 * 24 * 14 * 1000; // 14 days
 | 
				
			||||||
| 
						 | 
					@ -83,30 +92,36 @@ async function verifyToken<T>(token: string, secret: string): Promise<T> {
 | 
				
			||||||
export const accessTokenName = "access_token";
 | 
					export const accessTokenName = "access_token";
 | 
				
			||||||
export const refreshTokenName = "refresh_token";
 | 
					export const refreshTokenName = "refresh_token";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setToken(cookie: CookieJar, token_name: string, token_payload: string | null, expiredMilliseconds: number) {
 | 
					const setToken = (c: Context<AppEnv>, tokenName: string, tokenPayload: string | null, expiresMs: number) => {
 | 
				
			||||||
	if (token_payload === null) {
 | 
						const setting = get_setting();
 | 
				
			||||||
		cookie[token_name]?.remove();
 | 
						if (tokenPayload === null) {
 | 
				
			||||||
 | 
							deleteCookie(c, tokenName, {
 | 
				
			||||||
 | 
								path: "/",
 | 
				
			||||||
 | 
								secure: setting.secure,
 | 
				
			||||||
 | 
								sameSite: "Strict",
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const setting = get_setting();
 | 
						setCookie(c, tokenName, tokenPayload, {
 | 
				
			||||||
	cookie[token_name].set({
 | 
							path: "/",
 | 
				
			||||||
		value: token_payload,
 | 
					 | 
				
			||||||
		httpOnly: true,
 | 
							httpOnly: true,
 | 
				
			||||||
		secure: setting.secure,
 | 
							secure: setting.secure,
 | 
				
			||||||
		sameSite: "strict",
 | 
							sameSite: "Strict",
 | 
				
			||||||
		expires: new Date(Date.now() + expiredMilliseconds),
 | 
							expires: new Date(Date.now() + expiresMs),
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function removeToken(cookie: CookieJar, token_name: string) {
 | 
					type RawPayloadInfo = {
 | 
				
			||||||
	cookie[token_name]?.remove();
 | 
						username: string;
 | 
				
			||||||
}
 | 
						permission: unknown[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isUserState = (obj: unknown): obj is PayloadInfo => {
 | 
					const isUserState = (obj: unknown): obj is RawPayloadInfo => {
 | 
				
			||||||
	if (typeof obj !== "object" || obj === null) {
 | 
						if (typeof obj !== "object" || obj === null) {
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return "username" in obj && "permission" in obj && Array.isArray((obj as { permission: unknown }).permission);
 | 
						const candidate = obj as { username?: unknown; permission?: unknown };
 | 
				
			||||||
 | 
						return typeof candidate.username === "string" && Array.isArray(candidate.permission);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isRefreshToken = (obj: unknown): obj is RefreshPayloadInfo => {
 | 
					const isRefreshToken = (obj: unknown): obj is RefreshPayloadInfo => {
 | 
				
			||||||
| 
						 | 
					@ -123,25 +138,23 @@ type AuthResult = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function authenticate(
 | 
					async function authenticate(
 | 
				
			||||||
	cookie: CookieJar,
 | 
						c: Context<AppEnv>,
 | 
				
			||||||
	userController: UserAccessor,
 | 
						userController: UserAccessor,
 | 
				
			||||||
	options: { forceRefresh?: boolean } = {},
 | 
						options: { forceRefresh?: boolean } = {},
 | 
				
			||||||
): Promise<AuthResult> {
 | 
					): Promise<AuthResult> {
 | 
				
			||||||
	const setting = get_setting();
 | 
						const setting = get_setting();
 | 
				
			||||||
	const secretKey = setting.jwt_secretkey;
 | 
						const secretKey = setting.jwt_secretkey;
 | 
				
			||||||
	const accessCookie = cookie[accessTokenName];
 | 
						const accessValue = getCookie(c, accessTokenName);
 | 
				
			||||||
	const refreshCookie = cookie[refreshTokenName];
 | 
						const refreshValue = getCookie(c, refreshTokenName);
 | 
				
			||||||
	const accessValue = typeof accessCookie?.value === 'string' ? accessCookie.value : undefined;
 | 
					 | 
				
			||||||
	const refreshValue = typeof refreshCookie?.value === 'string' ? refreshCookie.value : undefined;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const guestUser: PayloadInfo = {
 | 
						const guestUser: PayloadInfo = {
 | 
				
			||||||
		username: "",
 | 
							username: "",
 | 
				
			||||||
		permission: setting.guest,
 | 
							permission: [...setting.guest],
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const setGuest = (): AuthResult => {
 | 
						const setGuest = (): AuthResult => {
 | 
				
			||||||
		accessCookie?.remove();
 | 
							setToken(c, accessTokenName, null, 0);
 | 
				
			||||||
		refreshCookie?.remove();
 | 
							setToken(c, refreshTokenName, null, 0);
 | 
				
			||||||
		return { user: guestUser, refreshed: false, success: false };
 | 
							return { user: guestUser, refreshed: false, success: false };
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -150,13 +163,13 @@ async function authenticate(
 | 
				
			||||||
		if (!account) {
 | 
							if (!account) {
 | 
				
			||||||
			return setGuest();
 | 
								return setGuest();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		const permissions = await account.get_permissions();
 | 
							const permissions = normalizePermissions(await account.get_permissions());
 | 
				
			||||||
		const payload: PayloadInfo = {
 | 
							const payload: PayloadInfo = {
 | 
				
			||||||
			username: account.username,
 | 
								username: account.username,
 | 
				
			||||||
			permission: permissions,
 | 
								permission: permissions,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		const accessToken = await createAccessToken(payload, secretKey);
 | 
							const accessToken = await createAccessToken(payload, secretKey);
 | 
				
			||||||
		setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
 | 
							setToken(c, accessTokenName, accessToken, accessExpiredTime);
 | 
				
			||||||
		return { user: payload, refreshed: true, success: true };
 | 
							return { user: payload, refreshed: true, success: true };
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -178,11 +191,15 @@ async function authenticate(
 | 
				
			||||||
	if (options.forceRefresh) {
 | 
						if (options.forceRefresh) {
 | 
				
			||||||
		if (accessValue) {
 | 
							if (accessValue) {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				const payload = await verifyToken<PayloadInfo>(accessValue, secretKey);
 | 
									const payload = await verifyToken<RawPayloadInfo>(accessValue, secretKey);
 | 
				
			||||||
				if (isUserState(payload)) {
 | 
									if (isUserState(payload)) {
 | 
				
			||||||
					const accessToken = await createAccessToken(payload, secretKey);
 | 
										const normalized: PayloadInfo = {
 | 
				
			||||||
					setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
 | 
											username: payload.username,
 | 
				
			||||||
					return { user: payload, refreshed: true, success: true };
 | 
											permission: normalizePermissions(payload.permission),
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										const accessToken = await createAccessToken(normalized, secretKey);
 | 
				
			||||||
 | 
										setToken(c, accessTokenName, accessToken, accessExpiredTime);
 | 
				
			||||||
 | 
										return { user: normalized, refreshed: true, success: true };
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				return setGuest();
 | 
									return setGuest();
 | 
				
			||||||
			} catch (error) {
 | 
								} catch (error) {
 | 
				
			||||||
| 
						 | 
					@ -196,9 +213,16 @@ async function authenticate(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (accessValue) {
 | 
						if (accessValue) {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			const payload = await verifyToken<PayloadInfo>(accessValue, secretKey);
 | 
								const payload = await verifyToken<RawPayloadInfo>(accessValue, secretKey);
 | 
				
			||||||
			if (isUserState(payload)) {
 | 
								if (isUserState(payload)) {
 | 
				
			||||||
				return { user: payload, refreshed: false, success: true };
 | 
									return {
 | 
				
			||||||
 | 
										user: {
 | 
				
			||||||
 | 
											username: payload.username,
 | 
				
			||||||
 | 
											permission: normalizePermissions(payload.permission),
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										refreshed: false,
 | 
				
			||||||
 | 
										success: true,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return setGuest();
 | 
								return setGuest();
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
| 
						 | 
					@ -212,13 +236,15 @@ async function authenticate(
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createLoginRouter = (userController: UserAccessor) => {
 | 
					export const createLoginRouter = (userController: UserAccessor) => {
 | 
				
			||||||
	return new Elysia({ name: "login-router" })
 | 
						const router = new Hono<AppEnv>();
 | 
				
			||||||
		.group("/user", (app) =>
 | 
					
 | 
				
			||||||
			app
 | 
						router.post(
 | 
				
			||||||
				.post("/login", async ({ body, cookie, set }) => {
 | 
							"/login",
 | 
				
			||||||
 | 
							sValidator("json", LoginBodySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
			const setting = get_setting();
 | 
								const setting = get_setting();
 | 
				
			||||||
			const secretKey = setting.jwt_secretkey;
 | 
								const secretKey = setting.jwt_secretkey;
 | 
				
			||||||
					const { username, password } = body;
 | 
								const { username, password } = c.req.valid("json");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (username === "admin" && setting.forbid_remote_admin_login) {
 | 
								if (username === "admin" && setting.forbid_remote_admin_login) {
 | 
				
			||||||
				throw new ClientRequestError(403, "forbidden remote admin login");
 | 
									throw new ClientRequestError(403, "forbidden remote admin login");
 | 
				
			||||||
| 
						 | 
					@ -229,98 +255,100 @@ export const createLoginRouter = (userController: UserAccessor) => {
 | 
				
			||||||
				throw new ClientRequestError(401, "not authorized");
 | 
									throw new ClientRequestError(401, "not authorized");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const permission = await user.get_permissions();
 | 
								const permission = normalizePermissions(await user.get_permissions());
 | 
				
			||||||
			const accessToken = await createAccessToken({ username: user.username, permission }, secretKey);
 | 
								const accessToken = await createAccessToken({ username: user.username, permission }, secretKey);
 | 
				
			||||||
			const refreshToken = await createRefreshToken({ username: user.username }, secretKey);
 | 
								const refreshToken = await createRefreshToken({ username: user.username }, secretKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
 | 
								setToken(c, accessTokenName, accessToken, accessExpiredTime);
 | 
				
			||||||
					setToken(cookie, refreshTokenName, refreshToken, refreshExpiredTime);
 | 
								setToken(c, refreshTokenName, refreshToken, refreshExpiredTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					set.status = 200;
 | 
								return c.json({
 | 
				
			||||||
 | 
					 | 
				
			||||||
					return {
 | 
					 | 
				
			||||||
				username: user.username,
 | 
									username: user.username,
 | 
				
			||||||
				permission,
 | 
									permission,
 | 
				
			||||||
						accessExpired: Math.floor(Date.now() ) + accessExpiredTime,
 | 
									accessExpired: Math.floor(Date.now()) + accessExpiredTime,
 | 
				
			||||||
					} satisfies LoginResponse;
 | 
								} satisfies LoginResponse);
 | 
				
			||||||
				}, {
 | 
							},
 | 
				
			||||||
					body: LoginBodySchema,
 | 
						);
 | 
				
			||||||
				})
 | 
					
 | 
				
			||||||
				.post("/logout", ({ cookie, set }) => {
 | 
						router.post("/logout", (c) => {
 | 
				
			||||||
		const setting = get_setting();
 | 
							const setting = get_setting();
 | 
				
			||||||
					removeToken(cookie, accessTokenName);
 | 
							setToken(c, accessTokenName, null, 0);
 | 
				
			||||||
					removeToken(cookie, refreshTokenName);
 | 
							setToken(c, refreshTokenName, null, 0);
 | 
				
			||||||
					set.status = 200;
 | 
							return c.json({
 | 
				
			||||||
					return {
 | 
					 | 
				
			||||||
			ok: true,
 | 
								ok: true,
 | 
				
			||||||
			username: "",
 | 
								username: "",
 | 
				
			||||||
			permission: setting.guest,
 | 
								permission: setting.guest,
 | 
				
			||||||
					};
 | 
							});
 | 
				
			||||||
				})
 | 
						});
 | 
				
			||||||
				.post("/refresh", async ({ cookie }) => {
 | 
					
 | 
				
			||||||
					const auth = await authenticate(cookie, userController, { forceRefresh: true });
 | 
						router.post("/refresh", async (c) => {
 | 
				
			||||||
 | 
							const auth = await authenticate(c, userController, { forceRefresh: true });
 | 
				
			||||||
		if (!auth.success) {
 | 
							if (!auth.success) {
 | 
				
			||||||
			throw new ClientRequestError(401, "not authorized");
 | 
								throw new ClientRequestError(401, "not authorized");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
					return {
 | 
							return c.json({
 | 
				
			||||||
			...auth.user,
 | 
								...auth.user,
 | 
				
			||||||
			refresh: true,
 | 
								refresh: true,
 | 
				
			||||||
			accessExpired: Math.floor(Date.now()) + accessExpiredTime,
 | 
								accessExpired: Math.floor(Date.now()) + accessExpiredTime,
 | 
				
			||||||
					} satisfies RefreshResponse;
 | 
							} satisfies RefreshResponse);
 | 
				
			||||||
				})
 | 
						});
 | 
				
			||||||
				.post("/reset", async ({ body }) => {
 | 
					
 | 
				
			||||||
					const { username, oldpassword, newpassword } = body;
 | 
						router.post(
 | 
				
			||||||
 | 
							"/reset",
 | 
				
			||||||
 | 
							sValidator("json", ResetBodySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const { username, oldpassword, newpassword } = c.req.valid("json");
 | 
				
			||||||
			const account = await userController.findUser(username);
 | 
								const account = await userController.findUser(username);
 | 
				
			||||||
			if (!account || !account.password.check_password(oldpassword)) {
 | 
								if (!account || !account.password.check_password(oldpassword)) {
 | 
				
			||||||
				throw new ClientRequestError(403, "not authorized");
 | 
									throw new ClientRequestError(403, "not authorized");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			await account.reset_password(newpassword);
 | 
								await account.reset_password(newpassword);
 | 
				
			||||||
					return { ok: true };
 | 
								return c.json({ ok: true });
 | 
				
			||||||
				}, {
 | 
							},
 | 
				
			||||||
					body: ResetBodySchema,
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				.get("/settings", async ({ store }) => {
 | 
					 | 
				
			||||||
					const { user } = store as AuthStore;
 | 
					 | 
				
			||||||
					if (!user.username) {
 | 
					 | 
				
			||||||
						throw new ClientRequestError(403, "not authorized");
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					const account = await userController.findUser(user.username);
 | 
					 | 
				
			||||||
					if (!account) {
 | 
					 | 
				
			||||||
						throw new ClientRequestError(403, "not authorized");
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					return (await account.get_settings()) ?? {};
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				.post("/settings", async ({ body, store }) => {
 | 
					 | 
				
			||||||
					const { user } = store as AuthStore;
 | 
					 | 
				
			||||||
					if (!user.username) {
 | 
					 | 
				
			||||||
						throw new ClientRequestError(403, "not authorized");
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					const account = await userController.findUser(user.username);
 | 
					 | 
				
			||||||
					if (!account) {
 | 
					 | 
				
			||||||
						throw new ClientRequestError(403, "not authorized");
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					await account.set_settings(body as Record<string, unknown>);
 | 
					 | 
				
			||||||
					return { ok: true };
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					body: SettingsBodySchema,
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router.get("/settings", async (c) => {
 | 
				
			||||||
 | 
							const auth = c.get("auth");
 | 
				
			||||||
 | 
							if (!auth.user.username) {
 | 
				
			||||||
 | 
								throw new ClientRequestError(403, "not authorized");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const account = await userController.findUser(auth.user.username);
 | 
				
			||||||
 | 
							if (!account) {
 | 
				
			||||||
 | 
								throw new ClientRequestError(403, "not authorized");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return c.json((await account.get_settings()) ?? {});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router.post(
 | 
				
			||||||
 | 
							"/settings",
 | 
				
			||||||
 | 
							sValidator("json", SettingsBodySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const auth = c.get("auth");
 | 
				
			||||||
 | 
								if (!auth.user.username) {
 | 
				
			||||||
 | 
									throw new ClientRequestError(403, "not authorized");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								const account = await userController.findUser(auth.user.username);
 | 
				
			||||||
 | 
								if (!account) {
 | 
				
			||||||
 | 
									throw new ClientRequestError(403, "not authorized");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								await account.set_settings(c.req.valid("json"));
 | 
				
			||||||
 | 
								return c.json({ ok: true });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return router;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createUserHandler = (userController: UserAccessor) => {
 | 
					export const createUserHandler = (userController: UserAccessor): MiddlewareHandler<AppEnv> =>
 | 
				
			||||||
	return new Elysia({
 | 
						async (c, next) => {
 | 
				
			||||||
		name: "user-handler",
 | 
							const auth = await authenticate(c, userController);
 | 
				
			||||||
		seed: "UserAccess",
 | 
							c.set("auth", {
 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
		.derive({ as: "scoped" }, async ({ cookie }) => {
 | 
					 | 
				
			||||||
			const auth = await authenticate(cookie, userController);
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
			user: auth.user,
 | 
								user: auth.user,
 | 
				
			||||||
			refreshed: auth.refreshed,
 | 
								refreshed: auth.refreshed,
 | 
				
			||||||
			authenticated: auth.success,
 | 
								authenticated: auth.success,
 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
};
 | 
							await next();
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getAdmin = async (cntr: UserAccessor) => {
 | 
					export const getAdmin = async (cntr: UserAccessor) => {
 | 
				
			||||||
	const admin = await cntr.findUser("admin");
 | 
						const admin = await cntr.findUser("admin");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { UserSetting } from "dbtype";
 | 
					import { UserSetting } from "dbtype";
 | 
				
			||||||
 | 
					import type { Permission } from "../permission/permission.ts";
 | 
				
			||||||
import { createHmac, randomBytes } from "node:crypto";
 | 
					import { createHmac, randomBytes } from "node:crypto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function hashForPassword(salt: string, password: string) {
 | 
					function hashForPassword(salt: string, password: string) {
 | 
				
			||||||
| 
						 | 
					@ -50,7 +51,7 @@ export interface IUser {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * return user's permission list.
 | 
						 * return user's permission list.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	get_permissions(): Promise<string[]>;
 | 
						get_permissions(): Promise<Permission[]>;
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * add permission
 | 
						 * add permission
 | 
				
			||||||
	 * @param name permission name to add
 | 
						 * @param name permission name to add
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,68 +1,66 @@
 | 
				
			||||||
 | 
					import { PERMISSIONS as SHARED_PERMISSIONS, type PermissionName } from "dbtype";
 | 
				
			||||||
 | 
					import type { Context, MiddlewareHandler } from "hono";
 | 
				
			||||||
 | 
					import type { AppEnv, PayloadInfo } from "../login.ts";
 | 
				
			||||||
import { sendError } from "../route/error_handler.ts";
 | 
					import { sendError } from "../route/error_handler.ts";
 | 
				
			||||||
import type { UserState } from "../login.ts";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum Permission {
 | 
					export const PERMISSIONS = SHARED_PERMISSIONS;
 | 
				
			||||||
	// ========
 | 
					 | 
				
			||||||
	// not implemented
 | 
					 | 
				
			||||||
	// admin only
 | 
					 | 
				
			||||||
	/** remove document */
 | 
					 | 
				
			||||||
	// removeContent = 'removeContent',
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** upload document */
 | 
					export type Permission = PermissionName;
 | 
				
			||||||
	// uploadContent = 'uploadContent',
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** modify document except base path, filename, content_hash. but admin can modify all. */
 | 
					export const PERMISSION = {
 | 
				
			||||||
	// modifyContent = 'modifyContent',
 | 
						ModifyTag: PERMISSIONS[0],
 | 
				
			||||||
 | 
						QueryContent: PERMISSIONS[1],
 | 
				
			||||||
 | 
						ModifyTagDesc: PERMISSIONS[2],
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** add tag into document */
 | 
					const PERMISSION_SET = new Set<Permission>(PERMISSIONS);
 | 
				
			||||||
	// addTagContent = 'addTagContent',
 | 
					 | 
				
			||||||
	/** remove tag from document */
 | 
					 | 
				
			||||||
	// removeTagContent = 'removeTagContent',
 | 
					 | 
				
			||||||
	/** ModifyTagInDoc */
 | 
					 | 
				
			||||||
	ModifyTag = "ModifyTag",
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** find documents with query */
 | 
					export const isPermission = (value: unknown): value is Permission =>
 | 
				
			||||||
	// findAllContent = 'findAllContent',
 | 
						typeof value === "string" && PERMISSION_SET.has(value as Permission);
 | 
				
			||||||
	/** find one document. */
 | 
					 | 
				
			||||||
	// findOneContent = 'findOneContent',
 | 
					 | 
				
			||||||
	/** view content*/
 | 
					 | 
				
			||||||
	// viewContent = 'viewContent',
 | 
					 | 
				
			||||||
	QueryContent = "QueryContent",
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** modify description about the one tag. */
 | 
					export const normalizePermissions = (values?: Iterable<unknown>): Permission[] => {
 | 
				
			||||||
	modifyTagDesc = "ModifyTagDesc",
 | 
						if (!values) {
 | 
				
			||||||
}
 | 
							return [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
type PermissionCheckContext = {
 | 
					 | 
				
			||||||
	user?: UserState["user"];
 | 
					 | 
				
			||||||
	store?: { user?: UserState["user"] };
 | 
					 | 
				
			||||||
} & Record<string, unknown>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const resolveUser = (context: PermissionCheckContext): UserState["user"] => {
 | 
					 | 
				
			||||||
	const user = context.user ?? context.store?.user;
 | 
					 | 
				
			||||||
	if (!user) {
 | 
					 | 
				
			||||||
		sendError(401, "you are guest. login needed.");
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return user as UserState["user"];
 | 
						const normalized = new Set<Permission>();
 | 
				
			||||||
 | 
						for (const value of values) {
 | 
				
			||||||
 | 
							if (isPermission(value)) {
 | 
				
			||||||
 | 
								normalized.add(value);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Array.from(normalized);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createPermissionCheck = (...permissions: string[]) => (context: PermissionCheckContext) => {
 | 
					const resolveUser = (c: Context<AppEnv>): PayloadInfo => {
 | 
				
			||||||
    const user = resolveUser(context);
 | 
						const auth = c.get("auth");
 | 
				
			||||||
 | 
						if (!auth?.user) {
 | 
				
			||||||
 | 
							sendError(401, "you are guest. login needed.");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return auth.user;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createPermissionCheck = (
 | 
				
			||||||
 | 
						...permissions: Permission[]
 | 
				
			||||||
 | 
					): MiddlewareHandler<AppEnv> => async (c, next) => {
 | 
				
			||||||
 | 
						const user = resolveUser(c);
 | 
				
			||||||
	if (user.username === "admin") {
 | 
						if (user.username === "admin") {
 | 
				
			||||||
 | 
							await next();
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
    const user_permission = user.permission;
 | 
						const userPermission = user.permission;
 | 
				
			||||||
    if (!permissions.every((p) => user_permission.includes(p))) {
 | 
						if (!permissions.every((p) => userPermission.includes(p))) {
 | 
				
			||||||
		if (user.username === "") {
 | 
							if (user.username === "") {
 | 
				
			||||||
			throw sendError(401, "you are guest. login needed.");
 | 
								throw sendError(401, "you are guest. login needed.");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		throw sendError(403, "do not have permission");
 | 
							throw sendError(403, "do not have permission");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						await next();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AdminOnly = (context: PermissionCheckContext) => {
 | 
					export const AdminOnly: MiddlewareHandler<AppEnv> = async (c, next) => {
 | 
				
			||||||
	const user = resolveUser(context);
 | 
						const user = resolveUser(c);
 | 
				
			||||||
	if (user.username !== "admin") {
 | 
						if (user.username !== "admin") {
 | 
				
			||||||
		throw sendError(403, "admin only");
 | 
							throw sendError(403, "admin only");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						await next();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,3 @@
 | 
				
			||||||
import type { Context as ElysiaContext } from "elysia";
 | 
					 | 
				
			||||||
import { Readable } from "node:stream";
 | 
					import { Readable } from "node:stream";
 | 
				
			||||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
 | 
					import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
 | 
				
			||||||
import { Entry } from "@zip.js/zip.js";
 | 
					import { Entry } from "@zip.js/zip.js";
 | 
				
			||||||
| 
						 | 
					@ -10,21 +9,24 @@ const extensionToMime = (ext: string) => {
 | 
				
			||||||
	return `image/${ext}`;
 | 
						return `image/${ext}`;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ResponseSet = Pick<ElysiaContext["set"], "status" | "headers">;
 | 
					export type ResponseMeta = {
 | 
				
			||||||
 | 
						status: number;
 | 
				
			||||||
 | 
						headers: Record<string, string>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RenderOptions = {
 | 
					type RenderOptions = {
 | 
				
			||||||
	path: string;
 | 
						path: string;
 | 
				
			||||||
	page: number;
 | 
						page: number;
 | 
				
			||||||
	reqHeaders: Headers;
 | 
						reqHeaders: Headers;
 | 
				
			||||||
	set: ResponseSet;
 | 
						set: ResponseMeta;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function setHeadersForEntry(entry: Entry, reqHeaders: Headers, set: ResponseSet) {
 | 
					async function setHeadersForEntry(entry: Entry, reqHeaders: Headers, set: ResponseMeta) {
 | 
				
			||||||
	const ext = entry.filename.split(".").pop()?.toLowerCase() ?? "jpeg";
 | 
						const ext = entry.filename.split(".").pop()?.toLowerCase() ?? "jpeg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	set.headers["content-type"] = extensionToMime(ext);
 | 
						set.headers["content-type"] = extensionToMime(ext);
 | 
				
			||||||
	if (typeof entry.uncompressedSize === "number") {
 | 
						if (typeof entry.uncompressedSize === "number") {
 | 
				
			||||||
		set.headers["content-length"] = entry.uncompressedSize;
 | 
							set.headers["content-length"] = entry.uncompressedSize.toString();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const lastModified = entry.lastModDate ?? new Date();
 | 
						const lastModified = entry.lastModDate ?? new Date();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,227 +1,316 @@
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { sValidator } from "@hono/standard-validator";
 | 
				
			||||||
import { join } from "node:path";
 | 
					import { join } from "node:path";
 | 
				
			||||||
import type { Document, QueryListOption } from "dbtype";
 | 
					import type { Document, QueryListOption } from "dbtype";
 | 
				
			||||||
import type { DocumentAccessor } from "../model/doc.ts";
 | 
					import type { DocumentAccessor } from "../model/doc.ts";
 | 
				
			||||||
import { AdminOnly, createPermissionCheck, Permission as Per } from "../permission/permission.ts";
 | 
					import type { AppEnv } from "../login.ts";
 | 
				
			||||||
 | 
					import { AdminOnly, createPermissionCheck, PERMISSION as Per } from "../permission/permission.ts";
 | 
				
			||||||
import { sendError } from "./error_handler.ts";
 | 
					import { sendError } from "./error_handler.ts";
 | 
				
			||||||
import { oshash } from "src/util/oshash.ts";
 | 
					import { oshash } from "src/util/oshash.ts";
 | 
				
			||||||
import { headComicPage, renderComicPage } from "./comic.ts";
 | 
					import { headComicPage, renderComicPage, type ResponseMeta } from "./comic.ts";
 | 
				
			||||||
 | 
					import { DocumentBodySchema } from "dbtype";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const searchQuerySchema = z.object({
 | 
				
			||||||
 | 
					    limit: z.string().optional(),
 | 
				
			||||||
 | 
					    cursor: z.string().optional(),
 | 
				
			||||||
 | 
					    word: z.string().optional(),
 | 
				
			||||||
 | 
					    content_type: z.string().optional(),
 | 
				
			||||||
 | 
					    offset: z.string().optional(),
 | 
				
			||||||
 | 
					    use_offset: z.string().optional(),
 | 
				
			||||||
 | 
					    allow_tag: z.string().optional(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const gidQuerySchema = z.object({
 | 
				
			||||||
 | 
					    gid: z.string(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const idParamSchema = z.object({
 | 
				
			||||||
 | 
					    num: z.coerce.number().int().nonnegative(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const idAndTagParamSchema = z.object({
 | 
				
			||||||
 | 
					    num: z.coerce.number().int().nonnegative(),
 | 
				
			||||||
 | 
					    tag: z.string(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const idAndPageParamSchema = z.object({
 | 
				
			||||||
 | 
					    num: z.coerce.number().int().nonnegative(),
 | 
				
			||||||
 | 
					    page: z.coerce.number().int().nonnegative(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateBodySchema = DocumentBodySchema.partial();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ensureFinite = <T extends number>(value: T, message: string) => {
 | 
				
			||||||
 | 
					    if (!Number.isFinite(value)) {
 | 
				
			||||||
 | 
					        throw sendError(400, message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return value;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const buildResponse = (meta: ResponseMeta, body: BodyInit | null = null) =>
 | 
				
			||||||
 | 
					    new Response(body, { status: meta.status, headers: meta.headers });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getContentRouter = (controller: DocumentAccessor) => {
 | 
					export const getContentRouter = (controller: DocumentAccessor) => {
 | 
				
			||||||
    return new Elysia({
 | 
					    const router = new Hono<AppEnv>();
 | 
				
			||||||
        name: "content-router",
 | 
					
 | 
				
			||||||
        prefix: "/doc",
 | 
					    router.get(
 | 
				
			||||||
    })
 | 
					        "/search",
 | 
				
			||||||
        .get("/search", async ({ query }) => {
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
            const limit = Math.min(Number(query.limit ?? 20), 100);
 | 
					        sValidator("query", searchQuerySchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const query = c.req.valid("query");
 | 
				
			||||||
 | 
					            const parsedLimit = Number(query.limit ?? 20);
 | 
				
			||||||
 | 
					            const limit = Math.min(Number.isFinite(parsedLimit) ? parsedLimit : 20, 100);
 | 
				
			||||||
            const option: QueryListOption = {
 | 
					            const option: QueryListOption = {
 | 
				
			||||||
                limit: limit,
 | 
					                limit,
 | 
				
			||||||
                allow_tag: query.allow_tag?.split(",") ?? [],
 | 
					                allow_tag: query.allow_tag?.split(",") ?? [],
 | 
				
			||||||
                word: query.word,
 | 
					                word: query.word,
 | 
				
			||||||
                cursor: query.cursor,
 | 
					                cursor: query.cursor ? ensureFinite(Number(query.cursor), "invalid cursor") : undefined,
 | 
				
			||||||
                eager_loading: true,
 | 
					                eager_loading: true,
 | 
				
			||||||
                offset: Number(query.offset),
 | 
					                offset: query.offset ? ensureFinite(Number(query.offset), "invalid offset") : undefined,
 | 
				
			||||||
                use_offset: query.use_offset === 'true',
 | 
					                use_offset: query.use_offset === "true",
 | 
				
			||||||
                content_type: query.content_type,
 | 
					                content_type: query.content_type,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            return await controller.findList(option);
 | 
					            return c.json(await controller.findList(option));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					    );
 | 
				
			||||||
            query: t.Object({
 | 
					
 | 
				
			||||||
                limit: t.Optional(t.String()),
 | 
					    router.get(
 | 
				
			||||||
                cursor: t.Optional(t.Number()),
 | 
					        "/_gid",
 | 
				
			||||||
                word: t.Optional(t.String()),
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
                content_type: t.Optional(t.String()),
 | 
					        sValidator("query", gidQuerySchema),
 | 
				
			||||||
                offset: t.Optional(t.Number()),
 | 
					        async (c) => {
 | 
				
			||||||
                use_offset: t.Optional(t.String()),
 | 
					            const { gid } = c.req.valid("query");
 | 
				
			||||||
                allow_tag: t.Optional(t.String()),
 | 
					            const gidList = gid.split(",").map((x) => Number.parseInt(x, 10));
 | 
				
			||||||
            })
 | 
					            if (gidList.some((x) => Number.isNaN(x)) || gidList.length > 100) {
 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .get("/_gid", async ({ query }) => {
 | 
					 | 
				
			||||||
            const gid_list = query.gid.split(",").map(x => Number.parseInt(x));
 | 
					 | 
				
			||||||
            if (gid_list.some(x => Number.isNaN(x)) || gid_list.length > 100) {
 | 
					 | 
				
			||||||
                throw sendError(400, "Invalid GID list");
 | 
					                throw sendError(400, "Invalid GID list");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return await controller.findByGidList(gid_list);
 | 
					            return c.json(await controller.findByGidList(gidList));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					    );
 | 
				
			||||||
            query: t.Object({ gid: t.String() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.get(
 | 
				
			||||||
        .get("/:num", async ({ params: { num } }) => {
 | 
					        "/:num",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            const document = await controller.findById(num, true);
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
            if (document === undefined) {
 | 
					            if (document === undefined) {
 | 
				
			||||||
                throw sendError(404, "document does not exist.");
 | 
					                throw sendError(404, "document does not exist.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return document;
 | 
					            return c.json(document);
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.post(
 | 
				
			||||||
        .post("/:num", async ({ params: { num }, body }) => {
 | 
					        "/:num",
 | 
				
			||||||
            const content_desc: Partial<Document> & { id: number } = {
 | 
					        AdminOnly,
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        sValidator("json", updateBodySchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
 | 
					            const body = c.req.valid("json");
 | 
				
			||||||
 | 
					            const contentDesc: Partial<Document> & { id: number } = {
 | 
				
			||||||
                id: num,
 | 
					                id: num,
 | 
				
			||||||
                ...body,
 | 
					                ...(body as Record<string, unknown>),
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            return await controller.update(content_desc);
 | 
					            return c.json(await controller.update(contentDesc));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: AdminOnly,
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() }),
 | 
					
 | 
				
			||||||
            body: t.Object({}, { additionalProperties: true })
 | 
					    router.delete(
 | 
				
			||||||
        })
 | 
					        "/:num",
 | 
				
			||||||
        .delete("/:num", async ({ params: { num } }) => {
 | 
					        AdminOnly,
 | 
				
			||||||
            return await controller.del(num);
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
        }, {
 | 
					        async (c) => {
 | 
				
			||||||
            beforeHandle: AdminOnly,
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					            return c.json(await controller.del(num));
 | 
				
			||||||
        })
 | 
					        },
 | 
				
			||||||
        .get("/:num/similars", async ({ params: { num } }) => {
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.get(
 | 
				
			||||||
 | 
					        "/:num/similars",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            const doc = await controller.findById(num, true);
 | 
					            const doc = await controller.findById(num, true);
 | 
				
			||||||
            if (doc === undefined) {
 | 
					            if (doc === undefined) {
 | 
				
			||||||
                throw sendError(404);
 | 
					                throw sendError(404);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return await controller.getSimilarDocument(doc);
 | 
					            return c.json(await controller.getSimilarDocument(doc));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.get(
 | 
				
			||||||
        .get("/:num/tags", async ({ params: { num } }) => {
 | 
					        "/:num/tags",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            const document = await controller.findById(num, true);
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
            if (document === undefined) {
 | 
					            if (document === undefined) {
 | 
				
			||||||
                throw sendError(404, "document does not exist.");
 | 
					                throw sendError(404, "document does not exist.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return document.tags;
 | 
					            return c.json(document.tags);
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.post(
 | 
				
			||||||
        .post("/:num/tags/:tag", async ({ params: { num, tag } }) => {
 | 
					        "/:num/tags/:tag",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.ModifyTag),
 | 
				
			||||||
 | 
					        sValidator("param", idAndTagParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num, tag } = c.req.valid("param");
 | 
				
			||||||
            const doc = await controller.findById(num);
 | 
					            const doc = await controller.findById(num);
 | 
				
			||||||
            if (doc === undefined) {
 | 
					            if (doc === undefined) {
 | 
				
			||||||
                throw sendError(404);
 | 
					                throw sendError(404);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return await controller.addTag(doc, tag);
 | 
					            return c.json(await controller.addTag(doc, tag));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.ModifyTag),
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric(), tag: t.String() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.delete(
 | 
				
			||||||
        .delete("/:num/tags/:tag", async ({ params: { num, tag } }) => {
 | 
					        "/:num/tags/:tag",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.ModifyTag),
 | 
				
			||||||
 | 
					        sValidator("param", idAndTagParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num, tag } = c.req.valid("param");
 | 
				
			||||||
            const doc = await controller.findById(num);
 | 
					            const doc = await controller.findById(num);
 | 
				
			||||||
            if (doc === undefined) {
 | 
					            if (doc === undefined) {
 | 
				
			||||||
                throw sendError(404);
 | 
					                throw sendError(404);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return await controller.delTag(doc, tag);
 | 
					            return c.json(await controller.delTag(doc, tag));
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: createPermissionCheck(Per.ModifyTag),
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric(), tag: t.String() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.post(
 | 
				
			||||||
        .post("/:num/_rehash", async ({ params: { num } }) => {
 | 
					        "/:num/_rehash",
 | 
				
			||||||
 | 
					        AdminOnly,
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            const doc = await controller.findById(num);
 | 
					            const doc = await controller.findById(num);
 | 
				
			||||||
            if (doc === undefined || doc.deleted_at !== null) {
 | 
					            if (doc === undefined || doc.deleted_at !== null) {
 | 
				
			||||||
                throw sendError(404);
 | 
					                throw sendError(404);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const filepath = join(doc.basepath, doc.filename);
 | 
					            const filepath = join(doc.basepath, doc.filename);
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                const new_hash = (await oshash(filepath)).toString();
 | 
					                const newHash = (await oshash(filepath)).toString();
 | 
				
			||||||
                return await controller.update({ id: num, content_hash: new_hash });
 | 
					                return c.json(await controller.update({ id: num, content_hash: newHash }));
 | 
				
			||||||
            } catch (e) {
 | 
					            } catch (error) {
 | 
				
			||||||
                if ((e as NodeJS.ErrnoException).code === "ENOENT") {
 | 
					                if ((error as NodeJS.ErrnoException).code === "ENOENT") {
 | 
				
			||||||
                    throw sendError(404, "file not found");
 | 
					                    throw sendError(404, "file not found");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                throw e;
 | 
					                throw error;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: AdminOnly,
 | 
					    );
 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					
 | 
				
			||||||
        })
 | 
					    router.post(
 | 
				
			||||||
        .post("/:num/_rescan", async ({ params: { num }, set }) => {
 | 
					        "/:num/_rescan",
 | 
				
			||||||
 | 
					        AdminOnly,
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
            const doc = await controller.findById(num, true);
 | 
					            const doc = await controller.findById(num, true);
 | 
				
			||||||
            if (doc === undefined) {
 | 
					            if (doc === undefined) {
 | 
				
			||||||
                throw sendError(404);
 | 
					                throw sendError(404);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            await controller.rescanDocument(doc);
 | 
					            await controller.rescanDocument(doc);
 | 
				
			||||||
            set.status = 204; // No Content
 | 
					            return c.body(null, 204);
 | 
				
			||||||
        }, {
 | 
					        },
 | 
				
			||||||
            beforeHandle: AdminOnly,
 | 
					 | 
				
			||||||
            params: t.Object({ num: t.Numeric() })
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .group("/:num", (app) =>
 | 
					 | 
				
			||||||
            app
 | 
					 | 
				
			||||||
                .derive(async ({ params: { num } }) => {
 | 
					 | 
				
			||||||
                    const docId = typeof num === "number" ? num : Number.parseInt(String(num));
 | 
					 | 
				
			||||||
                    if (Number.isNaN(docId)) {
 | 
					 | 
				
			||||||
                        throw sendError(400, "invalid document id");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const document = await controller.findById(docId, true);
 | 
					 | 
				
			||||||
                    if (document === undefined) {
 | 
					 | 
				
			||||||
                        throw sendError(404, "document does not exist.");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    return { document, docId };
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .head("/comic/thumbnail", async ({ document, request, set }) => {
 | 
					 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					 | 
				
			||||||
                        throw sendError(404);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const path = join(document.basepath, document.filename);
 | 
					 | 
				
			||||||
                    await headComicPage({
 | 
					 | 
				
			||||||
                        path,
 | 
					 | 
				
			||||||
                        page: 0,
 | 
					 | 
				
			||||||
                        reqHeaders: request.headers,
 | 
					 | 
				
			||||||
                        set,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }, {
 | 
					 | 
				
			||||||
                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					 | 
				
			||||||
                    params: t.Object({ num: t.Numeric() }),
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .get("/comic/thumbnail", async ({ document, request, set }) => {
 | 
					 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					 | 
				
			||||||
                        throw sendError(404);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const path = join(document.basepath, document.filename);
 | 
					 | 
				
			||||||
                    const body = await renderComicPage({
 | 
					 | 
				
			||||||
                        path,
 | 
					 | 
				
			||||||
                        page: 0,
 | 
					 | 
				
			||||||
                        reqHeaders: request.headers,
 | 
					 | 
				
			||||||
                        set,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                    return body ?? undefined;
 | 
					 | 
				
			||||||
                }, {
 | 
					 | 
				
			||||||
                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					 | 
				
			||||||
                    params: t.Object({ num: t.Numeric() }),
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .head("/comic/:page", async ({ document, params: { page }, request, set }) => {
 | 
					 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					 | 
				
			||||||
                        throw sendError(404);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const pageIndex = page;
 | 
					 | 
				
			||||||
                    const path = join(document.basepath, document.filename);
 | 
					 | 
				
			||||||
                    await headComicPage({
 | 
					 | 
				
			||||||
                        path,
 | 
					 | 
				
			||||||
                        page: pageIndex,
 | 
					 | 
				
			||||||
                        reqHeaders: request.headers,
 | 
					 | 
				
			||||||
                        set,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }, {
 | 
					 | 
				
			||||||
                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					 | 
				
			||||||
                    params: t.Object({ num: t.Numeric(), page: t.Numeric() }),
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .get("/comic/:page", async ({ document, params: { page }, request, set }) => {
 | 
					 | 
				
			||||||
                    if (document.content_type !== "comic") {
 | 
					 | 
				
			||||||
                        throw sendError(404);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    const pageIndex = page;
 | 
					 | 
				
			||||||
                    const path = join(document.basepath, document.filename);
 | 
					 | 
				
			||||||
                    const body = await renderComicPage({
 | 
					 | 
				
			||||||
                        path,
 | 
					 | 
				
			||||||
                        page: pageIndex,
 | 
					 | 
				
			||||||
                        reqHeaders: request.headers,
 | 
					 | 
				
			||||||
                        set,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                    return body ?? undefined;
 | 
					 | 
				
			||||||
                }, {
 | 
					 | 
				
			||||||
                    beforeHandle: createPermissionCheck(Per.QueryContent),
 | 
					 | 
				
			||||||
                    params: t.Object({ num: t.Numeric(), page: t.Numeric() }),
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.on(
 | 
				
			||||||
 | 
					        "HEAD",
 | 
				
			||||||
 | 
					        "/:num/comic/thumbnail",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
 | 
					            if (document === undefined || document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                throw sendError(404);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const meta: ResponseMeta = { status: 200, headers: {} };
 | 
				
			||||||
 | 
					            await headComicPage({
 | 
				
			||||||
 | 
					                path: join(document.basepath, document.filename),
 | 
				
			||||||
 | 
					                page: 0,
 | 
				
			||||||
 | 
					                reqHeaders: c.req.raw.headers,
 | 
				
			||||||
 | 
					                set: meta,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return buildResponse(meta);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.get(
 | 
				
			||||||
 | 
					        "/:num/comic/thumbnail",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num } = c.req.valid("param");
 | 
				
			||||||
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
 | 
					            if (document === undefined || document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                throw sendError(404);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const meta: ResponseMeta = { status: 200, headers: {} };
 | 
				
			||||||
 | 
					            const body = await renderComicPage({
 | 
				
			||||||
 | 
					                path: join(document.basepath, document.filename),
 | 
				
			||||||
 | 
					                page: 0,
 | 
				
			||||||
 | 
					                reqHeaders: c.req.raw.headers,
 | 
				
			||||||
 | 
					                set: meta,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return buildResponse(meta, body ?? null);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.on(
 | 
				
			||||||
 | 
					        "HEAD",
 | 
				
			||||||
 | 
					        "/:num/comic/:page",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idAndPageParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num, page } = c.req.valid("param");
 | 
				
			||||||
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
 | 
					            if (document === undefined || document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                throw sendError(404);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const meta: ResponseMeta = { status: 200, headers: {} };
 | 
				
			||||||
 | 
					            await headComicPage({
 | 
				
			||||||
 | 
					                path: join(document.basepath, document.filename),
 | 
				
			||||||
 | 
					                page,
 | 
				
			||||||
 | 
					                reqHeaders: c.req.raw.headers,
 | 
				
			||||||
 | 
					                set: meta,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return buildResponse(meta);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router.get(
 | 
				
			||||||
 | 
					        "/:num/comic/:page",
 | 
				
			||||||
 | 
					        createPermissionCheck(Per.QueryContent),
 | 
				
			||||||
 | 
					        sValidator("param", idAndPageParamSchema),
 | 
				
			||||||
 | 
					        async (c) => {
 | 
				
			||||||
 | 
					            const { num, page } = c.req.valid("param");
 | 
				
			||||||
 | 
					            const document = await controller.findById(num, true);
 | 
				
			||||||
 | 
					            if (document === undefined || document.content_type !== "comic") {
 | 
				
			||||||
 | 
					                throw sendError(404);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const meta: ResponseMeta = { status: 200, headers: {} };
 | 
				
			||||||
 | 
					            const body = await renderComicPage({
 | 
				
			||||||
 | 
					                path: join(document.basepath, document.filename),
 | 
				
			||||||
 | 
					                page,
 | 
				
			||||||
 | 
					                reqHeaders: c.req.raw.headers,
 | 
				
			||||||
 | 
					                set: meta,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return buildResponse(meta, body ?? null);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return router;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default getContentRouter;
 | 
					export default getContentRouter;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { ZodError } from "dbtype";
 | 
					import { ZodError } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ErrorFormat {
 | 
					export interface ErrorFormat {
 | 
				
			||||||
	code: number;
 | 
						code: number;
 | 
				
			||||||
| 
						 | 
					@ -21,30 +21,36 @@ const code_to_message_table: { [key: number]: string | undefined } = {
 | 
				
			||||||
	404: "NotFound",
 | 
						404: "NotFound",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const error_handler = ({ code, error, set }: { code: string, error: Error, set: { status?: number | string } }) => {
 | 
					export const mapErrorToResponse = (error: Error): { status: number; body: ErrorFormat } => {
 | 
				
			||||||
    if (error instanceof ClientRequestError) {
 | 
					    if (error instanceof ClientRequestError) {
 | 
				
			||||||
        set.status = error.code;
 | 
					 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 | 
					            status: error.code,
 | 
				
			||||||
 | 
					            body: {
 | 
				
			||||||
                code: error.code,
 | 
					                code: error.code,
 | 
				
			||||||
                message: code_to_message_table[error.code] ?? "",
 | 
					                message: code_to_message_table[error.code] ?? "",
 | 
				
			||||||
                detail: error.message,
 | 
					                detail: error.message,
 | 
				
			||||||
        } satisfies ErrorFormat;
 | 
					            },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (error instanceof ZodError) {
 | 
					    if (error instanceof ZodError) {
 | 
				
			||||||
        set.status = 400;
 | 
					 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 | 
					            status: 400,
 | 
				
			||||||
 | 
					            body: {
 | 
				
			||||||
                code: 400,
 | 
					                code: 400,
 | 
				
			||||||
                message: "BadRequest",
 | 
					                message: "BadRequest",
 | 
				
			||||||
            detail: error.errors.map((x) => x.message).join(", "),
 | 
					                detail: error.issues.map((issue) => issue.message).join(", "),
 | 
				
			||||||
        } satisfies ErrorFormat;
 | 
					            },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    set.status = 500;
 | 
					 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					        status: 500,
 | 
				
			||||||
 | 
					        body: {
 | 
				
			||||||
            code: 500,
 | 
					            code: 500,
 | 
				
			||||||
            message: "Internal Server Error",
 | 
					            message: "Internal Server Error",
 | 
				
			||||||
            detail: error.message,
 | 
					            detail: error.message,
 | 
				
			||||||
    }
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sendError = (code: number, message?: string): never => {
 | 
					export const sendError = (code: number, message?: string): never => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,23 @@
 | 
				
			||||||
import { Elysia, t, type Static } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { sValidator } from "@hono/standard-validator";
 | 
				
			||||||
import type { Kysely } from "kysely";
 | 
					import type { Kysely } from "kysely";
 | 
				
			||||||
import type { db } from "dbtype";
 | 
					import type { db } from "dbtype";
 | 
				
			||||||
import { AdminOnly, Permission } from "../permission/permission.ts";
 | 
					import type { AppEnv } from "../login.ts";
 | 
				
			||||||
 | 
					import { AdminOnly, PERMISSIONS } from "../permission/permission.ts";
 | 
				
			||||||
 | 
					import type { Permission } from "../permission/permission.ts";
 | 
				
			||||||
import { get_setting, updatePersistedSetting, type PersistedSettingUpdate } from "../SettingConfig.ts";
 | 
					import { get_setting, updatePersistedSetting, type PersistedSettingUpdate } from "../SettingConfig.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const permissionOptions = Object.values(Permission).sort() as string[];
 | 
					const permissionOptions = [...PERMISSIONS].sort() as Permission[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateBodySchema = t.Object({
 | 
					const updateBodySchema = z.object({
 | 
				
			||||||
	secure: t.Optional(t.Boolean()),
 | 
						secure: z.boolean().optional(),
 | 
				
			||||||
	cli: t.Optional(t.Boolean()),
 | 
						cli: z.boolean().optional(),
 | 
				
			||||||
	forbid_remote_admin_login: t.Optional(t.Boolean()),
 | 
						forbid_remote_admin_login: z.boolean().optional(),
 | 
				
			||||||
	guest: t.Optional(t.Array(t.Enum(Permission))),
 | 
						guest: z.array(z.enum(PERMISSIONS)).optional(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UpdateBody = Static<typeof updateBodySchema>;
 | 
					type UpdateBody = z.infer<typeof updateBodySchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SettingResponse = {
 | 
					type SettingResponse = {
 | 
				
			||||||
	env: {
 | 
						env: {
 | 
				
			||||||
| 
						 | 
					@ -25,9 +29,9 @@ type SettingResponse = {
 | 
				
			||||||
		secure: boolean;
 | 
							secure: boolean;
 | 
				
			||||||
		cli: boolean;
 | 
							cli: boolean;
 | 
				
			||||||
		forbid_remote_admin_login: boolean;
 | 
							forbid_remote_admin_login: boolean;
 | 
				
			||||||
		guest: string[];
 | 
							guest: Permission[];
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	permissions: string[];
 | 
						permissions: Permission[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const buildResponse = (): SettingResponse => {
 | 
					const buildResponse = (): SettingResponse => {
 | 
				
			||||||
| 
						 | 
					@ -48,14 +52,17 @@ const buildResponse = (): SettingResponse => {
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createSettingsRouter = (db: Kysely<db.DB>) =>
 | 
					export const createSettingsRouter = (db: Kysely<db.DB>) => {
 | 
				
			||||||
	new Elysia({ name: "settings-router" })
 | 
						const router = new Hono<AppEnv>();
 | 
				
			||||||
		.get("/settings", () => {
 | 
					
 | 
				
			||||||
			return buildResponse()}, {
 | 
						router.get("/settings", AdminOnly, (c) => c.json(buildResponse()));
 | 
				
			||||||
			beforeHandle: AdminOnly,
 | 
					
 | 
				
			||||||
		})
 | 
						router.patch(
 | 
				
			||||||
		.patch("/settings", async ({ body }) => {
 | 
							"/settings",
 | 
				
			||||||
			const payload = body as UpdateBody;
 | 
							AdminOnly,
 | 
				
			||||||
 | 
							sValidator("json", updateBodySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const payload = c.req.valid("json") as UpdateBody;
 | 
				
			||||||
			const update: PersistedSettingUpdate = {
 | 
								const update: PersistedSettingUpdate = {
 | 
				
			||||||
				secure: payload.secure,
 | 
									secure: payload.secure,
 | 
				
			||||||
				cli: payload.cli,
 | 
									cli: payload.cli,
 | 
				
			||||||
| 
						 | 
					@ -63,10 +70,11 @@ export const createSettingsRouter = (db: Kysely<db.DB>) =>
 | 
				
			||||||
				guest: payload.guest,
 | 
									guest: payload.guest,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			await updatePersistedSetting(db, update);
 | 
								await updatePersistedSetting(db, update);
 | 
				
			||||||
			return buildResponse();
 | 
								return c.json(buildResponse());
 | 
				
			||||||
		}, {
 | 
							},
 | 
				
			||||||
			beforeHandle: AdminOnly,
 | 
						);
 | 
				
			||||||
			body: updateBodySchema,
 | 
					
 | 
				
			||||||
		});
 | 
						return router;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default createSettingsRouter;
 | 
					export default createSettingsRouter;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,33 +1,48 @@
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { sValidator } from "@hono/standard-validator";
 | 
				
			||||||
import type { TagAccessor } from "../model/tag.ts";
 | 
					import type { TagAccessor } from "../model/tag.ts";
 | 
				
			||||||
import { createPermissionCheck, Permission } from "../permission/permission.ts";
 | 
					import type { AppEnv } from "../login.ts";
 | 
				
			||||||
 | 
					import { createPermissionCheck, PERMISSION } from "../permission/permission.ts";
 | 
				
			||||||
import { sendError } from "./error_handler.ts";
 | 
					import { sendError } from "./error_handler.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tagQuerySchema = z.object({
 | 
				
			||||||
 | 
						withCount: z.string().optional(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tagParamSchema = z.object({
 | 
				
			||||||
 | 
						tag_name: z.string(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getTagRounter(tagController: TagAccessor) {
 | 
					export function getTagRounter(tagController: TagAccessor) {
 | 
				
			||||||
	return new Elysia({ name: "tags-router",
 | 
						const router = new Hono<AppEnv>();
 | 
				
			||||||
		prefix: "/tags",
 | 
					
 | 
				
			||||||
	 })
 | 
						router.get(
 | 
				
			||||||
		.get("/", async ({ query }) => {
 | 
							"/",
 | 
				
			||||||
			if (query.withCount !== undefined) {
 | 
							createPermissionCheck(PERMISSION.QueryContent),
 | 
				
			||||||
				return await tagController.getAllTagCount();
 | 
							sValidator("query", tagQuerySchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const { withCount } = c.req.valid("query");
 | 
				
			||||||
 | 
								if (withCount !== undefined) {
 | 
				
			||||||
 | 
									return c.json(await tagController.getAllTagCount());
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return await tagController.getAllTagList();
 | 
								return c.json(await tagController.getAllTagList());
 | 
				
			||||||
		}, {
 | 
							},
 | 
				
			||||||
			beforeHandle: createPermissionCheck(Permission.QueryContent),
 | 
						);
 | 
				
			||||||
			query: t.Object({
 | 
					
 | 
				
			||||||
				withCount: t.Optional(t.String()),
 | 
						router.get(
 | 
				
			||||||
			})
 | 
							"/:tag_name",
 | 
				
			||||||
		})
 | 
							createPermissionCheck(PERMISSION.QueryContent),
 | 
				
			||||||
		.get("/:tag_name", async ({ params: { tag_name } }) => {
 | 
							sValidator("param", tagParamSchema),
 | 
				
			||||||
 | 
							async (c) => {
 | 
				
			||||||
 | 
								const { tag_name } = c.req.valid("param");
 | 
				
			||||||
			const tag = await tagController.getTagByName(tag_name);
 | 
								const tag = await tagController.getTagByName(tag_name);
 | 
				
			||||||
			if (!tag) {
 | 
								if (!tag) {
 | 
				
			||||||
				sendError(404, "tags not found");
 | 
									sendError(404, "tags not found");
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return tag;
 | 
								return c.json(tag);
 | 
				
			||||||
		}, {
 | 
							},
 | 
				
			||||||
			beforeHandle: createPermissionCheck(Permission.QueryContent),
 | 
						);
 | 
				
			||||||
			params: t.Object({
 | 
					
 | 
				
			||||||
				tag_name: t.String(),
 | 
						return router;
 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,35 +1,30 @@
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
import { cors } from "@elysiajs/cors";
 | 
					import { cors } from "hono/cors";
 | 
				
			||||||
import { staticPlugin } from "./util/static.ts";
 | 
					import { serve } from "@hono/node-server";
 | 
				
			||||||
import { html } from "@elysiajs/html";
 | 
					import { readFileSync } from "node:fs";
 | 
				
			||||||
 | 
					import { createInterface as createReadlineInterface } from "node:readline";
 | 
				
			||||||
 | 
					import { config } from "dotenv";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { connectDB } from "./database.ts";
 | 
					import { connectDB } from "./database.ts";
 | 
				
			||||||
import { createDiffRouter, DiffManager } from "./diff/mod.ts";
 | 
					import { createDiffRouter, DiffManager } from "./diff/mod.ts";
 | 
				
			||||||
import { get_setting, initializeSetting } from "./SettingConfig.ts";
 | 
					import { get_setting, initializeSetting } from "./SettingConfig.ts";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import { readFileSync } from "node:fs";
 | 
					 | 
				
			||||||
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
 | 
					import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
 | 
				
			||||||
import { createLoginRouter, createUserHandler, getAdmin, isAdminFirst } from "./login.ts";
 | 
					import { createLoginRouter, createUserHandler, getAdmin, isAdminFirst, type AppEnv } from "./login.ts";
 | 
				
			||||||
import getContentRouter from "./route/contents.ts";
 | 
					import getContentRouter from "./route/contents.ts";
 | 
				
			||||||
import { error_handler } from "./route/error_handler.ts";
 | 
					import { mapErrorToResponse } from "./route/error_handler.ts";
 | 
				
			||||||
import { createSettingsRouter } from "./route/settings.ts";
 | 
					import { createSettingsRouter } from "./route/settings.ts";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import { createInterface as createReadlineInterface } from "node:readline";
 | 
					 | 
				
			||||||
import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
 | 
					import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
 | 
				
			||||||
import { loadComicConfig } from "./diff/watcher/ComicConfig.ts";
 | 
					import { loadComicConfig } from "./diff/watcher/ComicConfig.ts";
 | 
				
			||||||
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod.ts";
 | 
					 | 
				
			||||||
import { getTagRounter } from "./route/tags.ts";
 | 
					import { getTagRounter } from "./route/tags.ts";
 | 
				
			||||||
import { node } from "@elysiajs/node";
 | 
					import { createStaticRouter } from "./util/static.ts";
 | 
				
			||||||
import { openapi } from "@elysiajs/openapi";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { config } from "dotenv";
 | 
					 | 
				
			||||||
config();
 | 
					config();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createMetaTagContent(key: string, value: string) {
 | 
					function createMetaTagContent(key: string, value: string) {
 | 
				
			||||||
	return `<meta property="${key}" content="${value}">`;
 | 
						return `<meta property="${key}" content="${value}">`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createOgTagContent(title: string, description:string, image: string) {
 | 
					function createOgTagContent(title: string, description: string, image: string) {
 | 
				
			||||||
	return [
 | 
						return [
 | 
				
			||||||
		createMetaTagContent("og:title", title),
 | 
							createMetaTagContent("og:title", title),
 | 
				
			||||||
		createMetaTagContent("og:type", "website"),
 | 
							createMetaTagContent("og:type", "website"),
 | 
				
			||||||
| 
						 | 
					@ -46,6 +41,12 @@ function makeMetaTagInjectedHTML(html: string, tagContent: string) {
 | 
				
			||||||
	return html.replace("<!--MetaTag-Outlet-->", tagContent);
 | 
						return html.replace("<!--MetaTag-Outlet-->", tagContent);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const htmlResponse = (content: string, status = 200) =>
 | 
				
			||||||
 | 
						new Response(content, {
 | 
				
			||||||
 | 
							status,
 | 
				
			||||||
 | 
							headers: { "Content-Type": "text/html; charset=utf-8" },
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeError = (error: unknown): Error => {
 | 
					const normalizeError = (error: unknown): Error => {
 | 
				
			||||||
	if (error instanceof Error) {
 | 
						if (error instanceof Error) {
 | 
				
			||||||
		return error;
 | 
							return error;
 | 
				
			||||||
| 
						 | 
					@ -60,7 +61,6 @@ const normalizeError = (error: unknown): Error => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function create_server() {
 | 
					export async function create_server() {
 | 
				
			||||||
	const db = await connectDB();
 | 
						const db = await connectDB();
 | 
				
			||||||
	await initializeSetting(db);
 | 
						await initializeSetting(db);
 | 
				
			||||||
| 
						 | 
					@ -69,9 +69,9 @@ export async function create_server() {
 | 
				
			||||||
	const userController = createSqliteUserController(db);
 | 
						const userController = createSqliteUserController(db);
 | 
				
			||||||
	const documentController = createSqliteDocumentAccessor(db);
 | 
						const documentController = createSqliteDocumentAccessor(db);
 | 
				
			||||||
	const tagController = createSqliteTagController(db);
 | 
						const tagController = createSqliteTagController(db);
 | 
				
			||||||
    const diffManger = new DiffManager(documentController);
 | 
						const diffManager = new DiffManager(documentController);
 | 
				
			||||||
	const comicConfig = await loadComicConfig(db);
 | 
						const comicConfig = await loadComicConfig(db);
 | 
				
			||||||
    await diffManger.register("comic", createComicWatcher(comicConfig.watch));
 | 
						await diffManager.register("comic", createComicWatcher(comicConfig.watch));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (setting.cli) {
 | 
						if (setting.cli) {
 | 
				
			||||||
		const userAdmin = await getAdmin(userController);
 | 
							const userAdmin = await getAdmin(userController);
 | 
				
			||||||
| 
						 | 
					@ -80,9 +80,9 @@ export async function create_server() {
 | 
				
			||||||
				input: process.stdin,
 | 
									input: process.stdin,
 | 
				
			||||||
				output: process.stdout,
 | 
									output: process.stdout,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
            const pw = await new Promise((res: (data: string) => void) => {
 | 
								const pw = await new Promise<string>((resolve) => {
 | 
				
			||||||
				rl.question("put admin password :", (data) => {
 | 
									rl.question("put admin password :", (data) => {
 | 
				
			||||||
                    res(data);
 | 
										resolve(data);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			rl.close();
 | 
								rl.close();
 | 
				
			||||||
| 
						 | 
					@ -90,66 +90,69 @@ export async function create_server() {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const index_html = readFileSync("dist/index.html", "utf-8");
 | 
						const indexHtml = readFileSync("dist/index.html", "utf-8");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const app = new Elysia({ 
 | 
						const app = new Hono<AppEnv>();
 | 
				
			||||||
        adapter: node(),
 | 
					
 | 
				
			||||||
    })
 | 
						app.use("*", cors());
 | 
				
			||||||
        .use(cors())
 | 
						app.use("*", createUserHandler(userController));
 | 
				
			||||||
        .use(staticPlugin({
 | 
					
 | 
				
			||||||
 | 
						const staticRouter = createStaticRouter({
 | 
				
			||||||
		assets: "dist/assets",
 | 
							assets: "dist/assets",
 | 
				
			||||||
		prefix: "/assets",
 | 
							prefix: "/assets",
 | 
				
			||||||
		headers: {
 | 
							headers: {
 | 
				
			||||||
			"X-Content-Type-Options": "nosniff",
 | 
								"X-Content-Type-Options": "nosniff",
 | 
				
			||||||
			"Cache-Control": setting.mode === "development" ? "no-cache" : "public, max-age=3600",
 | 
								"Cache-Control": setting.mode === "development" ? "no-cache" : "public, max-age=3600",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						app.route("/", staticRouter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.onError((err, _c) => {
 | 
				
			||||||
 | 
							const { status, body } = mapErrorToResponse(normalizeError(err));
 | 
				
			||||||
 | 
							return new Response(JSON.stringify(body), {
 | 
				
			||||||
 | 
								status,
 | 
				
			||||||
 | 
								headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const api = new Hono<AppEnv>();
 | 
				
			||||||
 | 
						api.route("/diff", createDiffRouter(diffManager));
 | 
				
			||||||
 | 
						api.route("/doc", getContentRouter(documentController));
 | 
				
			||||||
 | 
						api.route("/tags", getTagRounter(tagController));
 | 
				
			||||||
 | 
						api.route("/", createSettingsRouter(db));
 | 
				
			||||||
 | 
						api.route("/user", createLoginRouter(userController));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.route("/api", api);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.get("/doc/:id", async (c) => {
 | 
				
			||||||
 | 
							const param = c.req.param("id");
 | 
				
			||||||
 | 
							const docId = Number.parseInt(param, 10);
 | 
				
			||||||
 | 
							if (Number.isNaN(docId)) {
 | 
				
			||||||
 | 
								const meta = createOgTagContent("Not Found Doc", "Not Found", "");
 | 
				
			||||||
 | 
								return htmlResponse(makeMetaTagInjectedHTML(indexHtml, meta), 400);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
        }))
 | 
					 | 
				
			||||||
        .use(openapi())
 | 
					 | 
				
			||||||
        .use(html())
 | 
					 | 
				
			||||||
        .onError((context) => error_handler({
 | 
					 | 
				
			||||||
            code: typeof context.code === "number" ? String(context.code) : context.code,
 | 
					 | 
				
			||||||
            error: normalizeError(context.error),
 | 
					 | 
				
			||||||
            set: context.set,
 | 
					 | 
				
			||||||
        }))
 | 
					 | 
				
			||||||
        .use(createUserHandler(userController))
 | 
					 | 
				
			||||||
        .group("/api", (app) => app
 | 
					 | 
				
			||||||
            .use(createDiffRouter(diffManger))
 | 
					 | 
				
			||||||
            .use(getContentRouter(documentController))
 | 
					 | 
				
			||||||
            .use(getTagRounter(tagController))
 | 
					 | 
				
			||||||
            .use(createSettingsRouter(db))
 | 
					 | 
				
			||||||
            .use(createLoginRouter(userController))
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .get("/doc/:id", async ({ params: { id }, set }) => {
 | 
					 | 
				
			||||||
            const docId = Number.parseInt(id, 10);
 | 
					 | 
				
			||||||
		const doc = await documentController.findById(docId, true);
 | 
							const doc = await documentController.findById(docId, true);
 | 
				
			||||||
            let meta;
 | 
							if (!doc) {
 | 
				
			||||||
            if (doc === undefined) {
 | 
								const meta = createOgTagContent("Not Found Doc", "Not Found", "");
 | 
				
			||||||
                set.status = 404;
 | 
								return htmlResponse(makeMetaTagInjectedHTML(indexHtml, meta), 404);
 | 
				
			||||||
                meta = createOgTagContent("Not Found Doc", "Not Found", "");
 | 
							}
 | 
				
			||||||
            } else {
 | 
							const meta = createOgTagContent(
 | 
				
			||||||
                set.status = 200;
 | 
					 | 
				
			||||||
                meta = createOgTagContent(
 | 
					 | 
				
			||||||
			doc.title,
 | 
								doc.title,
 | 
				
			||||||
			doc.tags.join(", "),
 | 
								doc.tags.join(", "),
 | 
				
			||||||
			`https://aeolian.monoid.top/api/doc/${docId}/comic/thumbnail`,
 | 
								`https://aeolian.monoid.top/api/doc/${docId}/comic/thumbnail`,
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
            }
 | 
							return htmlResponse(makeMetaTagInjectedHTML(indexHtml, meta));
 | 
				
			||||||
            return makeMetaTagInjectedHTML(index_html, meta);
 | 
						});
 | 
				
			||||||
        }, {
 | 
					 | 
				
			||||||
            params: t.Object({ id: t.String() })
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .get("/", () => index_html)
 | 
					 | 
				
			||||||
        .get("/doc/*", () => index_html)
 | 
					 | 
				
			||||||
        .get("/search", () => index_html)
 | 
					 | 
				
			||||||
        .get("/login", () => index_html)
 | 
					 | 
				
			||||||
        .get("/profile", () => index_html)
 | 
					 | 
				
			||||||
        .get("/difference", () => index_html)
 | 
					 | 
				
			||||||
        .get("/setting", () => index_html)
 | 
					 | 
				
			||||||
        .get("/tags", () => index_html)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.listen({
 | 
						const spaPaths = ["/", "/doc/*", "/search", "/login", "/profile", "/difference", "/setting", "/tags"] as const;
 | 
				
			||||||
        port: setting.port,
 | 
						for (const path of spaPaths) {
 | 
				
			||||||
 | 
							app.get(path, () => htmlResponse(indexHtml));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						serve({
 | 
				
			||||||
 | 
							fetch: app.fetch,
 | 
				
			||||||
		hostname: setting.hostname,
 | 
							hostname: setting.hostname,
 | 
				
			||||||
 | 
							port: setting.port,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	console.log(`Server started at http://${setting.hostname}:${setting.port}/`);
 | 
						console.log(`Server started at http://${setting.hostname}:${setting.port}/`);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +0,0 @@
 | 
				
			||||||
import { Elysia } from "elysia";
 | 
					 | 
				
			||||||
import { get_setting } from "./SettingConfig.ts";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const SettingPlugin = new Elysia({
 | 
					 | 
				
			||||||
	name: "setting",
 | 
					 | 
				
			||||||
	seed: "ServerConfig",
 | 
					 | 
				
			||||||
}).derive(() => ({ setting: get_setting() }));
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
export {};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
import { Elysia, NotFoundError, type Context } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
 | 
					import type { Context } from "hono";
 | 
				
			||||||
import { createReadStream } from "node:fs";
 | 
					import { createReadStream } from "node:fs";
 | 
				
			||||||
import { stat } from "node:fs/promises";
 | 
					import { stat } from "node:fs/promises";
 | 
				
			||||||
 | 
					import { Readable } from "node:stream";
 | 
				
			||||||
import { extname, resolve } from "node:path";
 | 
					import { extname, resolve } from "node:path";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MIME_TYPES: Record<string, string> = {
 | 
					const MIME_TYPES: Record<string, string> = {
 | 
				
			||||||
| 
						 | 
					@ -46,36 +48,44 @@ export type StaticPluginOptions = {
 | 
				
			||||||
    headers?: Record<string, string>;
 | 
					    headers?: Record<string, string>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const buildResponse = (status: number, headers: Record<string, string>, body: BodyInit | null) =>
 | 
				
			||||||
 | 
					    new Response(body, { status, headers });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resolveWildcard = (context: Context, wildcardParam: string) => {
 | 
				
			||||||
 | 
					    const wildcard = context.req.param(wildcardParam) ?? "";
 | 
				
			||||||
 | 
					    if (wildcard.length === 0) {
 | 
				
			||||||
 | 
					        return undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const decoded = decodeURI(wildcard);
 | 
				
			||||||
 | 
					        if (decoded.includes("\0")) {
 | 
				
			||||||
 | 
					            return undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return decoded;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					        return undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const handleStaticRequest = async (
 | 
					const handleStaticRequest = async (
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
    rootDir: string,
 | 
					    rootDir: string,
 | 
				
			||||||
    headersTemplate: Record<string, string>,
 | 
					    headersTemplate: Record<string, string>,
 | 
				
			||||||
    ctx: Context,
 | 
					 | 
				
			||||||
    sendBody: boolean,
 | 
					    sendBody: boolean,
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
    const wildcard = ctx.params?.["*"] ?? "";
 | 
					    const pathFragment = resolveWildcard(context, "*");
 | 
				
			||||||
    if (wildcard.length === 0) {
 | 
					    if (!pathFragment) {
 | 
				
			||||||
        throw new NotFoundError();
 | 
					        return buildResponse(404, {}, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let decoded: string;
 | 
					    const absolutePath = resolve(rootDir, pathFragment);
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        decoded = decodeURI(wildcard);
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
        throw new NotFoundError();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (decoded.includes("\0")) {
 | 
					 | 
				
			||||||
        throw new NotFoundError();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const absolutePath = resolve(rootDir, decoded);
 | 
					 | 
				
			||||||
    if (!isPathWithinRoot(absolutePath, rootDir)) {
 | 
					    if (!isPathWithinRoot(absolutePath, rootDir)) {
 | 
				
			||||||
        throw new NotFoundError();
 | 
					        return buildResponse(404, {}, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fileStat = await stat(absolutePath).catch(() => undefined);
 | 
					    const fileStat = await stat(absolutePath).catch(() => undefined);
 | 
				
			||||||
    if (!fileStat || fileStat.isDirectory()) {
 | 
					    if (!fileStat || fileStat.isDirectory()) {
 | 
				
			||||||
        throw new NotFoundError();
 | 
					        return buildResponse(404, {}, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const responseHeaders: Record<string, string> = {
 | 
					    const responseHeaders: Record<string, string> = {
 | 
				
			||||||
| 
						 | 
					@ -86,44 +96,40 @@ const handleStaticRequest = async (
 | 
				
			||||||
    const etag = generateETag(fileStat.mtimeMs, fileStat.size);
 | 
					    const etag = generateETag(fileStat.mtimeMs, fileStat.size);
 | 
				
			||||||
    responseHeaders.ETag = etag;
 | 
					    responseHeaders.ETag = etag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ifNoneMatch = ctx.request.headers.get("if-none-match");
 | 
					    const ifNoneMatch = context.req.header("if-none-match");
 | 
				
			||||||
    if (ifNoneMatch && ifNoneMatch === etag) {
 | 
					    if (ifNoneMatch && ifNoneMatch === etag) {
 | 
				
			||||||
        ctx.set.status = 304;
 | 
					        return buildResponse(304, responseHeaders, null);
 | 
				
			||||||
        ctx.set.headers = responseHeaders;
 | 
					 | 
				
			||||||
        return undefined;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ifModifiedSince = ctx.request.headers.get("if-modified-since");
 | 
					    const ifModifiedSince = context.req.header("if-modified-since");
 | 
				
			||||||
    if (ifModifiedSince) {
 | 
					    if (ifModifiedSince) {
 | 
				
			||||||
        const since = new Date(ifModifiedSince);
 | 
					        const since = new Date(ifModifiedSince);
 | 
				
			||||||
        if (!Number.isNaN(since.getTime()) && fileStat.mtime <= since) {
 | 
					        if (!Number.isNaN(since.getTime()) && fileStat.mtime <= since) {
 | 
				
			||||||
            ctx.set.status = 304;
 | 
					            return buildResponse(304, responseHeaders, null);
 | 
				
			||||||
            ctx.set.headers = responseHeaders;
 | 
					 | 
				
			||||||
            return undefined;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    responseHeaders["Content-Type"] = getMimeType(absolutePath);
 | 
					    responseHeaders["Content-Type"] = getMimeType(absolutePath);
 | 
				
			||||||
    responseHeaders["Content-Length"] = fileStat.size.toString();
 | 
					    responseHeaders["Content-Length"] = fileStat.size.toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.set.status = 200;
 | 
					 | 
				
			||||||
    ctx.set.headers = responseHeaders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!sendBody) {
 | 
					    if (!sendBody) {
 | 
				
			||||||
        return undefined;
 | 
					        return buildResponse(200, responseHeaders, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return createReadStream(absolutePath);
 | 
					    const nodeStream = createReadStream(absolutePath);
 | 
				
			||||||
 | 
					    const body = Readable.toWeb(nodeStream) as unknown as ReadableStream<Uint8Array>;
 | 
				
			||||||
 | 
					    return buildResponse(200, responseHeaders, body);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const staticPlugin = ({ assets, prefix = "/public", headers = {} }: StaticPluginOptions) => {
 | 
					export const createStaticRouter = ({ assets, prefix = "/public", headers = {} }: StaticPluginOptions) => {
 | 
				
			||||||
    const trimmedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
 | 
					    const trimmedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
 | 
				
			||||||
    const normalizedPrefix = trimmedPrefix.startsWith("/") ? trimmedPrefix : `/${trimmedPrefix}`;
 | 
					    const normalizedPrefix = trimmedPrefix.startsWith("/") ? trimmedPrefix : `/${trimmedPrefix}`;
 | 
				
			||||||
    const wildcardRoute = normalizedPrefix === "/" ? "/*" : `${normalizedPrefix}/*`;
 | 
					    const wildcardRoute = normalizedPrefix === "/" ? "/*" : `${normalizedPrefix}/*`;
 | 
				
			||||||
    const rootDir = resolve(process.cwd(), assets);
 | 
					    const rootDir = resolve(process.cwd(), assets);
 | 
				
			||||||
    const headersTemplate = { ...headers };
 | 
					    const headersTemplate = { ...headers };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new Elysia({ name: "node-static" })
 | 
					    const router = new Hono();
 | 
				
			||||||
        .get(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, true))
 | 
					    router.get(wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, true));
 | 
				
			||||||
        .head(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, false));
 | 
					    router.on("HEAD", wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, false));
 | 
				
			||||||
 | 
					    return router;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,27 @@
 | 
				
			||||||
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
 | 
					import { beforeEach, describe, expect, it, vi } from "vitest";
 | 
				
			||||||
import { Elysia } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
import { createDiffRouter } from "../src/diff/router.ts";
 | 
					import { createDiffRouter } from "../src/diff/router.ts";
 | 
				
			||||||
import type { DiffManager } from "../src/diff/diff.ts";
 | 
					import type { DiffManager } from "../src/diff/diff.ts";
 | 
				
			||||||
 | 
					import type { AppEnv, AuthStore } from "../src/login.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const adminUser = { username: "admin", permission: [] as string[] };
 | 
					const adminUser: AuthStore["user"] = { username: "admin", permission: [] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createTestApp = (diffManager: DiffManager) => {
 | 
					const createTestApp = (diffManager: DiffManager) => {
 | 
				
			||||||
	const authPlugin = new Elysia({ name: "test-auth" })
 | 
						const app = new Hono<AppEnv>();
 | 
				
			||||||
		.state("user", adminUser)
 | 
					 | 
				
			||||||
		.derive(() => ({ user: adminUser }));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return new Elysia({ name: "diff-test" })
 | 
						app.use("*", async (c, next) => {
 | 
				
			||||||
		.use(authPlugin)
 | 
							const auth: AuthStore = {
 | 
				
			||||||
		.use(createDiffRouter(diffManager));
 | 
								user: adminUser,
 | 
				
			||||||
 | 
								refreshed: false,
 | 
				
			||||||
 | 
								authenticated: true,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							c.set("auth", auth);
 | 
				
			||||||
 | 
							await next();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.route("/diff", createDiffRouter(diffManager));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return app;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("Diff router integration", () => {
 | 
					describe("Diff router integration", () => {
 | 
				
			||||||
| 
						 | 
					@ -43,15 +52,8 @@ describe("Diff router integration", () => {
 | 
				
			||||||
		app = createTestApp(diffManager);
 | 
							app = createTestApp(diffManager);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	afterEach(async () => {
 | 
					 | 
				
			||||||
		if (app?.server) {
 | 
					 | 
				
			||||||
			await app.stop();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		vi.clearAllMocks();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	it("GET /diff/list returns grouped pending items", async () => {
 | 
						it("GET /diff/list returns grouped pending items", async () => {
 | 
				
			||||||
		const response = await app.handle(new Request("http://localhost/diff/list"));
 | 
							const response = await app.fetch(new Request("http://localhost/diff/list"));
 | 
				
			||||||
		expect(response.status).toBe(200);
 | 
							expect(response.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const payload = await response.json();
 | 
							const payload = await response.json();
 | 
				
			||||||
| 
						 | 
					@ -77,7 +79,7 @@ describe("Diff router integration", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		commitMock.mockResolvedValueOnce(555);
 | 
							commitMock.mockResolvedValueOnce(555);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const response = await app.handle(request);
 | 
							const response = await app.fetch(request);
 | 
				
			||||||
		expect(response.status).toBe(200);
 | 
							expect(response.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const payload = await response.json();
 | 
							const payload = await response.json();
 | 
				
			||||||
| 
						 | 
					@ -92,7 +94,7 @@ describe("Diff router integration", () => {
 | 
				
			||||||
			body: JSON.stringify({ type: "comic" }),
 | 
								body: JSON.stringify({ type: "comic" }),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const response = await app.handle(request);
 | 
							const response = await app.fetch(request);
 | 
				
			||||||
		expect(response.status).toBe(200);
 | 
							expect(response.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const payload = await response.json();
 | 
							const payload = await response.json();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,12 @@
 | 
				
			||||||
import { describe, expect, it } from "vitest";
 | 
					import { describe, expect, it } from "vitest";
 | 
				
			||||||
import { ClientRequestError, error_handler } from "../src/route/error_handler.ts";
 | 
					import { z } from "zod";
 | 
				
			||||||
import { DocumentBodySchema } from "dbtype";
 | 
					import { ClientRequestError, mapErrorToResponse } from "../src/route/error_handler.ts";
 | 
				
			||||||
 | 
					 | 
				
			||||||
const createSet = () => ({ status: undefined as number | string | undefined });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("error_handler", () => {
 | 
					describe("error_handler", () => {
 | 
				
			||||||
	it("formats ClientRequestError with provided status", () => {
 | 
						it("formats ClientRequestError with provided status", () => {
 | 
				
			||||||
		const set = createSet();
 | 
							const { status, body } = mapErrorToResponse(new ClientRequestError(400, "invalid payload"));
 | 
				
			||||||
		const result = error_handler({
 | 
							expect(status).toBe(400);
 | 
				
			||||||
			code: "UNKNOWN",
 | 
							expect(body).toEqual({
 | 
				
			||||||
			error: new ClientRequestError(400, "invalid payload"),
 | 
					 | 
				
			||||||
			set,
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		expect(set.status).toBe(400);
 | 
					 | 
				
			||||||
		expect(result).toEqual({
 | 
					 | 
				
			||||||
			code: 400,
 | 
								code: 400,
 | 
				
			||||||
			message: "BadRequest",
 | 
								message: "BadRequest",
 | 
				
			||||||
			detail: "invalid payload",
 | 
								detail: "invalid payload",
 | 
				
			||||||
| 
						 | 
					@ -22,35 +14,26 @@ describe("error_handler", () => {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("coerces ZodError into a 400 response", () => {
 | 
						it("coerces ZodError into a 400 response", () => {
 | 
				
			||||||
		const parseResult = DocumentBodySchema.safeParse({});
 | 
							const schema = z.object({ foo: z.string() });
 | 
				
			||||||
		const set = createSet();
 | 
							const parseResult = schema.safeParse({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (parseResult.success) {
 | 
							if (parseResult.success) {
 | 
				
			||||||
			throw new Error("Expected validation error");
 | 
								throw new Error("Expected validation error");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const result = error_handler({
 | 
							const { status, body } = mapErrorToResponse(parseResult.error);
 | 
				
			||||||
			code: "VALIDATION",
 | 
					 | 
				
			||||||
			error: parseResult.error,
 | 
					 | 
				
			||||||
			set,
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expect(set.status).toBe(400);
 | 
							expect(status).toBe(400);
 | 
				
			||||||
		expect(result.code).toBe(400);
 | 
							expect(body.code).toBe(400);
 | 
				
			||||||
		expect(result.message).toBe("BadRequest");
 | 
							expect(body.message).toBe("BadRequest");
 | 
				
			||||||
		expect(result.detail).toContain("Required");
 | 
							expect(body.detail).toContain("expected string");
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("defaults to 500 for unexpected errors", () => {
 | 
						it("defaults to 500 for unexpected errors", () => {
 | 
				
			||||||
		const set = createSet();
 | 
							const { status, body } = mapErrorToResponse(new Error("boom"));
 | 
				
			||||||
		const result = error_handler({
 | 
					 | 
				
			||||||
			code: "INTERNAL_SERVER_ERROR",
 | 
					 | 
				
			||||||
			error: new Error("boom"),
 | 
					 | 
				
			||||||
			set,
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expect(set.status).toBe(500);
 | 
							expect(status).toBe(500);
 | 
				
			||||||
		expect(result).toEqual({
 | 
							expect(body).toEqual({
 | 
				
			||||||
			code: 500,
 | 
								code: 500,
 | 
				
			||||||
			message: "Internal Server Error",
 | 
								message: "Internal Server Error",
 | 
				
			||||||
			detail: "boom",
 | 
								detail: "boom",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,13 @@
 | 
				
			||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
 | 
					import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
 | 
				
			||||||
import { Elysia } from "elysia";
 | 
					import { Hono } from "hono";
 | 
				
			||||||
import { Kysely, SqliteDialect } from "kysely";
 | 
					import { Kysely, SqliteDialect } from "kysely";
 | 
				
			||||||
import SqliteDatabase from "better-sqlite3";
 | 
					import SqliteDatabase from "better-sqlite3";
 | 
				
			||||||
import type { db } from "dbtype";
 | 
					import type { db } from "dbtype";
 | 
				
			||||||
import { createSettingsRouter } from "../src/route/settings.ts";
 | 
					import { createSettingsRouter } from "../src/route/settings.ts";
 | 
				
			||||||
import { error_handler } from "../src/route/error_handler.ts";
 | 
					import { mapErrorToResponse } from "../src/route/error_handler.ts";
 | 
				
			||||||
import { get_setting, refreshSetting } from "../src/SettingConfig.ts";
 | 
					import { get_setting, refreshSetting } from "../src/SettingConfig.ts";
 | 
				
			||||||
import { Permission } from "../src/permission/permission.ts";
 | 
					import { PERMISSIONS } from "../src/permission/permission.ts";
 | 
				
			||||||
 | 
					import type { AppEnv, AuthStore } from "../src/login.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeError = (error: unknown): Error => {
 | 
					const normalizeError = (error: unknown): Error => {
 | 
				
			||||||
	if (error instanceof Error) {
 | 
						if (error instanceof Error) {
 | 
				
			||||||
| 
						 | 
					@ -56,36 +57,40 @@ describe("settings router", () => {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const createTestApp = (username: string) => {
 | 
						const createTestApp = (username: string) => {
 | 
				
			||||||
		const user = { username, permission: [] as string[] };
 | 
							const app = new Hono<AppEnv>();
 | 
				
			||||||
		return new Elysia({ name: `settings-test-${username}` })
 | 
							const auth: AuthStore = {
 | 
				
			||||||
			.state("user", user)
 | 
								user: { username, permission: [] },
 | 
				
			||||||
			.derive(() => ({ user }))
 | 
								refreshed: false,
 | 
				
			||||||
			.onError((context) =>
 | 
								authenticated: true,
 | 
				
			||||||
				error_handler({
 | 
							};
 | 
				
			||||||
					code: typeof context.code === "number" ? String(context.code) : context.code,
 | 
					
 | 
				
			||||||
					error: normalizeError(context.error),
 | 
							app.use("*", async (c, next) => {
 | 
				
			||||||
					set: context.set,
 | 
								c.set("auth", auth);
 | 
				
			||||||
				}),
 | 
								await next();
 | 
				
			||||||
			)
 | 
							});
 | 
				
			||||||
			.use(createSettingsRouter(database));
 | 
					
 | 
				
			||||||
 | 
							app.onError((err) => {
 | 
				
			||||||
 | 
								const { status, body } = mapErrorToResponse(normalizeError(err));
 | 
				
			||||||
 | 
								return new Response(JSON.stringify(body), {
 | 
				
			||||||
 | 
									status,
 | 
				
			||||||
 | 
									headers: { "Content-Type": "application/json" },
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							app.route("/", createSettingsRouter(database));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return app;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("rejects access for non-admin users", async () => {
 | 
						it("rejects access for non-admin users", async () => {
 | 
				
			||||||
		const app = createTestApp("guest");
 | 
							const app = createTestApp("guest");
 | 
				
			||||||
		try {
 | 
							const response = await app.fetch(new Request("http://localhost/settings"));
 | 
				
			||||||
			const response = await app.handle(new Request("http://localhost/settings"));
 | 
					 | 
				
			||||||
		expect(response.status).toBe(403);
 | 
							expect(response.status).toBe(403);
 | 
				
			||||||
		} finally {
 | 
					 | 
				
			||||||
			if (app.server) {
 | 
					 | 
				
			||||||
				await app.stop();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("returns current configuration for admin", async () => {
 | 
						it("returns current configuration for admin", async () => {
 | 
				
			||||||
		const app = createTestApp("admin");
 | 
							const app = createTestApp("admin");
 | 
				
			||||||
		try {
 | 
							const response = await app.fetch(new Request("http://localhost/settings"));
 | 
				
			||||||
			const response = await app.handle(new Request("http://localhost/settings"));
 | 
					 | 
				
			||||||
		expect(response.status).toBe(200);
 | 
							expect(response.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const payload = await response.json();
 | 
							const payload = await response.json();
 | 
				
			||||||
| 
						 | 
					@ -104,17 +109,11 @@ describe("settings router", () => {
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		expect(Array.isArray(payload.permissions)).toBe(true);
 | 
							expect(Array.isArray(payload.permissions)).toBe(true);
 | 
				
			||||||
			expect(new Set(payload.permissions)).toEqual(new Set(Object.values(Permission)));
 | 
							expect(new Set(payload.permissions)).toEqual(new Set(PERMISSIONS));
 | 
				
			||||||
		} finally {
 | 
					 | 
				
			||||||
			if (app.server) {
 | 
					 | 
				
			||||||
				await app.stop();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("updates persisted settings and returns the new state", async () => {
 | 
						it("updates persisted settings and returns the new state", async () => {
 | 
				
			||||||
		const app = createTestApp("admin");
 | 
							const app = createTestApp("admin");
 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
		const request = new Request("http://localhost/settings", {
 | 
							const request = new Request("http://localhost/settings", {
 | 
				
			||||||
			method: "PATCH",
 | 
								method: "PATCH",
 | 
				
			||||||
			headers: { "content-type": "application/json" },
 | 
								headers: { "content-type": "application/json" },
 | 
				
			||||||
| 
						 | 
					@ -126,7 +125,7 @@ describe("settings router", () => {
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const response = await app.handle(request);
 | 
							const response = await app.fetch(request);
 | 
				
			||||||
		expect(response.status).toBe(200);
 | 
							expect(response.status).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const payload = await response.json();
 | 
							const payload = await response.json();
 | 
				
			||||||
| 
						 | 
					@ -138,7 +137,7 @@ describe("settings router", () => {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// A follow-up GET should reflect the updated values
 | 
							// A follow-up GET should reflect the updated values
 | 
				
			||||||
			const followUp = await app.handle(new Request("http://localhost/settings"));
 | 
							const followUp = await app.fetch(new Request("http://localhost/settings"));
 | 
				
			||||||
		expect(followUp.status).toBe(200);
 | 
							expect(followUp.status).toBe(200);
 | 
				
			||||||
		const followUpPayload = await followUp.json();
 | 
							const followUpPayload = await followUp.json();
 | 
				
			||||||
		expect(followUpPayload.persisted).toEqual({
 | 
							expect(followUpPayload.persisted).toEqual({
 | 
				
			||||||
| 
						 | 
					@ -147,10 +146,5 @@ describe("settings router", () => {
 | 
				
			||||||
			forbid_remote_admin_login: false,
 | 
								forbid_remote_admin_login: false,
 | 
				
			||||||
			guest: ["QueryContent"],
 | 
								guest: ["QueryContent"],
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		} finally {
 | 
					 | 
				
			||||||
			if (app.server) {
 | 
					 | 
				
			||||||
				await app.stop();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										397
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										397
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -148,8 +148,8 @@ importers:
 | 
				
			||||||
  packages/dbtype:
 | 
					  packages/dbtype:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      zod:
 | 
					      zod:
 | 
				
			||||||
        specifier: ^3.23.8
 | 
					        specifier: ^4.1.12
 | 
				
			||||||
        version: 3.23.8
 | 
					        version: 4.1.12
 | 
				
			||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@types/better-sqlite3':
 | 
					      '@types/better-sqlite3':
 | 
				
			||||||
        specifier: ^7.6.9
 | 
					        specifier: ^7.6.9
 | 
				
			||||||
| 
						 | 
					@ -169,18 +169,18 @@ importers:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  packages/server:
 | 
					  packages/server:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@elysiajs/cors':
 | 
					      '@hono/node-server':
 | 
				
			||||||
        specifier: ^1.3.3
 | 
					        specifier: ^1.19.6
 | 
				
			||||||
        version: 1.3.3(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))
 | 
					        version: 1.19.6(hono@4.10.4)
 | 
				
			||||||
      '@elysiajs/html':
 | 
					      '@hono/standard-validator':
 | 
				
			||||||
        specifier: ^1.3.1
 | 
					        specifier: ^0.1.5
 | 
				
			||||||
        version: 1.3.1(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))(typescript@5.8.3)
 | 
					        version: 0.1.5(@standard-schema/spec@1.0.0)(hono@4.10.4)
 | 
				
			||||||
      '@elysiajs/node':
 | 
					      '@hono/zod-openapi':
 | 
				
			||||||
        specifier: ^1.4.1
 | 
					        specifier: ^1.1.4
 | 
				
			||||||
        version: 1.4.1(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))
 | 
					        version: 1.1.4(hono@4.10.4)(zod@4.1.12)
 | 
				
			||||||
      '@elysiajs/openapi':
 | 
					      '@hono/zod-validator':
 | 
				
			||||||
        specifier: ^1.4.11
 | 
					        specifier: ^0.7.4
 | 
				
			||||||
        version: 1.4.11(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))
 | 
					        version: 0.7.4(hono@4.10.4)(zod@4.1.12)
 | 
				
			||||||
      '@std/async':
 | 
					      '@std/async':
 | 
				
			||||||
        specifier: npm:@jsr/std__async@^1.0.13
 | 
					        specifier: npm:@jsr/std__async@^1.0.13
 | 
				
			||||||
        version: '@jsr/std__async@1.0.13'
 | 
					        version: '@jsr/std__async@1.0.13'
 | 
				
			||||||
| 
						 | 
					@ -199,9 +199,9 @@ importers:
 | 
				
			||||||
      dotenv:
 | 
					      dotenv:
 | 
				
			||||||
        specifier: ^16.5.0
 | 
					        specifier: ^16.5.0
 | 
				
			||||||
        version: 16.5.0
 | 
					        version: 16.5.0
 | 
				
			||||||
      elysia:
 | 
					      hono:
 | 
				
			||||||
        specifier: ^1.4.9
 | 
					        specifier: ^4.10.4
 | 
				
			||||||
        version: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
 | 
					        version: 4.10.4
 | 
				
			||||||
      jose:
 | 
					      jose:
 | 
				
			||||||
        specifier: ^5.10.0
 | 
					        specifier: ^5.10.0
 | 
				
			||||||
        version: 5.10.0
 | 
					        version: 5.10.0
 | 
				
			||||||
| 
						 | 
					@ -214,6 +214,9 @@ importers:
 | 
				
			||||||
      tiny-async-pool:
 | 
					      tiny-async-pool:
 | 
				
			||||||
        specifier: ^1.3.0
 | 
					        specifier: ^1.3.0
 | 
				
			||||||
        version: 1.3.0
 | 
					        version: 1.3.0
 | 
				
			||||||
 | 
					      zod:
 | 
				
			||||||
 | 
					        specifier: ^4.1.12
 | 
				
			||||||
 | 
					        version: 4.1.12
 | 
				
			||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@types/better-sqlite3':
 | 
					      '@types/better-sqlite3':
 | 
				
			||||||
        specifier: ^7.6.13
 | 
					        specifier: ^7.6.13
 | 
				
			||||||
| 
						 | 
					@ -248,6 +251,11 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==}
 | 
					    resolution: {integrity: sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==}
 | 
				
			||||||
    hasBin: true
 | 
					    hasBin: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@asteasolutions/zod-to-openapi@8.1.0':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-tQFxVs05J/6QXXqIzj6rTRk3nj1HFs4pe+uThwE95jL5II2JfpVXkK+CqkO7aT0Do5AYqO6LDrKpleLUFXgY+g==}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      zod: ^4.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@babel/code-frame@7.27.1':
 | 
					  '@babel/code-frame@7.27.1':
 | 
				
			||||||
    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
 | 
					    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
 | 
				
			||||||
    engines: {node: '>=6.9.0'}
 | 
					    engines: {node: '>=6.9.0'}
 | 
				
			||||||
| 
						 | 
					@ -412,29 +420,6 @@ packages:
 | 
				
			||||||
    cpu: [x64]
 | 
					    cpu: [x64]
 | 
				
			||||||
    os: [win32]
 | 
					    os: [win32]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@borewit/text-codec@0.1.1':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/cors@1.3.3':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      elysia: '>= 1.3.0'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/html@1.3.1':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-jOWUfvL9vZ2Gs3uCx2w4Po+jxOwRD/sXW3JgvOAD3rEjX0NuygwcvixtbONSzAH8lFhaDBbHAtmCfpue46X9IQ==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      elysia: '>= 1.3.0'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/node@1.4.1':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-2wAALwHK3IYi1XJPnxfp1xJsvps5FqqcQqe+QXjYlGQvsmSG+vI5wNDIuvIlB+6p9NE/laLbqV0aFromf3X7yg==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      elysia: '>= 1.4.0'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/openapi@1.4.11':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      elysia: '>= 1.4.0'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@esbuild/aix-ppc64@0.21.5':
 | 
					  '@esbuild/aix-ppc64@0.21.5':
 | 
				
			||||||
    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
 | 
					    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					    engines: {node: '>=12'}
 | 
				
			||||||
| 
						 | 
					@ -756,6 +741,31 @@ packages:
 | 
				
			||||||
  '@floating-ui/utils@0.2.9':
 | 
					  '@floating-ui/utils@0.2.9':
 | 
				
			||||||
    resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
 | 
					    resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/node-server@1.19.6':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==}
 | 
				
			||||||
 | 
					    engines: {node: '>=18.14.1'}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      hono: ^4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/standard-validator@0.1.5':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w==}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      '@standard-schema/spec': 1.0.0
 | 
				
			||||||
 | 
					      hono: '>=3.9.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/zod-openapi@1.1.4':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-4BbOtd6oKg20yo6HLluVbEycBLLIfdKX5o/gUSoKZ2uBmeP4Og/VDfIX3k9pbNEX5W3fRkuPeVjGA+zaQDVY1A==}
 | 
				
			||||||
 | 
					    engines: {node: '>=16.0.0'}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      hono: '>=4.3.6'
 | 
				
			||||||
 | 
					      zod: ^4.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/zod-validator@0.7.4':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q==}
 | 
				
			||||||
 | 
					    peerDependencies:
 | 
				
			||||||
 | 
					      hono: '>=3.9.0'
 | 
				
			||||||
 | 
					      zod: ^3.25.0 || ^4.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@humanwhocodes/config-array@0.13.0':
 | 
					  '@humanwhocodes/config-array@0.13.0':
 | 
				
			||||||
    resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
 | 
					    resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
 | 
				
			||||||
    engines: {node: '>=10.10.0'}
 | 
					    engines: {node: '>=10.10.0'}
 | 
				
			||||||
| 
						 | 
					@ -794,17 +804,6 @@ packages:
 | 
				
			||||||
  '@jsr/std__async@1.0.13':
 | 
					  '@jsr/std__async@1.0.13':
 | 
				
			||||||
    resolution: {integrity: sha512-GEApyNtzauJ0kEZ/GxebSkdEN0t29qJtkw+WEvzYTwkL6fHX8cq3YWzRjCqHu+4jMl+rpHiwyr/lfitNInntzA==, tarball: https://npm.jsr.io/~/11/@jsr/std__async/1.0.13.tgz}
 | 
					    resolution: {integrity: sha512-GEApyNtzauJ0kEZ/GxebSkdEN0t29qJtkw+WEvzYTwkL6fHX8cq3YWzRjCqHu+4jMl+rpHiwyr/lfitNInntzA==, tarball: https://npm.jsr.io/~/11/@jsr/std__async/1.0.13.tgz}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@kitajs/html@4.2.9':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA==}
 | 
					 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@kitajs/ts-html-plugin@4.1.2':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-XE9iIe93TELBdQSvNC3xxXOPDhkcK7on4Oi2HUKhln3jAc5hzn1o33uzjHCYhLeW36r/LXCT70beoXRCFcuTxQ==}
 | 
					 | 
				
			||||||
    hasBin: true
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      '@kitajs/html': ^4.2.5
 | 
					 | 
				
			||||||
      typescript: ^5.6.2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@nodelib/fs.scandir@2.1.5':
 | 
					  '@nodelib/fs.scandir@2.1.5':
 | 
				
			||||||
    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
 | 
					    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
 | 
				
			||||||
    engines: {node: '>= 8'}
 | 
					    engines: {node: '>= 8'}
 | 
				
			||||||
| 
						 | 
					@ -1337,8 +1336,8 @@ packages:
 | 
				
			||||||
    cpu: [x64]
 | 
					    cpu: [x64]
 | 
				
			||||||
    os: [win32]
 | 
					    os: [win32]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@sinclair/typebox@0.34.41':
 | 
					  '@standard-schema/spec@1.0.0':
 | 
				
			||||||
    resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
 | 
					    resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@swc/core-darwin-arm64@1.11.31':
 | 
					  '@swc/core-darwin-arm64@1.11.31':
 | 
				
			||||||
    resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==}
 | 
					    resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==}
 | 
				
			||||||
| 
						 | 
					@ -1424,13 +1423,6 @@ packages:
 | 
				
			||||||
  '@tanstack/virtual-core@3.13.9':
 | 
					  '@tanstack/virtual-core@3.13.9':
 | 
				
			||||||
    resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
 | 
					    resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@tokenizer/inflate@0.2.7':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
 | 
					 | 
				
			||||||
    engines: {node: '>=18'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@tokenizer/token@0.3.0':
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@ts-morph/common@0.19.0':
 | 
					  '@ts-morph/common@0.19.0':
 | 
				
			||||||
    resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==}
 | 
					    resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1767,10 +1759,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
 | 
					    resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
 | 
				
			||||||
    engines: {node: '>=6'}
 | 
					    engines: {node: '>=6'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cliui@8.0.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
 | 
					 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  clone@1.0.4:
 | 
					  clone@1.0.4:
 | 
				
			||||||
    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
 | 
					    resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
 | 
				
			||||||
    engines: {node: '>=0.8'}
 | 
					    engines: {node: '>=0.8'}
 | 
				
			||||||
| 
						 | 
					@ -1809,13 +1797,6 @@ packages:
 | 
				
			||||||
  convert-source-map@2.0.0:
 | 
					  convert-source-map@2.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 | 
					    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cookie-es@2.0.0:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cookie@1.0.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
 | 
					 | 
				
			||||||
    engines: {node: '>=18'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cosmiconfig@8.3.6:
 | 
					  cosmiconfig@8.3.6:
 | 
				
			||||||
    resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
 | 
					    resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
 | 
				
			||||||
    engines: {node: '>=14'}
 | 
					    engines: {node: '>=14'}
 | 
				
			||||||
| 
						 | 
					@ -1829,14 +1810,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
 | 
					    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
 | 
				
			||||||
    engines: {node: '>= 8'}
 | 
					    engines: {node: '>= 8'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  crossws@0.4.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      srvx: '>=0.7.1'
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      srvx:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cssesc@3.0.0:
 | 
					  cssesc@3.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
 | 
					    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
 | 
				
			||||||
    engines: {node: '>=4'}
 | 
					    engines: {node: '>=4'}
 | 
				
			||||||
| 
						 | 
					@ -1902,15 +1875,6 @@ packages:
 | 
				
			||||||
      supports-color:
 | 
					      supports-color:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  debug@4.4.3:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
 | 
					 | 
				
			||||||
    engines: {node: '>=6.0'}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      supports-color: '*'
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      supports-color:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  decimal.js-light@2.5.1:
 | 
					  decimal.js-light@2.5.1:
 | 
				
			||||||
    resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
 | 
					    resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1986,20 +1950,6 @@ packages:
 | 
				
			||||||
  electron-to-chromium@1.5.165:
 | 
					  electron-to-chromium@1.5.165:
 | 
				
			||||||
    resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==}
 | 
					    resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  elysia@1.4.9:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-BWNhA8DoKQvlQTjAUkMAmNeso24U+ibZxY/8LN96qSDK/6eevaX59r3GISow699JPxSnFY3gLMUzJzCLYVtbvg==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      '@sinclair/typebox': '>= 0.34.0 < 1'
 | 
					 | 
				
			||||||
      exact-mirror: '>= 0.0.9'
 | 
					 | 
				
			||||||
      file-type: '>= 20.0.0'
 | 
					 | 
				
			||||||
      openapi-types: '>= 12.0.0'
 | 
					 | 
				
			||||||
      typescript: '>= 5.0.0'
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      file-type:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
      typescript:
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  emoji-regex@8.0.0:
 | 
					  emoji-regex@8.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 | 
					    resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2096,14 +2046,6 @@ packages:
 | 
				
			||||||
  eventemitter3@4.0.7:
 | 
					  eventemitter3@4.0.7:
 | 
				
			||||||
    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
 | 
					    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  exact-mirror@0.2.0:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-XnP8M3gIk6vLnpZY4A/RsAXwQLyqj7lCRJhiCZMt3NaIIXHsfzpJRsvG5DMSSYYrjm2xTBGCrPbG4Z9JublGBg==}
 | 
					 | 
				
			||||||
    peerDependencies:
 | 
					 | 
				
			||||||
      '@sinclair/typebox': ^0.34.15
 | 
					 | 
				
			||||||
    peerDependenciesMeta:
 | 
					 | 
				
			||||||
      '@sinclair/typebox':
 | 
					 | 
				
			||||||
        optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  execa@7.2.0:
 | 
					  execa@7.2.0:
 | 
				
			||||||
    resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
 | 
					    resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
 | 
				
			||||||
    engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
 | 
					    engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
 | 
				
			||||||
| 
						 | 
					@ -2116,9 +2058,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
 | 
					    resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
 | 
				
			||||||
    engines: {node: '>=12.0.0'}
 | 
					    engines: {node: '>=12.0.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fast-decode-uri-component@1.0.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fast-deep-equal@3.1.3:
 | 
					  fast-deep-equal@3.1.3:
 | 
				
			||||||
    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 | 
					    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2143,17 +2082,10 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
 | 
					    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
 | 
				
			||||||
    engines: {node: ^12.20 || >= 14.13}
 | 
					    engines: {node: ^12.20 || >= 14.13}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fflate@0.8.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  file-entry-cache@6.0.1:
 | 
					  file-entry-cache@6.0.1:
 | 
				
			||||||
    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
 | 
					    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
 | 
				
			||||||
    engines: {node: ^10.12.0 || >=12.0.0}
 | 
					    engines: {node: ^10.12.0 || >=12.0.0}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  file-type@21.0.0:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
 | 
					 | 
				
			||||||
    engines: {node: '>=20'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  file-uri-to-path@1.0.0:
 | 
					  file-uri-to-path@1.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
 | 
					    resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2205,10 +2137,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
 | 
					    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
 | 
				
			||||||
    engines: {node: '>=6.9.0'}
 | 
					    engines: {node: '>=6.9.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get-caller-file@2.0.5:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
 | 
					 | 
				
			||||||
    engines: {node: 6.* || 8.* || >= 10.*}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get-nonce@1.0.1:
 | 
					  get-nonce@1.0.1:
 | 
				
			||||||
    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
 | 
					    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
 | 
				
			||||||
    engines: {node: '>=6'}
 | 
					    engines: {node: '>=6'}
 | 
				
			||||||
| 
						 | 
					@ -2273,6 +2201,10 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
 | 
					    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
 | 
				
			||||||
    engines: {node: '>= 0.4'}
 | 
					    engines: {node: '>= 0.4'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hono@4.10.4:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==}
 | 
				
			||||||
 | 
					    engines: {node: '>=16.9.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  https-proxy-agent@6.2.1:
 | 
					  https-proxy-agent@6.2.1:
 | 
				
			||||||
    resolution: {integrity: sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==}
 | 
					    resolution: {integrity: sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==}
 | 
				
			||||||
    engines: {node: '>= 14'}
 | 
					    engines: {node: '>= 14'}
 | 
				
			||||||
| 
						 | 
					@ -2653,8 +2585,8 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
 | 
					    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					    engines: {node: '>=12'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  openapi-types@12.1.3:
 | 
					  openapi3-ts@4.5.0:
 | 
				
			||||||
    resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
 | 
					    resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  optionator@0.9.4:
 | 
					  optionator@0.9.4:
 | 
				
			||||||
    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
 | 
					    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
 | 
				
			||||||
| 
						 | 
					@ -2910,10 +2842,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==}
 | 
					    resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==}
 | 
				
			||||||
    engines: {node: '>=8'}
 | 
					    engines: {node: '>=8'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  require-directory@2.1.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
 | 
					 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  resolve-from@4.0.0:
 | 
					  resolve-from@4.0.0:
 | 
				
			||||||
    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
 | 
					    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
 | 
				
			||||||
    engines: {node: '>=4'}
 | 
					    engines: {node: '>=4'}
 | 
				
			||||||
| 
						 | 
					@ -3018,11 +2946,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
 | 
					    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  srvx@0.8.9:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-wYc3VLZHRzwYrWJhkEqkhLb31TI0SOkfYZDkUhXdp3NoCnNS0FqajiQszZZjfow/VYEuc6Q5sZh9nM6kPy2NBQ==}
 | 
					 | 
				
			||||||
    engines: {node: '>=20.16.0'}
 | 
					 | 
				
			||||||
    hasBin: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  stackback@0.0.2:
 | 
					  stackback@0.0.2:
 | 
				
			||||||
    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 | 
					    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3068,10 +2991,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
 | 
					    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
 | 
				
			||||||
    engines: {node: '>=8'}
 | 
					    engines: {node: '>=8'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  strtok3@10.3.4:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
 | 
					 | 
				
			||||||
    engines: {node: '>=18'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  sucrase@3.35.0:
 | 
					  sucrase@3.35.0:
 | 
				
			||||||
    resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
 | 
					    resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
 | 
				
			||||||
    engines: {node: '>=16 || 14 >=14.17'}
 | 
					    engines: {node: '>=16 || 14 >=14.17'}
 | 
				
			||||||
| 
						 | 
					@ -3155,10 +3074,6 @@ packages:
 | 
				
			||||||
    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
 | 
					    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
 | 
				
			||||||
    engines: {node: '>=8.0'}
 | 
					    engines: {node: '>=8.0'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  token-types@6.1.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
 | 
					 | 
				
			||||||
    engines: {node: '>=14.16'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ts-api-utils@1.4.3:
 | 
					  ts-api-utils@1.4.3:
 | 
				
			||||||
    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
 | 
					    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
 | 
				
			||||||
    engines: {node: '>=16'}
 | 
					    engines: {node: '>=16'}
 | 
				
			||||||
| 
						 | 
					@ -3204,10 +3119,6 @@ packages:
 | 
				
			||||||
    engines: {node: '>=14.17'}
 | 
					    engines: {node: '>=14.17'}
 | 
				
			||||||
    hasBin: true
 | 
					    hasBin: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  uint8array-extras@1.5.0:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
 | 
					 | 
				
			||||||
    engines: {node: '>=18'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  undici-types@6.21.0:
 | 
					  undici-types@6.21.0:
 | 
				
			||||||
    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 | 
					    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3362,10 +3273,6 @@ packages:
 | 
				
			||||||
  wrappy@1.0.2:
 | 
					  wrappy@1.0.2:
 | 
				
			||||||
    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 | 
					    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  y18n@5.0.8:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
 | 
					 | 
				
			||||||
    engines: {node: '>=10'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yallist@3.1.1:
 | 
					  yallist@3.1.1:
 | 
				
			||||||
    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 | 
					    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3374,24 +3281,16 @@ packages:
 | 
				
			||||||
    engines: {node: '>= 14.6'}
 | 
					    engines: {node: '>= 14.6'}
 | 
				
			||||||
    hasBin: true
 | 
					    hasBin: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  yargs-parser@21.1.1:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
 | 
					 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yargs@17.7.2:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
 | 
					 | 
				
			||||||
    engines: {node: '>=12'}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yocto-queue@0.1.0:
 | 
					  yocto-queue@0.1.0:
 | 
				
			||||||
    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
 | 
					    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
 | 
				
			||||||
    engines: {node: '>=10'}
 | 
					    engines: {node: '>=10'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  zod@3.23.8:
 | 
					 | 
				
			||||||
    resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  zod@3.25.56:
 | 
					  zod@3.25.56:
 | 
				
			||||||
    resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==}
 | 
					    resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  zod@4.1.12:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
snapshots:
 | 
					snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@alloc/quick-lru@5.2.0': {}
 | 
					  '@alloc/quick-lru@5.2.0': {}
 | 
				
			||||||
| 
						 | 
					@ -3403,6 +3302,11 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@antfu/ni@0.21.12': {}
 | 
					  '@antfu/ni@0.21.12': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@asteasolutions/zod-to-openapi@8.1.0(zod@4.1.12)':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      openapi3-ts: 4.5.0
 | 
				
			||||||
 | 
					      zod: 4.1.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@babel/code-frame@7.27.1':
 | 
					  '@babel/code-frame@7.27.1':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@babel/helper-validator-identifier': 7.27.1
 | 
					      '@babel/helper-validator-identifier': 7.27.1
 | 
				
			||||||
| 
						 | 
					@ -3600,31 +3504,6 @@ snapshots:
 | 
				
			||||||
  '@biomejs/cli-win32-x64@1.6.3':
 | 
					  '@biomejs/cli-win32-x64@1.6.3':
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@borewit/text-codec@0.1.1':
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/cors@1.3.3(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/html@1.3.1(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))(typescript@5.8.3)':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@kitajs/html': 4.2.9
 | 
					 | 
				
			||||||
      '@kitajs/ts-html-plugin': 4.1.2(@kitajs/html@4.2.9)(typescript@5.8.3)
 | 
					 | 
				
			||||||
      elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
 | 
					 | 
				
			||||||
    transitivePeerDependencies:
 | 
					 | 
				
			||||||
      - typescript
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/node@1.4.1(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      crossws: 0.4.1(srvx@0.8.9)
 | 
					 | 
				
			||||||
      elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
 | 
					 | 
				
			||||||
      srvx: 0.8.9
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@elysiajs/openapi@1.4.11(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@esbuild/aix-ppc64@0.21.5':
 | 
					  '@esbuild/aix-ppc64@0.21.5':
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3809,6 +3688,28 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@floating-ui/utils@0.2.9': {}
 | 
					  '@floating-ui/utils@0.2.9': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/node-server@1.19.6(hono@4.10.4)':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      hono: 4.10.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/standard-validator@0.1.5(@standard-schema/spec@1.0.0)(hono@4.10.4)':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@standard-schema/spec': 1.0.0
 | 
				
			||||||
 | 
					      hono: 4.10.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/zod-openapi@1.1.4(hono@4.10.4)(zod@4.1.12)':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@asteasolutions/zod-to-openapi': 8.1.0(zod@4.1.12)
 | 
				
			||||||
 | 
					      '@hono/zod-validator': 0.7.4(hono@4.10.4)(zod@4.1.12)
 | 
				
			||||||
 | 
					      hono: 4.10.4
 | 
				
			||||||
 | 
					      openapi3-ts: 4.5.0
 | 
				
			||||||
 | 
					      zod: 4.1.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@hono/zod-validator@0.7.4(hono@4.10.4)(zod@4.1.12)':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      hono: 4.10.4
 | 
				
			||||||
 | 
					      zod: 4.1.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@humanwhocodes/config-array@0.13.0':
 | 
					  '@humanwhocodes/config-array@0.13.0':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@humanwhocodes/object-schema': 2.0.3
 | 
					      '@humanwhocodes/object-schema': 2.0.3
 | 
				
			||||||
| 
						 | 
					@ -3849,18 +3750,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@jsr/std__async@1.0.13': {}
 | 
					  '@jsr/std__async@1.0.13': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@kitajs/html@4.2.9':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      csstype: 3.1.3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@kitajs/ts-html-plugin@4.1.2(@kitajs/html@4.2.9)(typescript@5.8.3)':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@kitajs/html': 4.2.9
 | 
					 | 
				
			||||||
      chalk: 4.1.2
 | 
					 | 
				
			||||||
      tslib: 2.8.1
 | 
					 | 
				
			||||||
      typescript: 5.8.3
 | 
					 | 
				
			||||||
      yargs: 17.7.2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@nodelib/fs.scandir@2.1.5':
 | 
					  '@nodelib/fs.scandir@2.1.5':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@nodelib/fs.stat': 2.0.5
 | 
					      '@nodelib/fs.stat': 2.0.5
 | 
				
			||||||
| 
						 | 
					@ -4350,7 +4239,7 @@ snapshots:
 | 
				
			||||||
  '@rollup/rollup-win32-x64-msvc@4.42.0':
 | 
					  '@rollup/rollup-win32-x64-msvc@4.42.0':
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@sinclair/typebox@0.34.41': {}
 | 
					  '@standard-schema/spec@1.0.0': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@swc/core-darwin-arm64@1.11.31':
 | 
					  '@swc/core-darwin-arm64@1.11.31':
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
| 
						 | 
					@ -4412,18 +4301,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@tanstack/virtual-core@3.13.9': {}
 | 
					  '@tanstack/virtual-core@3.13.9': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@tokenizer/inflate@0.2.7':
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      debug: 4.4.3
 | 
					 | 
				
			||||||
      fflate: 0.8.2
 | 
					 | 
				
			||||||
      token-types: 6.1.1
 | 
					 | 
				
			||||||
    transitivePeerDependencies:
 | 
					 | 
				
			||||||
      - supports-color
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@tokenizer/token@0.3.0':
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  '@ts-morph/common@0.19.0':
 | 
					  '@ts-morph/common@0.19.0':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      fast-glob: 3.3.3
 | 
					      fast-glob: 3.3.3
 | 
				
			||||||
| 
						 | 
					@ -4812,12 +4689,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cli-spinners@2.9.2: {}
 | 
					  cli-spinners@2.9.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cliui@8.0.1:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      string-width: 4.2.3
 | 
					 | 
				
			||||||
      strip-ansi: 6.0.1
 | 
					 | 
				
			||||||
      wrap-ansi: 7.0.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  clone@1.0.4: {}
 | 
					  clone@1.0.4: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clsx@2.1.1: {}
 | 
					  clsx@2.1.1: {}
 | 
				
			||||||
| 
						 | 
					@ -4844,10 +4715,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  convert-source-map@2.0.0: {}
 | 
					  convert-source-map@2.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cookie-es@2.0.0: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cookie@1.0.2: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cosmiconfig@8.3.6(typescript@5.8.3):
 | 
					  cosmiconfig@8.3.6(typescript@5.8.3):
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      import-fresh: 3.3.1
 | 
					      import-fresh: 3.3.1
 | 
				
			||||||
| 
						 | 
					@ -4863,10 +4730,6 @@ snapshots:
 | 
				
			||||||
      shebang-command: 2.0.0
 | 
					      shebang-command: 2.0.0
 | 
				
			||||||
      which: 2.0.2
 | 
					      which: 2.0.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  crossws@0.4.1(srvx@0.8.9):
 | 
					 | 
				
			||||||
    optionalDependencies:
 | 
					 | 
				
			||||||
      srvx: 0.8.9
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  cssesc@3.0.0: {}
 | 
					  cssesc@3.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  csstype@3.1.3: {}
 | 
					  csstype@3.1.3: {}
 | 
				
			||||||
| 
						 | 
					@ -4915,11 +4778,6 @@ snapshots:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      ms: 2.1.3
 | 
					      ms: 2.1.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  debug@4.4.3:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      ms: 2.1.3
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  decimal.js-light@2.5.1: {}
 | 
					  decimal.js-light@2.5.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  decompress-response@6.0.0:
 | 
					  decompress-response@6.0.0:
 | 
				
			||||||
| 
						 | 
					@ -4975,17 +4833,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  electron-to-chromium@1.5.165: {}
 | 
					  electron-to-chromium@1.5.165: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3):
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@sinclair/typebox': 0.34.41
 | 
					 | 
				
			||||||
      cookie: 1.0.2
 | 
					 | 
				
			||||||
      exact-mirror: 0.2.0(@sinclair/typebox@0.34.41)
 | 
					 | 
				
			||||||
      fast-decode-uri-component: 1.0.1
 | 
					 | 
				
			||||||
      openapi-types: 12.1.3
 | 
					 | 
				
			||||||
    optionalDependencies:
 | 
					 | 
				
			||||||
      file-type: 21.0.0
 | 
					 | 
				
			||||||
      typescript: 5.8.3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  emoji-regex@8.0.0: {}
 | 
					  emoji-regex@8.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  emoji-regex@9.2.2: {}
 | 
					  emoji-regex@9.2.2: {}
 | 
				
			||||||
| 
						 | 
					@ -5148,10 +4995,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  eventemitter3@4.0.7: {}
 | 
					  eventemitter3@4.0.7: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  exact-mirror@0.2.0(@sinclair/typebox@0.34.41):
 | 
					 | 
				
			||||||
    optionalDependencies:
 | 
					 | 
				
			||||||
      '@sinclair/typebox': 0.34.41
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  execa@7.2.0:
 | 
					  execa@7.2.0:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      cross-spawn: 7.0.6
 | 
					      cross-spawn: 7.0.6
 | 
				
			||||||
| 
						 | 
					@ -5168,8 +5011,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  expect-type@1.2.1: {}
 | 
					  expect-type@1.2.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fast-decode-uri-component@1.0.1: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fast-deep-equal@3.1.3: {}
 | 
					  fast-deep-equal@3.1.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fast-equals@5.2.2: {}
 | 
					  fast-equals@5.2.2: {}
 | 
				
			||||||
| 
						 | 
					@ -5195,23 +5036,10 @@ snapshots:
 | 
				
			||||||
      node-domexception: 1.0.0
 | 
					      node-domexception: 1.0.0
 | 
				
			||||||
      web-streams-polyfill: 3.3.3
 | 
					      web-streams-polyfill: 3.3.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fflate@0.8.2:
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  file-entry-cache@6.0.1:
 | 
					  file-entry-cache@6.0.1:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      flat-cache: 3.2.0
 | 
					      flat-cache: 3.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  file-type@21.0.0:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@tokenizer/inflate': 0.2.7
 | 
					 | 
				
			||||||
      strtok3: 10.3.4
 | 
					 | 
				
			||||||
      token-types: 6.1.1
 | 
					 | 
				
			||||||
      uint8array-extras: 1.5.0
 | 
					 | 
				
			||||||
    transitivePeerDependencies:
 | 
					 | 
				
			||||||
      - supports-color
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  file-uri-to-path@1.0.0: {}
 | 
					  file-uri-to-path@1.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fill-range@7.1.1:
 | 
					  fill-range@7.1.1:
 | 
				
			||||||
| 
						 | 
					@ -5259,8 +5087,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gensync@1.0.0-beta.2: {}
 | 
					  gensync@1.0.0-beta.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get-caller-file@2.0.5: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get-nonce@1.0.1: {}
 | 
					  get-nonce@1.0.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get-stream@6.0.1: {}
 | 
					  get-stream@6.0.1: {}
 | 
				
			||||||
| 
						 | 
					@ -5332,6 +5158,8 @@ snapshots:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      function-bind: 1.1.2
 | 
					      function-bind: 1.1.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hono@4.10.4: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  https-proxy-agent@6.2.1:
 | 
					  https-proxy-agent@6.2.1:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      agent-base: 7.1.3
 | 
					      agent-base: 7.1.3
 | 
				
			||||||
| 
						 | 
					@ -5615,7 +5443,9 @@ snapshots:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      mimic-fn: 4.0.0
 | 
					      mimic-fn: 4.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  openapi-types@12.1.3: {}
 | 
					  openapi3-ts@4.5.0:
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      yaml: 2.8.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  optionator@0.9.4:
 | 
					  optionator@0.9.4:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
| 
						 | 
					@ -5899,8 +5729,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  regexparam@3.0.0: {}
 | 
					  regexparam@3.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  require-directory@2.1.1: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  resolve-from@4.0.0: {}
 | 
					  resolve-from@4.0.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resolve-pkg-maps@1.0.0: {}
 | 
					  resolve-pkg-maps@1.0.0: {}
 | 
				
			||||||
| 
						 | 
					@ -6026,10 +5854,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  source-map@0.6.1: {}
 | 
					  source-map@0.6.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  srvx@0.8.9:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      cookie-es: 2.0.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  stackback@0.0.2: {}
 | 
					  stackback@0.0.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  std-env@3.9.0: {}
 | 
					  std-env@3.9.0: {}
 | 
				
			||||||
| 
						 | 
					@ -6070,11 +5894,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  strip-json-comments@3.1.1: {}
 | 
					  strip-json-comments@3.1.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  strtok3@10.3.4:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@tokenizer/token': 0.3.0
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  sucrase@3.35.0:
 | 
					  sucrase@3.35.0:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@jridgewell/gen-mapping': 0.3.8
 | 
					      '@jridgewell/gen-mapping': 0.3.8
 | 
				
			||||||
| 
						 | 
					@ -6186,13 +6005,6 @@ snapshots:
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      is-number: 7.0.0
 | 
					      is-number: 7.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  token-types@6.1.1:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      '@borewit/text-codec': 0.1.1
 | 
					 | 
				
			||||||
      '@tokenizer/token': 0.3.0
 | 
					 | 
				
			||||||
      ieee754: 1.2.1
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ts-api-utils@1.4.3(typescript@5.8.3):
 | 
					  ts-api-utils@1.4.3(typescript@5.8.3):
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      typescript: 5.8.3
 | 
					      typescript: 5.8.3
 | 
				
			||||||
| 
						 | 
					@ -6233,9 +6045,6 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  typescript@5.8.3: {}
 | 
					  typescript@5.8.3: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  uint8array-extras@1.5.0:
 | 
					 | 
				
			||||||
    optional: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  undici-types@6.21.0: {}
 | 
					  undici-types@6.21.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  undici-types@7.8.0: {}
 | 
					  undici-types@7.8.0: {}
 | 
				
			||||||
| 
						 | 
					@ -6457,26 +6266,12 @@ snapshots:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  wrappy@1.0.2: {}
 | 
					  wrappy@1.0.2: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  y18n@5.0.8: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yallist@3.1.1: {}
 | 
					  yallist@3.1.1: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  yaml@2.8.0: {}
 | 
					  yaml@2.8.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  yargs-parser@21.1.1: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yargs@17.7.2:
 | 
					 | 
				
			||||||
    dependencies:
 | 
					 | 
				
			||||||
      cliui: 8.0.1
 | 
					 | 
				
			||||||
      escalade: 3.2.0
 | 
					 | 
				
			||||||
      get-caller-file: 2.0.5
 | 
					 | 
				
			||||||
      require-directory: 2.1.1
 | 
					 | 
				
			||||||
      string-width: 4.2.3
 | 
					 | 
				
			||||||
      y18n: 5.0.8
 | 
					 | 
				
			||||||
      yargs-parser: 21.1.1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  yocto-queue@0.1.0: {}
 | 
					  yocto-queue@0.1.0: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  zod@3.23.8: {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  zod@3.25.56: {}
 | 
					  zod@3.25.56: {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  zod@4.1.12: {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue