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" "typescript": "^5.4.3"
}, },
"type": "module", "type": "module",
"dependencies": { "peerDependencies": {
"zod": "^3.23.8" "zod": "^4.1.12"
} }
} }

View file

@ -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>;

View file

@ -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",

View file

@ -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,

View file

@ -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";

View file

@ -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

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 { 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;
};

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 { 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,97 +255,99 @@ 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) => {

View file

@ -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

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 { 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 [];
} }
const normalized = new Set<Permission>();
type PermissionCheckContext = { for (const value of values) {
user?: UserState["user"]; if (isPermission(value)) {
store?: { user?: UserState["user"] }; normalized.add(value);
} & 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"]; }
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();
}; };

View file

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

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 { 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;

View file

@ -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 => {

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 { 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;

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

View file

@ -1,28 +1,23 @@
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) {
@ -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}/`);

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 { 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;
}; };

View file

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

View file

@ -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",

View file

@ -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
View file

@ -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: {}