completely cleanup of the legacy hono router to leaner svelte remote functions

This commit is contained in:
user
2026-03-01 04:21:55 +02:00
parent ca056b817d
commit 596dcc78fc
18 changed files with 610 additions and 1379 deletions

View File

@@ -21,19 +21,16 @@
"@opentelemetry/exporter-logs-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.212.0",
"@opentelemetry/sdk-node": "^0.212.0",
"@opentelemetry/sdk-logs": "^0.212.0",
"@opentelemetry/sdk-node": "^0.212.0",
"@pkg/db": "workspace:*",
"@pkg/logger": "workspace:*",
"@pkg/logic": "workspace:*",
"@pkg/result": "workspace:*",
"@pkg/settings": "workspace:*",
"@tanstack/svelte-query": "^6.0.18",
"better-auth": "^1.4.20",
"date-fns": "^4.1.0",
"hono": "^4.11.1",
"import-in-the-middle": "^3.0.0",
"marked": "^17.0.1",
"nanoid": "^5.1.6",
"neverthrow": "^8.2.0",
"qrcode": "^1.5.4",

View File

@@ -1,13 +0,0 @@
import { usersRouter } from "@pkg/logic/domains/user/router";
import { twofaRouter } from "@pkg/logic/domains/2fa/router";
import { Hono } from "hono";
const baseRouter = new Hono()
.route("/users", usersRouter)
.route("/twofactor", twofaRouter);
export const apiBasePath = "/api/v1";
export const api = new Hono().route(apiBasePath, baseRouter);
export type Router = typeof baseRouter;

View File

