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