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