@@ -1,183 +0,0 @@
<script lang="ts">
import hljs from "highlight.js/lib/core";
import { marked } from "marked";
// Import common languages for syntax highlighting
import bash from "highlight.js/lib/languages/bash";
import css from "highlight.js/lib/languages/css";
import javascript from "highlight.js/lib/languages/javascript";
import json from "highlight.js/lib/languages/json";
import python from "highlight.js/lib/languages/python";
import sql from "highlight.js/lib/languages/sql";
import typescript from "highlight.js/lib/languages/typescript";
import xml from "highlight.js/lib/languages/xml";
// Register languages
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("typescript", typescript);
hljs.registerLanguage("python", python);
hljs.registerLanguage("sql", sql);
hljs.registerLanguage("json", json);
hljs.registerLanguage("xml", xml);
hljs.registerLanguage("css", css);
hljs.registerLanguage("bash", bash);
hljs.registerLanguage("html", xml); // HTML is an alias for XML
hljs.registerLanguage("js", javascript); // JS is an alias for JavaScript
hljs.registerLanguage("ts", typescript); // TS is an alias for TypeScript
hljs.registerLanguage("sh", bash); // SH is an alias for Bash
hljs.registerLanguage("shell", bash); // alias
interface Props {
content: string;
class?: string;
}
let { content, class: className = "" }: Props = $props();
// Configure marked with custom extensions
marked.use({
renderer: {
codespan(token: any) {
return `<code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono">${token.text}</code>`;
},
blockquote(token: any) {
return `<blockquote class="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-700 dark:text-gray-300 my-4">${this.parser.parseInline(token.tokens)}</blockquote>`;
},
table(token: any) {
let header = "";
let body = "";
// Process header
if (token.header && token.header.length > 0) {
header = "<thead><tr>";
for (const cell of token.header) {
header += `<th class="px-4 py-2 bg-secondary dark:bg-secondary font-semibold text-left">${this.parser.parseInline(cell.tokens)}</th>`;
}
header += "</tr></thead>";
}
// Process rows
if (token.rows && token.rows.length > 0) {
body = "<tbody>";
for (const row of token.rows) {
body += "<tr class='border-b border-border";
for (const cell of row) {
body += `<td class="px-4 py-2">${this.parser.parseInline(cell.tokens)}</td>`;
}
body += "</tr>";
}
body += "</tbody>";
}
return `<div class="overflow-x-auto my-4"><table class="min-w-full border border-border">${header}${body}</table></div>`;
},
},
gfm: true,
breaks: true,
});
</script>
<!-- Include highlight.js CSS for syntax highlighting -->
<svelte:head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
media="(prefers-color-scheme: dark)"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
media="(prefers-color-scheme: light)"
/>
</svelte:head>
<div
class="prose prose-sm dark:prose-invert max-w-none {className}"
class:prose-gray={true}
>
{@html marked(content)}
</div>
<style>
@reference "../../../app.css";
:global(.prose h1) {
@apply mt-6 mb-4 text-2xl font-bold text-gray-900 dark:text-gray-100;
}
:global(.prose h2) {
@apply mt-5 mb-3 text-xl font-semibold text-gray-900 dark:text-gray-100;
}
:global(.prose h3) {
@apply mt-4 mb-2 text-lg font-medium text-gray-900 dark:text-gray-100;
}
:global(.prose p) {
@apply mb-3 leading-relaxed text-gray-700 dark:text-gray-300;
}
:global(.prose a) {
@apply text-blue-600 underline hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300;
}
:global(.prose strong) {
@apply font-semibold text-gray-900 dark:text-gray-100;
}
:global(.prose em) {
@apply text-gray-700 italic dark:text-gray-300;
}
:global(.prose ul) {
@apply my-4 ml-4 list-inside list-disc space-y-1;
}
:global(.prose ol) {
@apply my-4 ml-4 list-inside list-decimal space-y-1;
}
:global(.prose li) {
@apply leading-relaxed text-gray-700 dark:text-gray-300;
}
:global(.prose ul ul) {
@apply mt-1 mb-1 ml-4;
}
:global(.prose ol ol) {
@apply mt-1 mb-1 ml-4;
}
:global(.prose ul ol) {
@apply mt-1 mb-1 ml-4;
}
:global(.prose ol ul) {
@apply mt-1 mb-1 ml-4;
}
/* Code block scrollbar styling */
:global(.prose pre) {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
:global(.prose pre::-webkit-scrollbar) {
height: 8px;
}
:global(.prose pre::-webkit-scrollbar-track) {
background: transparent;
}
:global(.prose pre::-webkit-scrollbar-thumb) {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
:global(.prose pre::-webkit-scrollbar-thumb:hover) {
background: rgba(255, 255, 255, 0.5);
}
</style>

View File

@@ -1,4 +1,5 @@
import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context";
import type { Err } from "@pkg/result";
export async function getFlowExecCtxForRemoteFuncs(
locals: App.Locals,
@@ -9,3 +10,16 @@ export async function getFlowExecCtxForRemoteFuncs(
sessionId: locals.session?.id,
};
}
export function unauthorized(fctx: FlowExecCtx) {
return {
data: null,
error: {
flowId: fctx.flowId,
code: "UNAUTHORIZED",
message: "User not authenticated",
description: "Please log in",
detail: "No user ID found in session",
} as Err,
};
}

View File

@@ -1,5 +0,0 @@
export function formatCurrency(amount: number, currency = "EUR"): string {
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(
amount,
);
}

View File

@@ -1,22 +1,168 @@
import { ensureAccountExistsSchema } from "@pkg/logic/domains/user/data";
import {
banUserSchema,
checkUsernameSchema,
ensureAccountExistsSchema,
rotatePasswordSchema,
} from "@pkg/logic/domains/user/data";
import {
getFlowExecCtxForRemoteFuncs,
unauthorized,
} from "$lib/core/server.utils";
import { getUserController } from "@pkg/logic/domains/user/controller";
import { command, getRequestEvent, query } from "$app/server";
import * as v from "valibot";
const uc = getUserController();
export const getMyInfoSQ = query(async () => {
const event = getRequestEvent();
// .... do stuff usually done in a router here
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
return { data: "testing" };
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.getUserInfo(fctx, fctx.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const getUsersInfoByIdSQ = query(async () => {
// .... do stuff usually done in a router here
return { data: "testing" };
});
export const getUserInfoByIdSQ = query(
v.object({ userId: v.string() }),
async (input) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
export const ensureAccountExistsSQ = command(
ensureAccountExistsSchema,
async (payload) => {
// .... do stuff usually done in a router here
return { data: "testing" };
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.getUserInfo(fctx, input.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const ensureAccountExistsSC = command(
ensureAccountExistsSchema,
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.ensureAccountExists(fctx, payload.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const checkUsernameSC = command(checkUsernameSchema, async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.isUsernameAvailable(fctx, payload.username);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const update2faVerifiedSC = command(
v.object({ userId: v.string() }),
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.updateLastVerified2FaAtToNow(fctx, payload.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const banUserSC = command(banUserSchema, async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.banUser(
fctx,
payload.userId,
payload.reason,
payload.banExpiresAt,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const isUserBannedSQ = query(
v.object({ userId: v.string() }),
async (input) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.isUserBanned(fctx, input.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const getBanInfoSQ = query(
v.object({ userId: v.string() }),
async (input) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.getBanInfo(fctx, input.userId);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const rotatePasswordSC = command(
rotatePasswordSchema,
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await uc.rotatePassword(
fctx,
payload.userId,
payload.password,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);

View File

@@ -1,10 +1,11 @@
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import type { User } from "@pkg/logic/domains/user/data";
import { apiClient, user as userStore } from "$lib/global.stores";
import { user as userStore } from "$lib/global.stores";
import { rotatePasswordSC } from "./account.remote";
import { authClient } from "$lib/auth.client";
import type { Err } from "@pkg/result";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import type { Err } from "@pkg/result";
class AccountViewModel {
loading = $state(false);
@@ -20,22 +21,20 @@ class AccountViewModel {
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (response.error) {
return errAsync({
code: "API_ERROR",
message:
response.error.message ??
"Failed to update profile picture",
description:
response.error.statusText ??
"Please try again later",
detail: response.error.statusText ?? "Unknown error",
});
}
return okAsync(response.data);
});
).andThen((response) => {
if (response.error) {
return errAsync({
code: "API_ERROR",
message:
response.error.message ??
"Failed to update profile picture",
description:
response.error.statusText ?? "Please try again later",
detail: response.error.statusText ?? "Unknown error",
});
}
return okAsync(response.data);
});
return result.match(
() => {
@@ -43,7 +42,8 @@ class AccountViewModel {
return true;
},
(error) => {
this.errorMessage = error.message ?? "Failed to update profile picture";
this.errorMessage =
error.message ?? "Failed to update profile picture";
toast.error(this.errorMessage, {
description: error.description,
});
@@ -71,21 +71,19 @@ class AccountViewModel {
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (response.error) {
return errAsync({
code: "API_ERROR",
message:
response.error.message ?? "Failed to update profile",
description:
response.error.statusText ??
"Please try again later",
detail: response.error.statusText ?? "Unknown error",
});
}
return okAsync(response.data);
});
).andThen((response) => {
if (response.error) {
return errAsync({
code: "API_ERROR",
message:
response.error.message ?? "Failed to update profile",
description:
response.error.statusText ?? "Please try again later",
detail: response.error.statusText ?? "Unknown error",
});
}
return okAsync(response.data);
});
const user = result.match(
(data) => {
@@ -117,52 +115,20 @@ class AccountViewModel {
toast.error(this.errorMessage);
return false;
}
const client = get(apiClient);
if (!client) {
this.passwordLoading = false;
this.errorMessage = "API client not initialized";
toast.error(this.errorMessage);
return false;
}
const result = await ResultAsync.fromPromise(
client.users["rotate-password"].$put({
json: { userId: currentUser.id, password },
}),
rotatePasswordSC({ userId: currentUser.id, password }),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to change password",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to change password",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail:
error instanceof Error
? error.message
: String(error),
}),
).andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
});
).andThen((apiResult: any) => {
if (apiResult?.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult?.data);
});
const success = result.match(
() => {
@@ -170,7 +136,8 @@ class AccountViewModel {
return true;
},
(error) => {
this.errorMessage = error.message ?? "Failed to change password";
this.errorMessage =
(error.message as string) ?? "Failed to change password";
toast.error(this.errorMessage, {
description: error.description,
});

View File

@@ -3,27 +3,15 @@ import {
getNotificationsSchema,
} from "@pkg/logic/domains/notifications/data";
import { getNotificationController } from "@pkg/logic/domains/notifications/controller";
import type { FlowExecCtx } from "@pkg/logic/core/flow.execution.context";
import { getFlowExecCtxForRemoteFuncs } from "$lib/core/server.utils";
import {
getFlowExecCtxForRemoteFuncs,
unauthorized,
} from "$lib/core/server.utils";
import { command, getRequestEvent, query } from "$app/server";
import type { Err } from "@pkg/result";
import * as v from "valibot";
const nc = getNotificationController();
export async function unauthorized(fctx: FlowExecCtx) {
return {
data: null,
error: {
flowId: fctx.flowId,
code: "UNAUTHORIZED",
message: "User not authenticated",
description: "Please log in",
detail: "No user found in request locals",
} as Err,
};
}
export const getNotificationsSQ = query(
getNotificationsSchema,
async (input) => {

View File

@@ -1,10 +1,12 @@
import { apiClient, session, user } from "$lib/global.stores";
import { session, user } from "$lib/global.stores";
import { authClient } from "$lib/auth.client";
import {
startVerificationSessionSC,
verifySessionCodeSC,
} from "$lib/domains/security/twofa.remote";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
import { page } from "$app/state";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import type { Err } from "@pkg/result";
class TwoFactorVerifyViewModel {
verifying = $state(false);
@@ -32,64 +34,33 @@ class TwoFactorVerifyViewModel {
return;
}
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor["start-verification-session"].$post({
json: { userId: uid, sessionId: sid },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to start verification",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to start verification",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
if (!apiResult.data?.verificationToken) {
return errAsync({
code: "API_ERROR",
message: "No verification token received",
description: "Invalid response data",
detail: "Missing verificationToken in response",
});
}
return okAsync(apiResult.data.verificationToken);
try {
const result = await startVerificationSessionSC({
userId: uid,
sessionId: sid,
});
result.match(
(token) => {
this.verificationToken = token;
},
(error) => {
this.errorMessage = error.message || "Failed to start verification";
if (result?.error || !result?.data?.verificationToken) {
this.errorMessage =
result?.error?.message || "Failed to start verification";
toast.error("Failed to start verification", {
description: this.errorMessage || "Failed to start verification",
description: this.errorMessage,
});
},
);
return;
}
this.startingVerification = false;
this.verificationToken = result.data.verificationToken;
} catch (error) {
this.errorMessage = "Failed to start verification";
toast.error("Failed to start verification", {
description:
error instanceof Error
? error.message
: "Failed to start verification",
});
} finally {
this.startingVerification = false;
}
}
async verifyCode() {
@@ -106,69 +77,22 @@ class TwoFactorVerifyViewModel {
this.verifying = true;
this.errorMessage = null;
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor["verify-session-code"].$post({
json: {
verificationToken: this.verificationToken,
code: this.verificationCode,
},
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Verification failed",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Verification failed",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
if (!apiResult.data?.success) {
return errAsync({
code: "VALIDATION_ERROR",
message: "Invalid verification code",
description: "Verification failed",
detail: "Code verification unsuccessful",
});
}
return okAsync(apiResult.data);
try {
const result = await verifySessionCodeSC({
verificationToken: this.verificationToken,
code: this.verificationCode,
});
result.match(
() => {
const redirectUrl = page.url.searchParams.get("redirect") || "/";
window.location.href = redirectUrl;
},
(error) => {
this.errorMessage = error.message || "Failed to verify code";
if (result?.error || !result?.data?.success) {
this.errorMessage =
result?.error?.message || "Failed to verify code";
if (error.code === "BANNED") {
authClient.signOut();
if (result?.error?.code === "BANNED") {
await authClient.signOut();
window.location.href = "/auth/login";
return;
}
// Don't show toast for invalid codes, just show in UI
if (
this.errorMessage &&
!this.errorMessage.includes("Invalid") &&
@@ -178,10 +102,20 @@ class TwoFactorVerifyViewModel {
description: this.errorMessage,
});
}
},
);
return;
}
this.verifying = false;
const redirectUrl = page.url.searchParams.get("redirect") || "/";
window.location.href = redirectUrl;
} catch (error) {
this.errorMessage = "Failed to verify code";
toast.error("Verification failed", {
description:
error instanceof Error ? error.message : this.errorMessage,
});
} finally {
this.verifying = false;
}
}
async handleBackupCode() {
@@ -193,65 +127,25 @@ class TwoFactorVerifyViewModel {
this.verifying = true;
this.errorMessage = null;
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor["verify-session-code"].$post({
json: {
verificationToken: this.verificationToken,
code: this.verificationCode,
},
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Verification failed",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Verification failed",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
if (!apiResult.data?.success) {
return errAsync({
code: "VALIDATION_ERROR",
message: "Invalid backup code",
description: "Verification failed",
detail: "Backup code verification unsuccessful",
});
}
return okAsync(apiResult.data);
try {
const result = await verifySessionCodeSC({
verificationToken: this.verificationToken,
code: this.verificationCode,
});
result.match(
() => {
const redirectUrl = page.url.searchParams.get("redirect") || "/";
window.location.href = redirectUrl;
},
(error) => {
this.errorMessage = error.message || "Invalid backup code";
},
);
if (result?.error || !result?.data?.success) {
this.errorMessage = result?.error?.message || "Invalid backup code";
return;
}
this.verifying = false;
const redirectUrl = page.url.searchParams.get("redirect") || "/";
window.location.href = redirectUrl;
} catch (error) {
this.errorMessage =
error instanceof Error ? error.message : "Invalid backup code";
} finally {
this.verifying = false;
}
}
reset() {

View File

@@ -1,9 +1,11 @@
import { apiClient } from "$lib/global.stores";
import {
disableTwoFactorSC,
generateBackupCodesSQ,
setupTwoFactorSC,
verifyAndEnableTwoFactorSC,
} from "$lib/domains/security/twofa.remote";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
import QRCode from "qrcode";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import type { Err } from "@pkg/result";
class TwoFactorViewModel {
twoFactorEnabled = $state(false);
@@ -20,86 +22,38 @@ class TwoFactorViewModel {
this.isLoading = true;
this.errorMessage = null;
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor.setup.$post({
json: { code: "" },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to set up two-factor authentication.",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to set up two-factor authentication.",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
if (!apiResult.data || !apiResult.data.totpURI) {
return errAsync({
code: "API_ERROR",
message: "Failed to get 2FA setup information",
description: "Invalid response data",
detail: "Missing totpURI in response",
});
}
return okAsync(apiResult.data);
})
.andThen((data) => {
return ResultAsync.fromPromise(
QRCode.toDataURL(data.totpURI, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#FFFFFF" },
}),
(error): Err => ({
code: "PROCESSING_ERROR",
message: "Failed to generate QR code.",
description: "QR code generation failed",
detail: error instanceof Error ? error.message : String(error),
}),
).map((qrCodeDataUrl) => ({
...data,
qrCodeDataUrl,
}));
try {
const result = await setupTwoFactorSC({});
if (result?.error || !result?.data?.totpURI) {
this.errorMessage =
result?.error?.message || "Could not enable 2FA";
toast.error(this.errorMessage, {
description:
result?.error?.description || "Please try again later",
});
return;
}
const qrCodeDataUrl = await QRCode.toDataURL(result.data.totpURI, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#FFFFFF" },
});
result.match(
({ qrCodeDataUrl, secret, totpURI }) => {
this.qrCodeUrl = qrCodeDataUrl;
this.twoFactorSetupInProgress = true;
this.twoFactorSecret = secret;
this.twoFactorVerificationCode = "";
toast("Setup enabled");
},
(error) => {
this.errorMessage = error.message || "Could not enable 2FA";
toast.error(this.errorMessage || "Could not enable 2FA", {
description: error.description || "Please try again later",
});
},
);
this.isLoading = false;
this.qrCodeUrl = qrCodeDataUrl;
this.twoFactorSetupInProgress = true;
this.twoFactorSecret = result.data.secret;
this.twoFactorVerificationCode = "";
toast("Setup enabled");
} catch (error) {
this.errorMessage = "Could not enable 2FA";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
async completeTwoFactorSetup() {
@@ -112,233 +66,109 @@ class TwoFactorViewModel {
this.isLoading = true;
this.errorMessage = null;
const verifyResult = await ResultAsync.fromPromise(
get(apiClient).twofactor["verify-and-enable"].$post({
json: { code: this.twoFactorVerificationCode },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to verify code.",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to verify code.",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
try {
const verifyResult = await verifyAndEnableTwoFactorSC({
code: this.twoFactorVerificationCode,
});
verifyResult.match(
async () => {
const backupCodesResult = await ResultAsync.fromPromise(
get(apiClient).twofactor["generate-backup-codes"].$get(),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to generate backup codes",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to generate backup codes",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data || []);
});
backupCodesResult.match(
(backupCodes) => {
this.backupCodes = backupCodes;
this.showingBackupCodes = true;
},
() => {
toast.error(
"2FA enabled, but failed to generate backup codes",
{
description: "You can generate them later in settings",
},
);
},
);
this.twoFactorEnabled = true;
this.twoFactorSetupInProgress = false;
this.twoFactorVerificationCode = "";
toast.success("Two-factor authentication enabled", {
description: "Your account is now more secure",
if (verifyResult?.error) {
this.errorMessage =
verifyResult.error.message || "Invalid verification code";
toast.error(this.errorMessage, {
description:
verifyResult.error.description || "Please try again",
});
},
(error) => {
this.errorMessage = error.message || "Invalid verification code";
toast.error(this.errorMessage || "Invalid verification code", {
description: error.description || "Please try again",
});
},
);
return;
}
this.isLoading = false;
const backupCodesResult = await generateBackupCodesSQ();
if (backupCodesResult?.error || !Array.isArray(backupCodesResult?.data)) {
toast.error("2FA enabled, but failed to generate backup codes", {
description: "You can generate them later in settings",
});
} else {
this.backupCodes = backupCodesResult.data;
this.showingBackupCodes = true;
}
this.twoFactorEnabled = true;
this.twoFactorSetupInProgress = false;
this.twoFactorVerificationCode = "";
toast.success("Two-factor authentication enabled", {
description: "Your account is now more secure",
});
} catch (error) {
this.errorMessage = "Invalid verification code";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
this.isLoading = false;
}
}
async disableTwoFactor() {
this.isLoading = true;
this.errorMessage = null;
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor.disable.$delete({
json: { code: "" },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to disable two-factor authentication.",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to disable two-factor authentication.",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
() => {
this.twoFactorEnabled = false;
this.backupCodes = [];
this.qrCodeUrl = null;
this.showingBackupCodes = false;
this.twoFactorSecret = null;
toast.success("Two-factor authentication disabled");
},
(error) => {
this.errorMessage = error.message || "Failed to disable 2FA";
toast.error(this.errorMessage || "Failed to disable 2FA", {
description:
error.description || "Please try again later",
try {
const result = await disableTwoFactorSC({ code: "" });
if (result?.error) {
this.errorMessage = result.error.message || "Failed to disable 2FA";
toast.error(this.errorMessage, {
description: result.error.description || "Please try again later",
});
},
);
return;
}
this.isLoading = false;
this.twoFactorEnabled = false;
this.backupCodes = [];
this.qrCodeUrl = null;
this.showingBackupCodes = false;
this.twoFactorSecret = null;
toast.success("Two-factor authentication disabled");
} catch (error) {
this.errorMessage = "Failed to disable 2FA";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
async generateNewBackupCodes() {
this.isLoading = true;
this.errorMessage = null;
const result = await ResultAsync.fromPromise(
get(apiClient).twofactor["generate-backup-codes"].$get(),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to generate new backup codes.",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to generate new backup codes.",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data || []);
});
result.match(
(backupCodes) => {
this.backupCodes = backupCodes;
this.showingBackupCodes = true;
toast.success("New backup codes generated", {
description: "Your previous backup codes are now invalid",
});
},
(error) => {
try {
const result = await generateBackupCodesSQ();
if (result?.error || !Array.isArray(result?.data)) {
this.errorMessage =
error.message || "Failed to generate new backup codes";
toast.error(this.errorMessage || "Failed to generate new backup codes", {
result?.error?.message || "Failed to generate new backup codes";
toast.error(this.errorMessage, {
description:
error.description || "Please try again later",
result?.error?.description || "Please try again later",
});
},
);
return;
}
this.isLoading = false;
this.backupCodes = result.data;
this.showingBackupCodes = true;
toast.success("New backup codes generated", {
description: "Your previous backup codes are now invalid",
});
} catch (error) {
this.errorMessage = "Failed to generate new backup codes";
toast.error(this.errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.isLoading = false;
}
}
copyAllBackupCodes() {

View File

@@ -0,0 +1,205 @@
import {
disable2FASchema,
enable2FACodeSchema,
startVerificationSchema,
verifyCodeSchema,
} from "@pkg/logic/domains/2fa/data";
import {
getFlowExecCtxForRemoteFuncs,
unauthorized,
} from "$lib/core/server.utils";
import { getTwofaController } from "@pkg/logic/domains/2fa/controller";
import { command, getRequestEvent, query } from "$app/server";
import { auth } from "@pkg/logic/domains/auth/config.base";
import type { User } from "@pkg/logic/domains/user/data";
import * as v from "valibot";
const tc = getTwofaController();
function buildIpAddress(headers: Headers) {
return (
headers.get("x-forwarded-for") ?? headers.get("x-real-ip") ?? "unknown"
);
}
function buildUserAgent(headers: Headers) {
return headers.get("user-agent") ?? "unknown";
}
export const setupTwoFactorSC = command(v.object({}), async () => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.setup2FA(fctx, currentUser as User);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const verifyAndEnableTwoFactorSC = command(
enable2FACodeSchema,
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.verifyAndEnable2FA(
fctx,
currentUser as User,
payload.code,
event.request.headers,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const generateBackupCodesSQ = query(async () => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.generateBackupCodes(fctx, currentUser as User);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const disableTwoFactorSC = command(disable2FASchema, async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.disable(fctx, currentUser as User, payload.code);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const requiresVerificationSQ = query(
v.object({ sessionId: v.string() }),
async (input) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.requiresInitialVerification(
fctx,
currentUser as User,
input.sessionId,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const requiresSensitiveActionSQ = query(async () => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
const currentUser = event.locals.user;
if (!fctx.userId || !currentUser) {
return unauthorized(fctx);
}
const res = await tc.requiresSensitiveActionVerification(
fctx,
currentUser as User,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});
export const startVerificationSessionSC = command(
startVerificationSchema,
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await tc.startVerification(fctx, {
userId: payload.userId,
sessionId: payload.sessionId,
ipAddress: buildIpAddress(event.request.headers),
userAgent: buildUserAgent(event.request.headers),
});
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const verifySessionCodeSC = command(
verifyCodeSchema,
async (payload) => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
let currentUser = event.locals.user;
if (!currentUser) {
const sess = await auth.api.getSession({
headers: event.request.headers,
});
currentUser = sess?.user as User | undefined;
}
const res = await tc.verifyCode(
fctx,
{
verificationSessToken: payload.verificationToken,
code: payload.code,
},
currentUser as User | undefined,
);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
},
);
export const cleanupExpiredSessionsSC = command(v.object({}), async () => {
const event = getRequestEvent();
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
if (!fctx.userId) {
return unauthorized(fctx);
}
const res = await tc.cleanupExpiredSessions(fctx);
return res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error };
});

View File

@@ -2,13 +2,7 @@
import { goto } from "$app/navigation";
import { mainNavTree } from "$lib/core/constants";
import { breadcrumbs } from "$lib/global.stores";
import { makeClient } from "$lib/make-client.js";
import { onMount } from "svelte";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
const client = makeClient(fetch);
breadcrumbs.set([mainNavTree[0]]);

View File

@@ -1,25 +1,11 @@
<script lang="ts">
import { browser } from "$app/environment";
import { Toaster } from "$lib/components/ui/sonner/index.js";
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
import { ModeWatcher } from "mode-watcher";
import { apiClient, breadcrumbs } from "$lib/global.stores";
import { makeClient } from "$lib/make-client";
import { onMount } from "svelte";
import { breadcrumbs } from "$lib/global.stores";
import "./layout.css";
let { children }: { children: any } = $props();
const queryClient = new QueryClient({
defaultOptions: {
queries: { enabled: browser },
},
});
onMount(() => {
apiClient.set(makeClient(fetch));
});
</script>
<svelte:head>
@@ -31,6 +17,4 @@
<ModeWatcher />
<Toaster />
<QueryClientProvider client={queryClient}>
{@render children()}
</QueryClientProvider>
{@render children()}

View File

@@ -1,170 +0,0 @@
import {
disable2FASchema,
enable2FACodeSchema,
startVerificationSchema,
verifyCodeSchema,
} from "./data";
import { sValidator } from "@hono/standard-validator";
import { HonoContext } from "@/core/hono.helpers";
import { getTwofaController } from "./controller";
import { auth } from "@domains/auth/config.base";
import { Hono } from "hono";
const twofaController = getTwofaController();
export const twofaRouter = new Hono<HonoContext>()
.post("/setup", async (c) => {
const res = await twofaController.setup2FA(
c.env.locals.fCtx,
c.env.locals.user,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
.post(
"/verify-and-enable",
sValidator("json", enable2FACodeSchema),
async (c) => {
const data = c.req.valid("json");
const res = await twofaController.verifyAndEnable2FA(
c.env.locals.fCtx,
c.env.locals.user,
data.code,
c.req.raw.headers,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
},
)
.get("/generate-backup-codes", async (c) => {
const res = await twofaController.generateBackupCodes(
c.env.locals.fCtx,
c.env.locals.user,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
.delete("/disable", sValidator("json", disable2FASchema), async (c) => {
const data = c.req.valid("json");
const res = await twofaController.disable(
c.env.locals.fCtx,
c.env.locals.user,
data.code,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
.get("/requires-verification", async (c) => {
const user = c.env.locals.user;
const sessionId = c.req.query("sessionId")?.toString() ?? "";
const res = await twofaController.requiresInitialVerification(
c.env.locals.fCtx,
user,
sessionId,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
.get("/requires-sensitive-action", async (c) => {
const res = await twofaController.requiresSensitiveActionVerification(
c.env.locals.fCtx,
c.env.locals.user,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
.post(
"/start-verification-session",
sValidator("json", startVerificationSchema),
async (c) => {
const data = c.req.valid("json");
const ipAddress =
c.req.header("x-forwarded-for") ||
c.req.header("x-real-ip") ||
"unknown";
const userAgent = c.req.header("user-agent") || "unknown";
const res = await twofaController.startVerification(
c.env.locals.fCtx,
{
userId: data.userId,
sessionId: data.sessionId,
ipAddress,
userAgent,
},
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
);
},
)
.post(
"/verify-session-code",
sValidator("json", verifyCodeSchema),
async (c) => {
const data = c.req.valid("json");
let user = c.env.locals.user;
if (!user) {
const out = await auth.api.getSession({
headers: c.req.raw.headers,
});
user = out?.user as any;
}
const res = await twofaController.verifyCode(
c.env.locals.fCtx,
{
verificationSessToken: data.verificationToken,
code: data.code,
},
user,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
},
)
.post("/cleanup-expired-sessions", async (c) => {
const res = await twofaController.cleanupExpiredSessions(
c.env.locals.fCtx,
);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
});

View File

@@ -1,165 +0,0 @@
import {
banUserSchema,
checkUsernameSchema,
ensureAccountExistsSchema,
rotatePasswordSchema,
} from "./data";
import { HonoContext } from "@core/hono.helpers";
import { sValidator } from "@hono/standard-validator";
import { getUserController } from "./controller";
import { Hono } from "hono";
const uc = getUserController();
export const usersRouter = new Hono<HonoContext>()
// Get current user info
.get("/me", async (c) => {
const fctx = c.env.locals.fCtx;
const userId = c.env.locals.user?.id;
if (!userId) {
return c.json(
{
error: {
code: "UNAUTHORIZED",
message: "User not authenticated",
description: "Please log in",
detail: "No user ID found in session",
},
},
401,
);
}
const res = await uc.getUserInfo(fctx, userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Get user info by ID
.get("/:userId", async (c) => {
const fctx = c.env.locals.fCtx;
const userId = c.req.param("userId");
const res = await uc.getUserInfo(fctx, userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Ensure account exists
.put(
"/ensure-account-exists",
sValidator("json", ensureAccountExistsSchema),
async (c) => {
const fctx = c.env.locals.fCtx;
const data = c.req.valid("json");
const res = await uc.ensureAccountExists(fctx, data.userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
},
)
// Check username availability
.post(
"/check-username",
sValidator("json", checkUsernameSchema),
async (c) => {
const fctx = c.env.locals.fCtx;
const data = c.req.valid("json");
const res = await uc.isUsernameAvailable(fctx, data.username);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
},
)
// Update last 2FA verification time
.put("/update-2fa-verified/:userId", async (c) => {
const fctx = c.env.locals.fCtx;
const userId = c.req.param("userId");
const res = await uc.updateLastVerified2FaAtToNow(fctx, userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Ban user
.post("/ban", sValidator("json", banUserSchema), async (c) => {
const fctx = c.env.locals.fCtx;
const data = c.req.valid("json");
const res = await uc.banUser(fctx, data.userId, data.reason, data.banExpiresAt);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Check if user is banned
.get("/:userId/is-banned", async (c) => {
const fctx = c.env.locals.fCtx;
const userId = c.req.param("userId");
const res = await uc.isUserBanned(fctx, userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Get ban info
.get("/:userId/ban-info", async (c) => {
const fctx = c.env.locals.fCtx;
const userId = c.req.param("userId");
const res = await uc.getBanInfo(fctx, userId);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
})
// Rotate password
.put(
"/rotate-password",
sValidator("json", rotatePasswordSchema),
async (c) => {
const fctx = c.env.locals.fCtx;
const data = c.req.valid("json");
const res = await uc.rotatePassword(fctx, data.userId, data.password);
return c.json(
res.isOk()
? { data: res.value, error: null }
: { data: null, error: res.error },
res.isOk() ? 200 : 400,
);
},
);

View File

@@ -4,14 +4,13 @@
"auth:schemagen": "pnpm dlx @better-auth/cli generate --config ./domains/auth/config.base.ts --output ../../packages/db/schema/better.auth.schema.ts"
},
"dependencies": {
"@hono/standard-validator": "^0.2.1",
"@opentelemetry/api": "^1.9.0",
"@otplib/plugin-base32-scure": "^13.3.0",
"@otplib/plugin-crypto-noble": "^13.3.0",
"@otplib/totp": "^13.3.0",
"@pkg/db": "workspace:*",
"@pkg/logger": "workspace:*",
"@pkg/keystore": "workspace:*",
"@pkg/logger": "workspace:*",
"@pkg/result": "workspace:*",
"@pkg/settings": "workspace:*",
"@types/pdfkit": "^0.14.0",
@@ -19,17 +18,13 @@
"better-auth": "^1.4.7",
"date-fns-tz": "^3.2.0",
"dotenv": "^16.5.0",
"hono": "^4.11.1",
"imapflow": "^1.0.188",
"mailparser": "^3.7.3",
"nanoid": "^5.1.5",
"neverthrow": "^8.2.0",
"otplib": "^13.3.0",
"pdfkit": "^0.17.1",
"tmp": "^0.2.3",
"uuid": "^11.1.0",
"valibot": "^1.2.0",
"xlsx": "^0.18.5"
"valibot": "^1.2.0"
},
"devDependencies": {
"@types/bun": "latest",

247
pnpm-lock.yaml generated
View File

@@ -65,24 +65,15 @@ importers:
'@pkg/settings':
specifier: workspace:*
version: link:../../packages/settings
'@tanstack/svelte-query':
specifier: ^6.0.18
version: 6.0.18(svelte@5.53.6)
better-auth:
specifier: ^1.4.20
version: 1.4.20(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))
date-fns:
specifier: ^4.1.0
version: 4.1.0
hono:
specifier: ^4.11.1
version: 4.12.3
import-in-the-middle:
specifier: ^3.0.0
version: 3.0.0
marked:
specifier: ^17.0.1
version: 17.0.3
nanoid:
specifier: ^5.1.6
version: 5.1.6
@@ -289,9 +280,6 @@ importers:
packages/logic:
dependencies:
'@hono/standard-validator':
specifier: ^0.2.1
version: 0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.3)
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.0
@@ -334,9 +322,6 @@ importers:
dotenv:
specifier: ^16.5.0
version: 16.6.1
hono:
specifier: ^4.11.1
version: 4.12.3
imapflow:
specifier: ^1.0.188
version: 1.2.10
@@ -352,12 +337,6 @@ importers:
otplib:
specifier: ^13.3.0
version: 13.3.0
pdfkit:
specifier: ^0.17.1
version: 0.17.2
tmp:
specifier: ^0.2.3
version: 0.2.5
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -367,9 +346,6 @@ importers:
valibot:
specifier: ^1.2.0
version: 1.2.0(typescript@5.9.3)
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies:
'@types/bun':
specifier: latest
@@ -930,12 +906,6 @@ packages:
peerDependencies:
hono: ^4
'@hono/standard-validator@0.2.2':
resolution: {integrity: sha512-mJ7W84Bt/rSvoIl63Ynew+UZOHAzzRAoAXb3JaWuxAkM/Lzg+ZHTCUiz77KOtn2e623WNN8LkD57Dk0szqUrIw==}
peerDependencies:
'@standard-schema/spec': ^1.0.0
hono: '>=3.9.0'
'@iconify/json@2.2.444':
resolution: {integrity: sha512-z0UwFaVtaN/h/iWZ1kzEjqFU3sp0rRy93tzOtpepZU89DY39WsNeYZv2mxtft/2La6Bz2b4z1C/HkU5Cqv3gbw==}
@@ -1872,14 +1842,6 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@tanstack/query-core@5.90.20':
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
'@tanstack/svelte-query@6.0.18':
resolution: {integrity: sha512-iGS8osfrIVUW5pkV4Ig6pspNIMtiNjGnVTNJKDas0m/QaNDFFIKbgg74rCzcjwrTIvO38tMpzb4VUKklvAmjxw==}
peerDependencies:
svelte: ^5.25.0
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
@@ -2030,10 +1992,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -2086,13 +2044,6 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-js@0.0.8:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-auth@1.4.20:
resolution: {integrity: sha512-cUQaUhZ/EZwb7xgoL9wHl78yWp0eaxC/L++B/r8RJxk23L766Tk7fLjWG6bQK8eAHDDpfQNwXsJowiei8tJWJw==}
peerDependencies:
@@ -2176,9 +2127,6 @@ packages:
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -2193,10 +2141,6 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -2218,10 +2162,6 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -2230,10 +2170,6 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -2274,18 +2210,10 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -2447,9 +2375,6 @@ packages:
devalue@5.6.3:
resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==}
dfa@1.2.0:
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
@@ -2666,9 +2591,6 @@ packages:
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
engines: {node: '>=8.0.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -2692,9 +2614,6 @@ packages:
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
fontkit@2.0.4:
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -2713,10 +2632,6 @@ packages:
forwarded-parse@2.1.2:
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2851,10 +2766,6 @@ packages:
jose@6.1.3:
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
jpeg-exif@1.1.4:
resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
@@ -2967,9 +2878,6 @@ packages:
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
engines: {node: '>= 12.0.0'}
linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
@@ -3016,11 +2924,6 @@ packages:
mailparser@3.9.3:
resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==}
marked@17.0.3:
resolution: {integrity: sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==}
engines: {node: '>= 20'}
hasBin: true
memoize-weak@1.0.2:
resolution: {integrity: sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==}
@@ -3144,9 +3047,6 @@ packages:
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
paneforge@1.0.2:
resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==}
peerDependencies:
@@ -3173,9 +3073,6 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pdfkit@0.17.2:
resolution: {integrity: sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==}
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
@@ -3213,9 +3110,6 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
png-js@1.0.0:
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
@@ -3390,9 +3284,6 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
restructure@3.0.2:
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
rimraf@5.0.10:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
@@ -3516,10 +3407,6 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
@@ -3642,9 +3529,6 @@ packages:
tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -3664,10 +3548,6 @@ packages:
resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
hasBin: true
tmp@0.2.5:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
toposort@2.0.2:
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
@@ -3752,12 +3632,6 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
unplugin-icons@23.0.1:
resolution: {integrity: sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ==}
peerDependencies:
@@ -3914,14 +3788,6 @@ packages:
resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==}
engines: {node: '>= 12.0.0'}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@@ -3934,11 +3800,6 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -4301,11 +4162,6 @@ snapshots:
dependencies:
hono: 4.12.3
'@hono/standard-validator@0.2.2(@standard-schema/spec@1.1.0)(hono@4.12.3)':
dependencies:
'@standard-schema/spec': 1.1.0
hono: 4.12.3
'@iconify/json@2.2.444':
dependencies:
'@iconify/types': 2.0.0
@@ -5406,13 +5262,6 @@ snapshots:
tailwindcss: 4.2.1
vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)
'@tanstack/query-core@5.90.20': {}
'@tanstack/svelte-query@6.0.18(svelte@5.53.6)':
dependencies:
'@tanstack/query-core': 5.90.20
svelte: 5.53.6
'@tanstack/table-core@8.21.3': {}
'@types/aws-lambda@8.10.161': {}
@@ -5585,8 +5434,6 @@ snapshots:
acorn@8.16.0: {}
adler-32@1.3.1: {}
agent-base@7.1.4: {}
ansi-regex@5.0.1: {}
@@ -5629,10 +5476,6 @@ snapshots:
balanced-match@1.0.2: {}
base64-js@0.0.8: {}
base64-js@1.5.1: {}
better-auth@1.4.20(@sveltejs/kit@2.53.4(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.6)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.6)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)):
dependencies:
'@better-auth/core': 1.4.20(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
@@ -5680,10 +5523,6 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brotli@1.3.3:
dependencies:
base64-js: 1.5.1
buffer-from@1.1.2: {}
bun-types@1.3.9:
@@ -5695,11 +5534,6 @@ snapshots:
camelcase@8.0.0:
optional: true
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chai@6.2.2: {}
chokidar@4.0.3:
@@ -5727,14 +5561,10 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clone@2.1.2: {}
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -5766,16 +5596,12 @@ snapshots:
cookie@0.6.0: {}
crc-32@1.2.2: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
cssesc@3.0.0: {}
d3-array@2.12.1:
@@ -5911,8 +5737,6 @@ snapshots:
devalue@5.6.3: {}
dfa@1.2.0: {}
dijkstrajs@1.0.3: {}
dlv@1.1.3:
@@ -6106,8 +5930,6 @@ snapshots:
pure-rand: 6.1.0
optional: true
fast-deep-equal@3.1.3: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -6126,18 +5948,6 @@ snapshots:
fn.name@1.1.0: {}
fontkit@2.0.4:
dependencies:
'@swc/helpers': 0.5.19
brotli: 1.3.3
clone: 2.1.2
dfa: 1.2.0
fast-deep-equal: 3.1.3
restructure: 3.0.2
tiny-inflate: 1.0.3
unicode-properties: 1.4.1
unicode-trie: 2.0.0
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -6155,8 +5965,6 @@ snapshots:
forwarded-parse@2.1.2: {}
frac@1.1.2: {}
fsevents@2.3.3:
optional: true
@@ -6325,8 +6133,6 @@ snapshots:
jose@6.1.3: {}
jpeg-exif@1.1.4: {}
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
@@ -6439,11 +6245,6 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.31.1
lightningcss-win32-x64-msvc: 1.31.1
linebreak@1.1.0:
dependencies:
base64-js: 0.0.8
unicode-trie: 2.0.0
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
@@ -6500,8 +6301,6 @@ snapshots:
punycode.js: 2.3.1
tlds: 1.261.0
marked@17.0.3: {}
memoize-weak@1.0.2: {}
memoize@10.2.0:
@@ -6599,8 +6398,6 @@ snapshots:
package-manager-detector@1.6.0: {}
pako@0.2.9: {}
paneforge@1.0.2(svelte@5.53.6):
dependencies:
runed: 0.23.4(svelte@5.53.6)
@@ -6625,14 +6422,6 @@ snapshots:
pathe@2.0.3: {}
pdfkit@0.17.2:
dependencies:
crypto-js: 4.2.0
fontkit: 2.0.4
jpeg-exif: 1.1.4
linebreak: 1.1.0
png-js: 1.0.0
peberminta@0.9.0: {}
pg-int8@1.0.1: {}
@@ -6683,8 +6472,6 @@ snapshots:
exsolve: 1.0.8
pathe: 2.0.3
png-js@1.0.0: {}
pngjs@5.0.0: {}
postcss-selector-parser@6.0.10:
@@ -6814,8 +6601,6 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
restructure@3.0.2: {}
rimraf@5.0.10:
dependencies:
glob: 10.5.0
@@ -6949,10 +6734,6 @@ snapshots:
split2@4.2.0: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
stack-trace@0.0.10: {}
stackback@0.0.2: {}
@@ -7110,8 +6891,6 @@ snapshots:
tiny-case@1.0.3:
optional: true
tiny-inflate@1.0.3: {}
tinybench@2.9.0: {}
tinyexec@1.0.2: {}
@@ -7125,8 +6904,6 @@ snapshots:
tlds@1.261.0: {}
tmp@0.2.5: {}
toposort@2.0.2:
optional: true
@@ -7191,16 +6968,6 @@ snapshots:
undici-types@7.18.2: {}
unicode-properties@1.4.1:
dependencies:
base64-js: 1.5.1
unicode-trie: 2.0.0
unicode-trie@2.0.0:
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
unplugin-icons@23.0.1(svelte@5.53.6):
dependencies:
'@antfu/install-pkg': 1.1.0
@@ -7328,10 +7095,6 @@ snapshots:
triple-beam: 1.4.1
winston-transport: 4.9.0
wmf@1.0.2: {}
word@0.3.0: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
@@ -7350,16 +7113,6 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.2.0
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xtend@4.0.2: {}
y18n@4.0.3: {}