feat: (BREAKING!) migrate hono

This commit is contained in:
monoid 2025-10-31 23:23:48 +09:00
parent a319dc3337
commit f3b720a07c
23 changed files with 1042 additions and 1113 deletions

View file

@ -17,7 +17,7 @@
"typescript": "^5.4.3"
},
"type": "module",
"dependencies": {
"zod": "^3.23.8"
"peerDependencies": {
"zod": "^4.1.12"
}
}

View file

@ -1,6 +1,12 @@
import { z } 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({
title: z.string(),
content_type: z.string(),
@ -8,7 +14,7 @@ export const DocumentBodySchema = z.object({
filename: z.string(),
modified_at: z.number(),
content_hash: z.string(),
additional: z.record(z.unknown()),
additional: z.record(z.string(), z.unknown()),
tags: z.array(z.string()),
pagenum: z.number().int(),
gid: z.number().nullable(),
@ -40,7 +46,7 @@ export type TagRelation = z.infer<typeof TagRelationSchema>;
export const PermissionSchema = z.object({
username: z.string(),
name: z.string(),
name: PermissionNameSchema,
});
export type Permission = z.infer<typeof PermissionSchema>;
@ -103,7 +109,7 @@ export const ServerPersistedSettingSchema = z.object({
secure: z.boolean(),
cli: z.boolean(),
forbid_remote_admin_login: z.boolean(),
guest: z.array(z.string()),
guest: z.array(PermissionNameSchema),
});
export type ServerPersistedSetting = z.infer<typeof ServerPersistedSettingSchema>;
@ -115,7 +121,7 @@ export const ServerSettingResponseSchema = z.object({
mode: z.enum(["development", "production"]),
}),
persisted: ServerPersistedSettingSchema,
permissions: z.array(z.string()),
permissions: z.array(PermissionNameSchema),
});
export type ServerSettingResponse = z.infer<typeof ServerSettingResponseSchema>;

View file

@ -14,21 +14,22 @@
"author": "",
"license": "ISC",
"dependencies": {
"@elysiajs/cors": "^1.3.3",
"@elysiajs/html": "^1.3.1",
"@elysiajs/node": "^1.4.1",
"@elysiajs/openapi": "^1.4.11",
"@hono/node-server": "^1.19.6",
"@hono/standard-validator": "^0.1.5",
"@hono/zod-openapi": "^1.1.4",
"@hono/zod-validator": "^0.7.4",
"@std/async": "npm:@jsr/std__async@^1.0.13",
"@zip.js/zip.js": "^2.7.62",
"better-sqlite3": "^9.6.0",
"chokidar": "^3.6.0",
"dbtype": "workspace:dbtype",
"dotenv": "^16.5.0",
"elysia": "^1.4.9",
"hono": "^4.10.4",
"jose": "^5.10.0",
"kysely": "^0.27.6",
"natural-orderby": "^2.0.3",
"tiny-async-pool": "^1.3.0"
"tiny-async-pool": "^1.3.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",

View file

@ -1,7 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import type { Kysely } from "kysely";
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";
export interface SettingConfig {
@ -130,14 +130,8 @@ const loadPersistedSetting = async (db: Kysely<db.DB>): Promise<PersistedSetting
};
const mergePersisted = (input: Partial<PersistedSetting>): PersistedSetting => {
const validPermissions = new Set<Permission>(Object.values(Permission));
const guest = Array.isArray(input.guest)
? Array.from(
new Set(
input.guest.filter((value): value is Permission => validPermissions.has(value as Permission)),
),
)
: persistedDefault.guest;
const guestSource = Array.isArray(input.guest) ? input.guest : persistedDefault.guest;
const guest = normalizePermissions(guestSource);
return {
secure: input.secure ?? persistedDefault.secure,

View file

@ -1,4 +1,3 @@
import Elysia from "elysia";
import { connectDB } from "./database.ts";
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";

View file

@ -1,5 +1,6 @@
import { getKysely } from "./kysely.ts";
import { type IUser, IUserSettings, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
import { normalizePermissions } from "../permission/permission.ts";
class SqliteUser implements IUser {
readonly username: string;
@ -23,7 +24,7 @@ class SqliteUser implements IUser {
.selectAll()
.where("username", "=", this.username)
.execute();
return permissions.map((x) => x.name);
return normalizePermissions(permissions.map((x) => x.name));
}
async add(name: string) {
const result = await this.kysely

View file

@ -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 { ContentFile } from "../content/mod.ts";
import type { AppEnv } from "../login.ts";
import { AdminOnly } from "../permission/permission.ts";
import { sendError } from "../route/error_handler.ts";
const toSerializableContent = (file: ContentFile) => ({ path: file.path, type: file.type });
const CommitEntrySchema = t.Array(t.Object({
type: t.String(),
path: t.String(),
const commitEntrySchema = z.array(z.object({
type: z.string(),
path: z.string(),
}));
const CommitAllSchema = t.Object({
type: t.String(),
const commitAllSchema = z.object({
type: z.string(),
});
export const createDiffRouter = (diffmgr: DiffManager) =>
new Elysia({ name: "diff-router" })
.group("/diff", (app) =>
app
.get("/list", () => {
return diffmgr.getAdded().map((entry) => ({
export const createDiffRouter = (diffmgr: DiffManager) => {
const router = new Hono<AppEnv>();
router.get("/list", AdminOnly, (c) =>
c.json(
diffmgr.getAdded().map((entry) => ({
type: entry.type,
value: entry.value.map(toSerializableContent),
}));
}, {
beforeHandle: AdminOnly,
})
.post("/commit", async ({ body }) => {
if (body.length === 0) {
return { ok: true, docs: [] as number[] };
})),
),
);
router.post(
"/commit",
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)));
return {
ok: true,
docs: results,
};
}, {
beforeHandle: AdminOnly,
body: CommitEntrySchema,
})
.post("/commitall", async ({ body }) => {
const { type } = body;
const results = await Promise.all(entries.map(({ type, path }) => diffmgr.commit(type, path)));
return c.json({ ok: true, docs: results });
},
);
router.post(
"/commitall",
AdminOnly,
sValidator("json", commitAllSchema),
async (c) => {
const { type } = c.req.valid("json");
if (!type) {
sendError(400, 'format exception: there is no "type"');
}
await diffmgr.commitAll(type);
return { ok: true };
}, {
beforeHandle: AdminOnly,
body: CommitAllSchema,
})
.get("/*", () => {
sendError(404);
})
return c.json({ ok: true });
},
);
return router;
};

View file

@ -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 { z } from "zod";
import { sValidator } from "@hono/standard-validator";
import type { IUser, UserAccessor } from "./model/mod.ts";
import { ClientRequestError } from "./route/error_handler.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;
permission: string[];
permission: Permission[];
};
export type UserState = {
user: PayloadInfo;
};
type AuthStore = {
export type AuthStore = {
user: PayloadInfo;
refreshed: boolean;
authenticated: boolean;
};
export type AppEnv = {
Variables: {
auth: AuthStore;
};
};
type LoginResponse = {
accessExpired: number;
} & PayloadInfo;
@ -30,20 +41,18 @@ type RefreshResponse = {
type RefreshPayloadInfo = { username: string };
type CookieJar = Context["cookie"];
const LoginBodySchema = t.Object({
username: t.String(),
password: t.String(),
const LoginBodySchema = z.object({
username: z.string(),
password: z.string(),
});
const ResetBodySchema = t.Object({
username: t.String(),
oldpassword: t.String(),
newpassword: t.String(),
const ResetBodySchema = z.object({
username: z.string(),
oldpassword: z.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 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 refreshTokenName = "refresh_token";
function setToken(cookie: CookieJar, token_name: string, token_payload: string | null, expiredMilliseconds: number) {
if (token_payload === null) {
cookie[token_name]?.remove();
const setToken = (c: Context<AppEnv>, tokenName: string, tokenPayload: string | null, expiresMs: number) => {
const setting = get_setting();
if (tokenPayload === null) {
deleteCookie(c, tokenName, {
path: "/",
secure: setting.secure,
sameSite: "Strict",
});
return;
}
const setting = get_setting();
cookie[token_name].set({
value: token_payload,
setCookie(c, tokenName, tokenPayload, {
path: "/",
httpOnly: true,
secure: setting.secure,
sameSite: "strict",
expires: new Date(Date.now() + expiredMilliseconds),
sameSite: "Strict",
expires: new Date(Date.now() + expiresMs),
});
}
};
function removeToken(cookie: CookieJar, token_name: string) {
cookie[token_name]?.remove();
}
type RawPayloadInfo = {
username: string;
permission: unknown[];
};
const isUserState = (obj: unknown): obj is PayloadInfo => {
const isUserState = (obj: unknown): obj is RawPayloadInfo => {
if (typeof obj !== "object" || obj === null) {
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 => {
@ -123,25 +138,23 @@ type AuthResult = {
};
async function authenticate(
cookie: CookieJar,
c: Context<AppEnv>,
userController: UserAccessor,
options: { forceRefresh?: boolean } = {},
): Promise<AuthResult> {
const setting = get_setting();
const secretKey = setting.jwt_secretkey;
const accessCookie = cookie[accessTokenName];
const refreshCookie = cookie[refreshTokenName];
const accessValue = typeof accessCookie?.value === 'string' ? accessCookie.value : undefined;
const refreshValue = typeof refreshCookie?.value === 'string' ? refreshCookie.value : undefined;
const accessValue = getCookie(c, accessTokenName);
const refreshValue = getCookie(c, refreshTokenName);
const guestUser: PayloadInfo = {
username: "",
permission: setting.guest,
permission: [...setting.guest],
};
const setGuest = (): AuthResult => {
accessCookie?.remove();
refreshCookie?.remove();
setToken(c, accessTokenName, null, 0);
setToken(c, refreshTokenName, null, 0);
return { user: guestUser, refreshed: false, success: false };
};
@ -150,13 +163,13 @@ async function authenticate(
if (!account) {
return setGuest();
}
const permissions = await account.get_permissions();
const permissions = normalizePermissions(await account.get_permissions());
const payload: PayloadInfo = {
username: account.username,
permission: permissions,
};
const accessToken = await createAccessToken(payload, secretKey);
setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
setToken(c, accessTokenName, accessToken, accessExpiredTime);
return { user: payload, refreshed: true, success: true };
};
@ -178,11 +191,15 @@ async function authenticate(
if (options.forceRefresh) {
if (accessValue) {
try {
const payload = await verifyToken<PayloadInfo>(accessValue, secretKey);
const payload = await verifyToken<RawPayloadInfo>(accessValue, secretKey);
if (isUserState(payload)) {
const accessToken = await createAccessToken(payload, secretKey);
setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
return { user: payload, refreshed: true, success: true };
const normalized: PayloadInfo = {
username: payload.username,
permission: normalizePermissions(payload.permission),
};
const accessToken = await createAccessToken(normalized, secretKey);
setToken(c, accessTokenName, accessToken, accessExpiredTime);
return { user: normalized, refreshed: true, success: true };
}
return setGuest();
} catch (error) {
@ -196,9 +213,16 @@ async function authenticate(
if (accessValue) {
try {
const payload = await verifyToken<PayloadInfo>(accessValue, secretKey);
const payload = await verifyToken<RawPayloadInfo>(accessValue, secretKey);
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();
} catch (error) {
@ -212,13 +236,15 @@ async function authenticate(
}
export const createLoginRouter = (userController: UserAccessor) => {
return new Elysia({ name: "login-router" })
.group("/user", (app) =>
app
.post("/login", async ({ body, cookie, set }) => {
const router = new Hono<AppEnv>();
router.post(
"/login",
sValidator("json", LoginBodySchema),
async (c) => {
const setting = get_setting();
const secretKey = setting.jwt_secretkey;
const { username, password } = body;
const { username, password } = c.req.valid("json");
if (username === "admin" && setting.forbid_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");
}
const permission = await user.get_permissions();
const permission = normalizePermissions(await user.get_permissions());
const accessToken = await createAccessToken({ username: user.username, permission }, secretKey);
const refreshToken = await createRefreshToken({ username: user.username }, secretKey);
setToken(cookie, accessTokenName, accessToken, accessExpiredTime);
setToken(cookie, refreshTokenName, refreshToken, refreshExpiredTime);
setToken(c, accessTokenName, accessToken, accessExpiredTime);
setToken(c, refreshTokenName, refreshToken, refreshExpiredTime);
set.status = 200;
return {
return c.json({
username: user.username,
permission,
accessExpired: Math.floor(Date.now() ) + accessExpiredTime,
} satisfies LoginResponse;
}, {
body: LoginBodySchema,
})
.post("/logout", ({ cookie, set }) => {
accessExpired: Math.floor(Date.now()) + accessExpiredTime,
} satisfies LoginResponse);
},
);
router.post("/logout", (c) => {
const setting = get_setting();
removeToken(cookie, accessTokenName);
removeToken(cookie, refreshTokenName);
set.status = 200;
return {
setToken(c, accessTokenName, null, 0);
setToken(c, refreshTokenName, null, 0);
return c.json({
ok: true,
username: "",
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) {
throw new ClientRequestError(401, "not authorized");
}
return {
return c.json({
...auth.user,
refresh: true,
accessExpired: Math.floor(Date.now()) + accessExpiredTime,
} satisfies RefreshResponse;
})
.post("/reset", async ({ body }) => {
const { username, oldpassword, newpassword } = body;
} satisfies RefreshResponse);
});
router.post(
"/reset",
sValidator("json", ResetBodySchema),
async (c) => {
const { username, oldpassword, newpassword } = c.req.valid("json");
const account = await userController.findUser(username);
if (!account || !account.password.check_password(oldpassword)) {
throw new ClientRequestError(403, "not authorized");
}
await account.reset_password(newpassword);
return { 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,
}),
return c.json({ ok: true });
},
);
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) => {
return new Elysia({
name: "user-handler",
seed: "UserAccess",
})
.derive({ as: "scoped" }, async ({ cookie }) => {
const auth = await authenticate(cookie, userController);
return {
export const createUserHandler = (userController: UserAccessor): MiddlewareHandler<AppEnv> =>
async (c, next) => {
const auth = await authenticate(c, userController);
c.set("auth", {
user: auth.user,
refreshed: auth.refreshed,
authenticated: auth.success,
};
});
};
await next();
};
export const getAdmin = async (cntr: UserAccessor) => {
const admin = await cntr.findUser("admin");

View file

@ -1,4 +1,5 @@
import { UserSetting } from "dbtype";
import type { Permission } from "../permission/permission.ts";
import { createHmac, randomBytes } from "node:crypto";
function hashForPassword(salt: string, password: string) {
@ -50,7 +51,7 @@ export interface IUser {
/**
* return user's permission list.
*/
get_permissions(): Promise<string[]>;
get_permissions(): Promise<Permission[]>;
/**
* add permission
* @param name permission name to add

View file

@ -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 type { UserState } from "../login.ts";
export enum Permission {
// ========
// not implemented
// admin only
/** remove document */
// removeContent = 'removeContent',
export const PERMISSIONS = SHARED_PERMISSIONS;
/** upload document */
// uploadContent = 'uploadContent',
export type Permission = PermissionName;
/** modify document except base path, filename, content_hash. but admin can modify all. */
// modifyContent = 'modifyContent',
export const PERMISSION = {
ModifyTag: PERMISSIONS[0],
QueryContent: PERMISSIONS[1],
ModifyTagDesc: PERMISSIONS[2],
} as const;
/** add tag into document */
// addTagContent = 'addTagContent',
/** remove tag from document */
// removeTagContent = 'removeTagContent',
/** ModifyTagInDoc */
ModifyTag = "ModifyTag",
const PERMISSION_SET = new Set<Permission>(PERMISSIONS);
/** find documents with query */
// findAllContent = 'findAllContent',
/** find one document. */
// findOneContent = 'findOneContent',
/** view content*/
// viewContent = 'viewContent',
QueryContent = "QueryContent",
export const isPermission = (value: unknown): value is Permission =>
typeof value === "string" && PERMISSION_SET.has(value as Permission);
/** modify description about the one tag. */
modifyTagDesc = "ModifyTagDesc",
}
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.");
export const normalizePermissions = (values?: Iterable<unknown>): Permission[] => {
if (!values) {
return [];
}
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 user = resolveUser(context);
const resolveUser = (c: Context<AppEnv>): PayloadInfo => {
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") {
await next();
return;
}
const user_permission = user.permission;
if (!permissions.every((p) => user_permission.includes(p))) {
const userPermission = user.permission;
if (!permissions.every((p) => userPermission.includes(p))) {
if (user.username === "") {
throw sendError(401, "you are guest. login needed.");
}
throw sendError(403, "do not have permission");
}
await next();
};
export const AdminOnly = (context: PermissionCheckContext) => {
const user = resolveUser(context);
export const AdminOnly: MiddlewareHandler<AppEnv> = async (c, next) => {
const user = resolveUser(c);
if (user.username !== "admin") {
throw sendError(403, "admin only");
}
await next();
};

View file

@ -1,4 +1,3 @@
import type { Context as ElysiaContext } from "elysia";
import { Readable } from "node:stream";
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip } from "../util/zipwrap.ts";
import { Entry } from "@zip.js/zip.js";
@ -10,21 +9,24 @@ const extensionToMime = (ext: string) => {
return `image/${ext}`;
};
type ResponseSet = Pick<ElysiaContext["set"], "status" | "headers">;
export type ResponseMeta = {
status: number;
headers: Record<string, string>;
};
type RenderOptions = {
path: string;
page: number;
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";
set.headers["content-type"] = extensionToMime(ext);
if (typeof entry.uncompressedSize === "number") {
set.headers["content-length"] = entry.uncompressedSize;
set.headers["content-length"] = entry.uncompressedSize.toString();
}
const lastModified = entry.lastModDate ?? new Date();

View file

@ -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 type { Document, QueryListOption } from "dbtype";
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 { 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) => {
return new Elysia({
name: "content-router",
prefix: "/doc",
})
.get("/search", async ({ query }) => {
const limit = Math.min(Number(query.limit ?? 20), 100);
const router = new Hono<AppEnv>();
router.get(
"/search",
createPermissionCheck(Per.QueryContent),
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 = {
limit: limit,
limit,
allow_tag: query.allow_tag?.split(",") ?? [],
word: query.word,
cursor: query.cursor,
cursor: query.cursor ? ensureFinite(Number(query.cursor), "invalid cursor") : undefined,
eager_loading: true,
offset: Number(query.offset),
use_offset: query.use_offset === 'true',
offset: query.offset ? ensureFinite(Number(query.offset), "invalid offset") : undefined,
use_offset: query.use_offset === "true",
content_type: query.content_type,
};
return await controller.findList(option);
}, {
beforeHandle: createPermissionCheck(Per.QueryContent),
query: t.Object({
limit: t.Optional(t.String()),
cursor: t.Optional(t.Number()),
word: t.Optional(t.String()),
content_type: t.Optional(t.String()),
offset: t.Optional(t.Number()),
use_offset: t.Optional(t.String()),
allow_tag: t.Optional(t.String()),
})
})
.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) {
return c.json(await controller.findList(option));
},
);
router.get(
"/_gid",
createPermissionCheck(Per.QueryContent),
sValidator("query", gidQuerySchema),
async (c) => {
const { gid } = c.req.valid("query");
const gidList = gid.split(",").map((x) => Number.parseInt(x, 10));
if (gidList.some((x) => Number.isNaN(x)) || gidList.length > 100) {
throw sendError(400, "Invalid GID list");
}
return await controller.findByGidList(gid_list);
}, {
beforeHandle: createPermissionCheck(Per.QueryContent),
query: t.Object({ gid: t.String() })
})
.get("/:num", async ({ params: { num } }) => {
return c.json(await controller.findByGidList(gidList));
},
);
router.get(
"/:num",
createPermissionCheck(Per.QueryContent),
sValidator("param", idParamSchema),
async (c) => {
const { num } = c.req.valid("param");
const document = await controller.findById(num, true);
if (document === undefined) {
throw sendError(404, "document does not exist.");
}
return document;
}, {
beforeHandle: createPermissionCheck(Per.QueryContent),
params: t.Object({ num: t.Numeric() })
})
.post("/:num", async ({ params: { num }, body }) => {
const content_desc: Partial<Document> & { id: number } = {
return c.json(document);
},
);
router.post(
"/:num",
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,
...body,
...(body as Record<string, unknown>),
};
return await controller.update(content_desc);
}, {
beforeHandle: AdminOnly,
params: t.Object({ num: t.Numeric() }),
body: t.Object({}, { additionalProperties: true })
})
.delete("/:num", async ({ params: { num } }) => {
return await controller.del(num);
}, {
beforeHandle: AdminOnly,
params: t.Object({ num: t.Numeric() })
})
.get("/:num/similars", async ({ params: { num } }) => {
return c.json(await controller.update(contentDesc));
},
);
router.delete(
"/:num",
AdminOnly,
sValidator("param", idParamSchema),
async (c) => {
const { num } = c.req.valid("param");
return c.json(await controller.del(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);
if (doc === undefined) {
throw sendError(404);
}
return await controller.getSimilarDocument(doc);
}, {
beforeHandle: createPermissionCheck(Per.QueryContent),
params: t.Object({ num: t.Numeric() })
})
.get("/:num/tags", async ({ params: { num } }) => {
return c.json(await controller.getSimilarDocument(doc));
},
);
router.get(
"/:num/tags",
createPermissionCheck(Per.QueryContent),
sValidator("param", idParamSchema),
async (c) => {
const { num } = c.req.valid("param");
const document = await controller.findById(num, true);
if (document === undefined) {
throw sendError(404, "document does not exist.");
}
return document.tags;
}, {
beforeHandle: createPermissionCheck(Per.QueryContent),
params: t.Object({ num: t.Numeric() })
})
.post("/:num/tags/:tag", async ({ params: { num, tag } }) => {
return c.json(document.tags);
},
);
router.post(
"/:num/tags/:tag",
createPermissionCheck(Per.ModifyTag),
sValidator("param", idAndTagParamSchema),
async (c) => {
const { num, tag } = c.req.valid("param");
const doc = await controller.findById(num);
if (doc === undefined) {
throw sendError(404);
}
return await controller.addTag(doc, tag);
}, {
beforeHandle: createPermissionCheck(Per.ModifyTag),
params: t.Object({ num: t.Numeric(), tag: t.String() })
})
.delete("/:num/tags/:tag", async ({ params: { num, tag } }) => {
return c.json(await controller.addTag(doc, tag));
},
);
router.delete(
"/:num/tags/:tag",
createPermissionCheck(Per.ModifyTag),
sValidator("param", idAndTagParamSchema),
async (c) => {
const { num, tag } = c.req.valid("param");
const doc = await controller.findById(num);
if (doc === undefined) {
throw sendError(404);
}
return await controller.delTag(doc, tag);
}, {
beforeHandle: createPermissionCheck(Per.ModifyTag),
params: t.Object({ num: t.Numeric(), tag: t.String() })
})
.post("/:num/_rehash", async ({ params: { num } }) => {
return c.json(await controller.delTag(doc, tag));
},
);
router.post(
"/:num/_rehash",
AdminOnly,
sValidator("param", idParamSchema),
async (c) => {
const { num } = c.req.valid("param");
const doc = await controller.findById(num);
if (doc === undefined || doc.deleted_at !== null) {
throw sendError(404);
}
const filepath = join(doc.basepath, doc.filename);
try {
const new_hash = (await oshash(filepath)).toString();
return await controller.update({ id: num, content_hash: new_hash });
} catch (e) {
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
const newHash = (await oshash(filepath)).toString();
return c.json(await controller.update({ id: num, content_hash: newHash }));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw sendError(404, "file not found");
}
throw e;
throw error;
}
}, {
beforeHandle: AdminOnly,
params: t.Object({ num: t.Numeric() })
})
.post("/:num/_rescan", async ({ params: { num }, set }) => {
},
);
router.post(
"/:num/_rescan",
AdminOnly,
sValidator("param", idParamSchema),
async (c) => {
const { num } = c.req.valid("param");
const doc = await controller.findById(num, true);
if (doc === undefined) {
throw sendError(404);
}
await controller.rescanDocument(doc);
set.status = 204; // No Content
}, {
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() }),
})
return c.body(null, 204);
},
);
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;

View file

@ -1,4 +1,4 @@
import { ZodError } from "dbtype";
import { ZodError } from "zod";
export interface ErrorFormat {
code: number;
@ -21,30 +21,36 @@ const code_to_message_table: { [key: number]: string | undefined } = {
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) {
set.status = error.code;
return {
status: error.code,
body: {
code: error.code,
message: code_to_message_table[error.code] ?? "",
detail: error.message,
} satisfies ErrorFormat;
},
};
}
if (error instanceof ZodError) {
set.status = 400;
return {
status: 400,
body: {
code: 400,
message: "BadRequest",
detail: error.errors.map((x) => x.message).join(", "),
} satisfies ErrorFormat;
detail: error.issues.map((issue) => issue.message).join(", "),
},
};
}
set.status = 500;
return {
status: 500,
body: {
code: 500,
message: "Internal Server Error",
detail: error.message,
}
},
};
};
export const sendError = (code: number, message?: string): never => {

View file

@ -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 { 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";
const permissionOptions = Object.values(Permission).sort() as string[];
const permissionOptions = [...PERMISSIONS].sort() as Permission[];
const updateBodySchema = t.Object({
secure: t.Optional(t.Boolean()),
cli: t.Optional(t.Boolean()),
forbid_remote_admin_login: t.Optional(t.Boolean()),
guest: t.Optional(t.Array(t.Enum(Permission))),
const updateBodySchema = z.object({
secure: z.boolean().optional(),
cli: z.boolean().optional(),
forbid_remote_admin_login: z.boolean().optional(),
guest: z.array(z.enum(PERMISSIONS)).optional(),
});
type UpdateBody = Static<typeof updateBodySchema>;
type UpdateBody = z.infer<typeof updateBodySchema>;
type SettingResponse = {
env: {
@ -25,9 +29,9 @@ type SettingResponse = {
secure: boolean;
cli: boolean;
forbid_remote_admin_login: boolean;
guest: string[];
guest: Permission[];
};
permissions: string[];
permissions: Permission[];
};
const buildResponse = (): SettingResponse => {
@ -48,14 +52,17 @@ const buildResponse = (): SettingResponse => {
};
};
export const createSettingsRouter = (db: Kysely<db.DB>) =>
new Elysia({ name: "settings-router" })
.get("/settings", () => {
return buildResponse()}, {
beforeHandle: AdminOnly,
})
.patch("/settings", async ({ body }) => {
const payload = body as UpdateBody;
export const createSettingsRouter = (db: Kysely<db.DB>) => {
const router = new Hono<AppEnv>();
router.get("/settings", AdminOnly, (c) => c.json(buildResponse()));
router.patch(
"/settings",
AdminOnly,
sValidator("json", updateBodySchema),
async (c) => {
const payload = c.req.valid("json") as UpdateBody;
const update: PersistedSettingUpdate = {
secure: payload.secure,
cli: payload.cli,
@ -63,10 +70,11 @@ export const createSettingsRouter = (db: Kysely<db.DB>) =>
guest: payload.guest,
};
await updatePersistedSetting(db, update);
return buildResponse();
}, {
beforeHandle: AdminOnly,
body: updateBodySchema,
});
return c.json(buildResponse());
},
);
return router;
};
export default createSettingsRouter;

View file

@ -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 { 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";
const tagQuerySchema = z.object({
withCount: z.string().optional(),
});
const tagParamSchema = z.object({
tag_name: z.string(),
});
export function getTagRounter(tagController: TagAccessor) {
return new Elysia({ name: "tags-router",
prefix: "/tags",
})
.get("/", async ({ query }) => {
if (query.withCount !== undefined) {
return await tagController.getAllTagCount();
const router = new Hono<AppEnv>();
router.get(
"/",
createPermissionCheck(PERMISSION.QueryContent),
sValidator("query", tagQuerySchema),
async (c) => {
const { withCount } = c.req.valid("query");
if (withCount !== undefined) {
return c.json(await tagController.getAllTagCount());
}
return await tagController.getAllTagList();
}, {
beforeHandle: createPermissionCheck(Permission.QueryContent),
query: t.Object({
withCount: t.Optional(t.String()),
})
})
.get("/:tag_name", async ({ params: { tag_name } }) => {
return c.json(await tagController.getAllTagList());
},
);
router.get(
"/:tag_name",
createPermissionCheck(PERMISSION.QueryContent),
sValidator("param", tagParamSchema),
async (c) => {
const { tag_name } = c.req.valid("param");
const tag = await tagController.getTagByName(tag_name);
if (!tag) {
sendError(404, "tags not found");
}
return tag;
}, {
beforeHandle: createPermissionCheck(Permission.QueryContent),
params: t.Object({
tag_name: t.String(),
})
});
return c.json(tag);
},
);
return router;
}

View file

@ -1,35 +1,30 @@
import { Elysia, t } from "elysia";
import { cors } from "@elysiajs/cors";
import { staticPlugin } from "./util/static.ts";
import { html } from "@elysiajs/html";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { serve } from "@hono/node-server";
import { readFileSync } from "node:fs";
import { createInterface as createReadlineInterface } from "node:readline";
import { config } from "dotenv";
import { connectDB } from "./database.ts";
import { createDiffRouter, DiffManager } from "./diff/mod.ts";
import { get_setting, initializeSetting } from "./SettingConfig.ts";
import { readFileSync } from "node:fs";
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 { error_handler } from "./route/error_handler.ts";
import { mapErrorToResponse } from "./route/error_handler.ts";
import { createSettingsRouter } from "./route/settings.ts";
import { createInterface as createReadlineInterface } from "node:readline";
import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
import { loadComicConfig } from "./diff/watcher/ComicConfig.ts";
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod.ts";
import { getTagRounter } from "./route/tags.ts";
import { node } from "@elysiajs/node";
import { openapi } from "@elysiajs/openapi";
import { createStaticRouter } from "./util/static.ts";
import { config } from "dotenv";
config();
function createMetaTagContent(key: string, value: string) {
return `<meta property="${key}" content="${value}">`;
}
function createOgTagContent(title: string, description:string, image: string) {
function createOgTagContent(title: string, description: string, image: string) {
return [
createMetaTagContent("og:title", title),
createMetaTagContent("og:type", "website"),
@ -46,6 +41,12 @@ function makeMetaTagInjectedHTML(html: string, tagContent: string) {
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 => {
if (error instanceof Error) {
return error;
@ -60,7 +61,6 @@ const normalizeError = (error: unknown): Error => {
}
};
export async function create_server() {
const db = await connectDB();
await initializeSetting(db);
@ -69,9 +69,9 @@ export async function create_server() {
const userController = createSqliteUserController(db);
const documentController = createSqliteDocumentAccessor(db);
const tagController = createSqliteTagController(db);
const diffManger = new DiffManager(documentController);
const diffManager = new DiffManager(documentController);
const comicConfig = await loadComicConfig(db);
await diffManger.register("comic", createComicWatcher(comicConfig.watch));
await diffManager.register("comic", createComicWatcher(comicConfig.watch));
if (setting.cli) {
const userAdmin = await getAdmin(userController);
@ -80,9 +80,9 @@ export async function create_server() {
input: process.stdin,
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) => {
res(data);
resolve(data);
});
});
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({
adapter: node(),
})
.use(cors())
.use(staticPlugin({
const app = new Hono<AppEnv>();
app.use("*", cors());
app.use("*", createUserHandler(userController));
const staticRouter = createStaticRouter({
assets: "dist/assets",
prefix: "/assets",
headers: {
"X-Content-Type-Options": "nosniff",
"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);
let meta;
if (doc === undefined) {
set.status = 404;
meta = createOgTagContent("Not Found Doc", "Not Found", "");
} else {
set.status = 200;
meta = createOgTagContent(
if (!doc) {
const meta = createOgTagContent("Not Found Doc", "Not Found", "");
return htmlResponse(makeMetaTagInjectedHTML(indexHtml, meta), 404);
}
const meta = createOgTagContent(
doc.title,
doc.tags.join(", "),
`https://aeolian.monoid.top/api/doc/${docId}/comic/thumbnail`,
);
}
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)
return htmlResponse(makeMetaTagInjectedHTML(indexHtml, meta));
});
app.listen({
port: setting.port,
const spaPaths = ["/", "/doc/*", "/search", "/login", "/profile", "/difference", "/setting", "/tags"] as const;
for (const path of spaPaths) {
app.get(path, () => htmlResponse(indexHtml));
}
serve({
fetch: app.fetch,
hostname: setting.hostname,
port: setting.port,
});
console.log(`Server started at http://${setting.hostname}:${setting.port}/`);

View file

@ -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() }));

View file

@ -1 +0,0 @@
export {};

View file

@ -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 { stat } from "node:fs/promises";
import { Readable } from "node:stream";
import { extname, resolve } from "node:path";
const MIME_TYPES: Record<string, string> = {
@ -46,36 +48,44 @@ export type StaticPluginOptions = {
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 (
context: Context,
rootDir: string,
headersTemplate: Record<string, string>,
ctx: Context,
sendBody: boolean,
) => {
const wildcard = ctx.params?.["*"] ?? "";
if (wildcard.length === 0) {
throw new NotFoundError();
const pathFragment = resolveWildcard(context, "*");
if (!pathFragment) {
return buildResponse(404, {}, null);
}
let decoded: string;
try {
decoded = decodeURI(wildcard);
} catch {
throw new NotFoundError();
}
if (decoded.includes("\0")) {
throw new NotFoundError();
}
const absolutePath = resolve(rootDir, decoded);
const absolutePath = resolve(rootDir, pathFragment);
if (!isPathWithinRoot(absolutePath, rootDir)) {
throw new NotFoundError();
return buildResponse(404, {}, null);
}
const fileStat = await stat(absolutePath).catch(() => undefined);
if (!fileStat || fileStat.isDirectory()) {
throw new NotFoundError();
return buildResponse(404, {}, null);
}
const responseHeaders: Record<string, string> = {
@ -86,44 +96,40 @@ const handleStaticRequest = async (
const etag = generateETag(fileStat.mtimeMs, fileStat.size);
responseHeaders.ETag = etag;
const ifNoneMatch = ctx.request.headers.get("if-none-match");
const ifNoneMatch = context.req.header("if-none-match");
if (ifNoneMatch && ifNoneMatch === etag) {
ctx.set.status = 304;
ctx.set.headers = responseHeaders;
return undefined;
return buildResponse(304, responseHeaders, null);
}
const ifModifiedSince = ctx.request.headers.get("if-modified-since");
const ifModifiedSince = context.req.header("if-modified-since");
if (ifModifiedSince) {
const since = new Date(ifModifiedSince);
if (!Number.isNaN(since.getTime()) && fileStat.mtime <= since) {
ctx.set.status = 304;
ctx.set.headers = responseHeaders;
return undefined;
return buildResponse(304, responseHeaders, null);
}
}
responseHeaders["Content-Type"] = getMimeType(absolutePath);
responseHeaders["Content-Length"] = fileStat.size.toString();
ctx.set.status = 200;
ctx.set.headers = responseHeaders;
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 normalizedPrefix = trimmedPrefix.startsWith("/") ? trimmedPrefix : `/${trimmedPrefix}`;
const wildcardRoute = normalizedPrefix === "/" ? "/*" : `${normalizedPrefix}/*`;
const rootDir = resolve(process.cwd(), assets);
const headersTemplate = { ...headers };
return new Elysia({ name: "node-static" })
.get(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, true))
.head(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, false));
const router = new Hono();
router.get(wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, true));
router.on("HEAD", wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, false));
return router;
};

View file

@ -1,18 +1,27 @@
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
import { Elysia } from "elysia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { Hono } from "hono";
import { createDiffRouter } from "../src/diff/router.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 authPlugin = new Elysia({ name: "test-auth" })
.state("user", adminUser)
.derive(() => ({ user: adminUser }));
const app = new Hono<AppEnv>();
return new Elysia({ name: "diff-test" })
.use(authPlugin)
.use(createDiffRouter(diffManager));
app.use("*", async (c, next) => {
const auth: AuthStore = {
user: adminUser,
refreshed: false,
authenticated: true,
};
c.set("auth", auth);
await next();
});
app.route("/diff", createDiffRouter(diffManager));
return app;
};
describe("Diff router integration", () => {
@ -43,15 +52,8 @@ describe("Diff router integration", () => {
app = createTestApp(diffManager);
});
afterEach(async () => {
if (app?.server) {
await app.stop();
}
vi.clearAllMocks();
});
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);
const payload = await response.json();
@ -77,7 +79,7 @@ describe("Diff router integration", () => {
commitMock.mockResolvedValueOnce(555);
const response = await app.handle(request);
const response = await app.fetch(request);
expect(response.status).toBe(200);
const payload = await response.json();
@ -92,7 +94,7 @@ describe("Diff router integration", () => {
body: JSON.stringify({ type: "comic" }),
});
const response = await app.handle(request);
const response = await app.fetch(request);
expect(response.status).toBe(200);
const payload = await response.json();

View file

@ -1,20 +1,12 @@
import { describe, expect, it } from "vitest";
import { ClientRequestError, error_handler } from "../src/route/error_handler.ts";
import { DocumentBodySchema } from "dbtype";
const createSet = () => ({ status: undefined as number | string | undefined });
import { z } from "zod";
import { ClientRequestError, mapErrorToResponse } from "../src/route/error_handler.ts";
describe("error_handler", () => {
it("formats ClientRequestError with provided status", () => {
const set = createSet();
const result = error_handler({
code: "UNKNOWN",
error: new ClientRequestError(400, "invalid payload"),
set,
});
expect(set.status).toBe(400);
expect(result).toEqual({
const { status, body } = mapErrorToResponse(new ClientRequestError(400, "invalid payload"));
expect(status).toBe(400);
expect(body).toEqual({
code: 400,
message: "BadRequest",
detail: "invalid payload",
@ -22,35 +14,26 @@ describe("error_handler", () => {
});
it("coerces ZodError into a 400 response", () => {
const parseResult = DocumentBodySchema.safeParse({});
const set = createSet();
const schema = z.object({ foo: z.string() });
const parseResult = schema.safeParse({});
if (parseResult.success) {
throw new Error("Expected validation error");
}
const result = error_handler({
code: "VALIDATION",
error: parseResult.error,
set,
});
const { status, body } = mapErrorToResponse(parseResult.error);
expect(set.status).toBe(400);
expect(result.code).toBe(400);
expect(result.message).toBe("BadRequest");
expect(result.detail).toContain("Required");
expect(status).toBe(400);
expect(body.code).toBe(400);
expect(body.message).toBe("BadRequest");
expect(body.detail).toContain("expected string");
});
it("defaults to 500 for unexpected errors", () => {
const set = createSet();
const result = error_handler({
code: "INTERNAL_SERVER_ERROR",
error: new Error("boom"),
set,
});
const { status, body } = mapErrorToResponse(new Error("boom"));
expect(set.status).toBe(500);
expect(result).toEqual({
expect(status).toBe(500);
expect(body).toEqual({
code: 500,
message: "Internal Server Error",
detail: "boom",

View file

@ -1,12 +1,13 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Elysia } from "elysia";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Hono } from "hono";
import { Kysely, SqliteDialect } from "kysely";
import SqliteDatabase from "better-sqlite3";
import type { db } from "dbtype";
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 { 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 => {
if (error instanceof Error) {
@ -56,36 +57,40 @@ describe("settings router", () => {
});
const createTestApp = (username: string) => {
const user = { username, permission: [] as string[] };
return new Elysia({ name: `settings-test-${username}` })
.state("user", user)
.derive(() => ({ user }))
.onError((context) =>
error_handler({
code: typeof context.code === "number" ? String(context.code) : context.code,
error: normalizeError(context.error),
set: context.set,
}),
)
.use(createSettingsRouter(database));
const app = new Hono<AppEnv>();
const auth: AuthStore = {
user: { username, permission: [] },
refreshed: false,
authenticated: true,
};
app.use("*", async (c, next) => {
c.set("auth", auth);
await next();
});
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 () => {
const app = createTestApp("guest");
try {
const response = await app.handle(new Request("http://localhost/settings"));
const response = await app.fetch(new Request("http://localhost/settings"));
expect(response.status).toBe(403);
} finally {
if (app.server) {
await app.stop();
}
}
});
it("returns current configuration for admin", async () => {
const app = createTestApp("admin");
try {
const response = await app.handle(new Request("http://localhost/settings"));
const response = await app.fetch(new Request("http://localhost/settings"));
expect(response.status).toBe(200);
const payload = await response.json();
@ -104,17 +109,11 @@ describe("settings router", () => {
},
});
expect(Array.isArray(payload.permissions)).toBe(true);
expect(new Set(payload.permissions)).toEqual(new Set(Object.values(Permission)));
} finally {
if (app.server) {
await app.stop();
}
}
expect(new Set(payload.permissions)).toEqual(new Set(PERMISSIONS));
});
it("updates persisted settings and returns the new state", async () => {
const app = createTestApp("admin");
try {
const request = new Request("http://localhost/settings", {
method: "PATCH",
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);
const payload = await response.json();
@ -138,7 +137,7 @@ describe("settings router", () => {
});
// 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);
const followUpPayload = await followUp.json();
expect(followUpPayload.persisted).toEqual({
@ -147,10 +146,5 @@ describe("settings router", () => {
forbid_remote_admin_login: false,
guest: ["QueryContent"],
});
} finally {
if (app.server) {
await app.stop();
}
}
});
});

397
pnpm-lock.yaml generated
View file

@ -148,8 +148,8 @@ importers:
packages/dbtype:
dependencies:
zod:
specifier: ^3.23.8
version: 3.23.8
specifier: ^4.1.12
version: 4.1.12
devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.9
@ -169,18 +169,18 @@ importers:
packages/server:
dependencies:
'@elysiajs/cors':
specifier: ^1.3.3
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))
'@elysiajs/html':
specifier: ^1.3.1
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)
'@elysiajs/node':
specifier: ^1.4.1
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))
'@elysiajs/openapi':
specifier: ^1.4.11
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))
'@hono/node-server':
specifier: ^1.19.6
version: 1.19.6(hono@4.10.4)
'@hono/standard-validator':
specifier: ^0.1.5
version: 0.1.5(@standard-schema/spec@1.0.0)(hono@4.10.4)
'@hono/zod-openapi':
specifier: ^1.1.4
version: 1.1.4(hono@4.10.4)(zod@4.1.12)
'@hono/zod-validator':
specifier: ^0.7.4
version: 0.7.4(hono@4.10.4)(zod@4.1.12)
'@std/async':
specifier: npm:@jsr/std__async@^1.0.13
version: '@jsr/std__async@1.0.13'
@ -199,9 +199,9 @@ importers:
dotenv:
specifier: ^16.5.0
version: 16.5.0
elysia:
specifier: ^1.4.9
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)
hono:
specifier: ^4.10.4
version: 4.10.4
jose:
specifier: ^5.10.0
version: 5.10.0
@ -214,6 +214,9 @@ importers:
tiny-async-pool:
specifier: ^1.3.0
version: 1.3.0
zod:
specifier: ^4.1.12
version: 4.1.12
devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.13
@ -248,6 +251,11 @@ packages:
resolution: {integrity: sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==}
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':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@ -412,29 +420,6 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@ -756,6 +741,31 @@ packages:
'@floating-ui/utils@0.2.9':
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':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'}
@ -794,17 +804,6 @@ packages:
'@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}
'@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':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1337,8 +1336,8 @@ packages:
cpu: [x64]
os: [win32]
'@sinclair/typebox@0.34.41':
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@swc/core-darwin-arm64@1.11.31':
resolution: {integrity: sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==}
@ -1424,13 +1423,6 @@ packages:
'@tanstack/virtual-core@3.13.9':
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':
resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==}
@ -1767,10 +1759,6 @@ packages:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
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:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
@ -1809,13 +1797,6 @@ packages:
convert-source-map@2.0.0:
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:
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'}
@ -1829,14 +1810,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
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:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@ -1902,15 +1875,6 @@ packages:
supports-color:
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:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
@ -1986,20 +1950,6 @@ packages:
electron-to-chromium@1.5.165:
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:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -2096,14 +2046,6 @@ packages:
eventemitter3@4.0.7:
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:
resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
@ -2116,9 +2058,6 @@ packages:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -2143,17 +2082,10 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
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:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@ -2205,10 +2137,6 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
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:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
@ -2273,6 +2201,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
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:
resolution: {integrity: sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA==}
engines: {node: '>= 14'}
@ -2653,8 +2585,8 @@ packages:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
openapi3-ts@4.5.0:
resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
@ -2910,10 +2842,6 @@ packages:
resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==}
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:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@ -3018,11 +2946,6 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
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:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@ -3068,10 +2991,6 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
strtok3@10.3.4:
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
engines: {node: '>=18'}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@ -3155,10 +3074,6 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
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:
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
engines: {node: '>=16'}
@ -3204,10 +3119,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uint8array-extras@1.5.0:
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
engines: {node: '>=18'}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@ -3362,10 +3273,6 @@ packages:
wrappy@1.0.2:
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:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -3374,24 +3281,16 @@ packages:
engines: {node: '>= 14.6'}
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:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
zod@3.25.56:
resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==}
zod@4.1.12:
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
snapshots:
'@alloc/quick-lru@5.2.0': {}
@ -3403,6 +3302,11 @@ snapshots:
'@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':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@ -3600,31 +3504,6 @@ snapshots:
'@biomejs/cli-win32-x64@1.6.3':
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':
optional: true
@ -3809,6 +3688,28 @@ snapshots:
'@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':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@ -3849,18 +3750,6 @@ snapshots:
'@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':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -4350,7 +4239,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.42.0':
optional: true
'@sinclair/typebox@0.34.41': {}
'@standard-schema/spec@1.0.0': {}
'@swc/core-darwin-arm64@1.11.31':
optional: true
@ -4412,18 +4301,6 @@ snapshots:
'@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':
dependencies:
fast-glob: 3.3.3
@ -4812,12 +4689,6 @@ snapshots:
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: {}
clsx@2.1.1: {}
@ -4844,10 +4715,6 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-es@2.0.0: {}
cookie@1.0.2: {}
cosmiconfig@8.3.6(typescript@5.8.3):
dependencies:
import-fresh: 3.3.1
@ -4863,10 +4730,6 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crossws@0.4.1(srvx@0.8.9):
optionalDependencies:
srvx: 0.8.9
cssesc@3.0.0: {}
csstype@3.1.3: {}
@ -4915,11 +4778,6 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
optional: true
decimal.js-light@2.5.1: {}
decompress-response@6.0.0:
@ -4975,17 +4833,6 @@ snapshots:
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@9.2.2: {}
@ -5148,10 +4995,6 @@ snapshots:
eventemitter3@4.0.7: {}
exact-mirror@0.2.0(@sinclair/typebox@0.34.41):
optionalDependencies:
'@sinclair/typebox': 0.34.41
execa@7.2.0:
dependencies:
cross-spawn: 7.0.6
@ -5168,8 +5011,6 @@ snapshots:
expect-type@1.2.1: {}
fast-decode-uri-component@1.0.1: {}
fast-deep-equal@3.1.3: {}
fast-equals@5.2.2: {}
@ -5195,23 +5036,10 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.8.2:
optional: true
file-entry-cache@6.0.1:
dependencies:
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: {}
fill-range@7.1.1:
@ -5259,8 +5087,6 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-nonce@1.0.1: {}
get-stream@6.0.1: {}
@ -5332,6 +5158,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
hono@4.10.4: {}
https-proxy-agent@6.2.1:
dependencies:
agent-base: 7.1.3
@ -5615,7 +5443,9 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
openapi-types@12.1.3: {}
openapi3-ts@4.5.0:
dependencies:
yaml: 2.8.0
optionator@0.9.4:
dependencies:
@ -5899,8 +5729,6 @@ snapshots:
regexparam@3.0.0: {}
require-directory@2.1.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@ -6026,10 +5854,6 @@ snapshots:
source-map@0.6.1: {}
srvx@0.8.9:
dependencies:
cookie-es: 2.0.0
stackback@0.0.2: {}
std-env@3.9.0: {}
@ -6070,11 +5894,6 @@ snapshots:
strip-json-comments@3.1.1: {}
strtok3@10.3.4:
dependencies:
'@tokenizer/token': 0.3.0
optional: true
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@ -6186,13 +6005,6 @@ snapshots:
dependencies:
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):
dependencies:
typescript: 5.8.3
@ -6233,9 +6045,6 @@ snapshots:
typescript@5.8.3: {}
uint8array-extras@1.5.0:
optional: true
undici-types@6.21.0: {}
undici-types@7.8.0: {}
@ -6457,26 +6266,12 @@ snapshots:
wrappy@1.0.2: {}
y18n@5.0.8: {}
yallist@3.1.1: {}
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: {}
zod@3.23.8: {}
zod@3.25.56: {}
zod@4.1.12: {}