From eb0822bbffa765125a383035b3a2b7ace197543e Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 24 Oct 2025 09:14:31 +0200 Subject: [PATCH] Enhance authentication and authorization components - Refactored `AppAuth` to introduce `getGuardContextSchema` for improved user context handling. - Updated `Authenticator` to utilize `pickKeys` for user data extraction in JWT generation. - Enhanced `Guard` class to improve permission checks and error handling. - Modified `SystemController` to return context schema alongside permissions in API responses. - Added new `permissions` method in `SystemApi` for fetching permissions. - Improved UI components with additional props and tooltip support for better user experience. --- app/package.json | 2 +- app/src/auth/AppAuth.ts | 15 +- app/src/auth/authenticate/Authenticator.ts | 8 +- app/src/auth/authorize/Guard.ts | 12 +- app/src/modules/ModuleHelper.ts | 2 +- app/src/modules/SystemApi.ts | 5 + app/src/modules/server/SystemController.ts | 9 +- app/src/ui/client/api/use-api.ts | 24 ++- app/src/ui/components/buttons/Button.tsx | 4 +- app/src/ui/components/code/CodePreview.tsx | 73 +++++++++ .../form/json-schema-form/FieldWrapper.tsx | 24 ++- .../ui/routes/auth/auth.roles.edit.$role.tsx | 152 +++++++++++++++--- app/src/ui/routes/tools/mcp/mcp.tsx | 2 +- app/src/ui/routes/tools/mcp/tools.tsx | 7 +- bun.lock | 8 +- 15 files changed, 290 insertions(+), 57 deletions(-) create mode 100644 app/src/ui/components/code/CodePreview.tsx diff --git a/app/package.json b/app/package.json index c40f3b1..c256cc9 100644 --- a/app/package.json +++ b/app/package.json @@ -65,7 +65,7 @@ "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", - "jsonv-ts": "0.8.6", + "jsonv-ts": "0.9.1", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index a73f9ec..4b23919 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -2,7 +2,7 @@ import type { DB, PrimaryFieldType } from "bknd"; import * as AuthPermissions from "auth/auth-permissions"; import type { AuthStrategy } from "auth/authenticate/strategies/Strategy"; import type { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy"; -import { $console, secureRandomString, transformObject, pick } from "bknd/utils"; +import { $console, secureRandomString, transformObject, pickKeys } from "bknd/utils"; import type { Entity, EntityManager } from "data/entities"; import { em, entity, enumm, type FieldSchema } from "data/prototype"; import { Module } from "modules/Module"; @@ -113,6 +113,19 @@ export class AppAuth extends Module { return authConfigSchema; } + getGuardContextSchema() { + const userschema = this.getUsersEntity().toSchema() as any; + return { + type: "object", + properties: { + user: { + type: "object", + properties: pickKeys(userschema.properties, this.config.jwt.fields as any), + }, + }, + }; + } + get authenticator(): Authenticator { this.throwIfNotBuilt(); return this._authenticator!; diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 711099b..b7ececc 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -6,10 +6,8 @@ import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { sign, verify } from "hono/jwt"; import { type CookieOptions, serializeSigned } from "hono/utils/cookie"; import type { ServerEnv } from "modules/Controller"; -import { pick } from "lodash-es"; import { InvalidConditionsException } from "auth/errors"; -import { s, parse, secret, runtimeSupports, truncate, $console } from "bknd/utils"; -import { $object } from "modules/mcp"; +import { s, parse, secret, runtimeSupports, truncate, $console, pickKeys } from "bknd/utils"; import type { AuthStrategy } from "./strategies/Strategy"; type Input = any; // workaround @@ -229,7 +227,7 @@ export class Authenticator< // @todo: add jwt tests async jwt(_user: SafeUser | ProfileExchange): Promise { - const user = pick(_user, this.config.jwt.fields); + const user = pickKeys(_user, this.config.jwt.fields as any); const payload: JWTPayload = { ...user, @@ -255,7 +253,7 @@ export class Authenticator< } async safeAuthResponse(_user: User): Promise { - const user = pick(_user, this.config.jwt.fields) as SafeUser; + const user = pickKeys(_user, this.config.jwt.fields as any) as SafeUser; return { user, token: await this.jwt(user), diff --git a/app/src/auth/authorize/Guard.ts b/app/src/auth/authorize/Guard.ts index 85349a9..e66dba1 100644 --- a/app/src/auth/authorize/Guard.ts +++ b/app/src/auth/authorize/Guard.ts @@ -125,7 +125,7 @@ export class Guard { return this.config?.enabled === true; } - private collect(permission: Permission, c: GuardContext, context: any) { + private collect(permission: Permission, c: GuardContext | undefined, context: any) { const user = c && "get" in c ? c.get("auth")?.user : c; const ctx = { ...((context ?? {}) as any), @@ -181,15 +181,15 @@ export class Guard { } if (!role) { - $console.debug("guard: user has no role, denying"); throw new GuardPermissionsException(permission, undefined, "User has no role"); - } else if (role.implicit_allow === true) { - $console.debug(`guard: role "${role.name}" has implicit allow, allowing`); - return; } if (!rolePermission) { - $console.debug("guard: rolePermission not found, denying"); + if (role.implicit_allow === true) { + $console.debug(`guard: role "${role.name}" has implicit allow, allowing`); + return; + } + throw new GuardPermissionsException( permission, undefined, diff --git a/app/src/modules/ModuleHelper.ts b/app/src/modules/ModuleHelper.ts index 29c4172..c227ba1 100644 --- a/app/src/modules/ModuleHelper.ts +++ b/app/src/modules/ModuleHelper.ts @@ -137,6 +137,6 @@ export class ModuleHelper { } const user = await auth.authenticator?.resolveAuthFromRequest(c.raw as any); - this.ctx.guard.granted(permission, { user }, context as any); + this.ctx.guard.granted(permission, user as any, context as any); } } diff --git a/app/src/modules/SystemApi.ts b/app/src/modules/SystemApi.ts index dc2e5c6..ab26bae 100644 --- a/app/src/modules/SystemApi.ts +++ b/app/src/modules/SystemApi.ts @@ -1,6 +1,7 @@ import type { ConfigUpdateResponse } from "modules/server/SystemController"; import { ModuleApi } from "./ModuleApi"; import type { ModuleConfigs, ModuleKey, ModuleSchemas } from "./ModuleManager"; +import type { TPermission } from "auth/authorize/Permission"; export type ApiSchemaResponse = { version: number; @@ -54,4 +55,8 @@ export class SystemApi extends ModuleApi { removeConfig(module: Module, path: string) { return this.delete(["config", "remove", module, path]); } + + permissions() { + return this.get<{ permissions: TPermission[]; context: object }>("permissions"); + } } diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 45787bf..1190d55 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -69,10 +69,13 @@ export class SystemController extends Controller { if (!config.mcp.enabled) { return; } + const { permission } = this.middlewares; this.registerMcp(); - app.server.use( + app.server.all( + config.mcp.path, + permission(SystemPermissions.mcp, {}), mcpMiddleware({ setup: async () => { if (!this._mcpServer) { @@ -110,7 +113,6 @@ export class SystemController extends Controller { explainEndpoint: true, }, endpoint: { - path: config.mcp.path as any, // @ts-ignore _init: isNode() ? { duplex: "half" } : {}, }, @@ -415,7 +417,6 @@ export class SystemController extends Controller { schema, config: config ? this.app.toJSON(secrets) : undefined, permissions: this.app.modules.ctx().guard.getPermissions(), - //permissions: this.app.modules.ctx().guard.getPermissionNames(), }); }, ); @@ -428,7 +429,7 @@ export class SystemController extends Controller { }), (c) => { const permissions = this.app.modules.ctx().guard.getPermissions(); - return c.json({ permissions }); + return c.json({ permissions, context: this.app.module.auth.getGuardContextSchema() }); }, ); diff --git a/app/src/ui/client/api/use-api.ts b/app/src/ui/client/api/use-api.ts index 6b6d546..573b990 100644 --- a/app/src/ui/client/api/use-api.ts +++ b/app/src/ui/client/api/use-api.ts @@ -1,6 +1,6 @@ import type { Api } from "Api"; import { FetchPromise, type ModuleApi, type ResponseObject } from "modules/ModuleApi"; -import useSWR, { type SWRConfiguration, useSWRConfig } from "swr"; +import useSWR, { type SWRConfiguration, useSWRConfig, type Middleware, type SWRHook } from "swr"; import useSWRInfinite from "swr/infinite"; import { useApi } from "ui/client"; import { useState } from "react"; @@ -89,3 +89,25 @@ export const useInvalidate = (options?: { exact?: boolean }) => { return mutate((k) => typeof k === "string" && k.startsWith(key)); }; }; + +const mountOnceCache = new Map(); + +/** + * Simple middleware to only load on first mount. + */ +export const mountOnce: Middleware = (useSWRNext: SWRHook) => (key, fetcher, config) => { + if (typeof key === "string") { + if (mountOnceCache.has(key)) { + return useSWRNext(key, fetcher, { + ...config, + revalidateOnMount: false, + }); + } + const swr = useSWRNext(key, fetcher, config); + if (swr.data) { + mountOnceCache.set(key, true); + } + return swr; + } + return useSWRNext(key, fetcher, config); +}; diff --git a/app/src/ui/components/buttons/Button.tsx b/app/src/ui/components/buttons/Button.tsx index b80f006..79ee3cc 100644 --- a/app/src/ui/components/buttons/Button.tsx +++ b/app/src/ui/components/buttons/Button.tsx @@ -5,13 +5,15 @@ import { twMerge } from "tailwind-merge"; import { Link } from "ui/components/wouter/Link"; const sizes = { + smaller: "px-1.5 py-1 rounded-md gap-1 !text-xs", small: "px-2 py-1.5 rounded-md gap-1 text-sm", default: "px-3 py-2.5 rounded-md gap-1.5", large: "px-4 py-3 rounded-md gap-2.5 text-lg", }; const iconSizes = { - small: 12, + smaller: 12, + small: 14, default: 16, large: 20, }; diff --git a/app/src/ui/components/code/CodePreview.tsx b/app/src/ui/components/code/CodePreview.tsx new file mode 100644 index 0000000..9796e93 --- /dev/null +++ b/app/src/ui/components/code/CodePreview.tsx @@ -0,0 +1,73 @@ +import { useEffect, useState } from "react"; +import { useTheme } from "ui/client/use-theme"; +import { cn } from "ui/lib/utils"; + +export type CodePreviewProps = { + code: string; + className?: string; + lang?: string; + theme?: string; + enabled?: boolean; +}; + +export const CodePreview = ({ + code, + className, + lang = "typescript", + theme: _theme, + enabled = true, +}: CodePreviewProps) => { + const [highlightedHtml, setHighlightedHtml] = useState(null); + const $theme = useTheme(); + const theme = (_theme ?? $theme.theme === "dark") ? "github-dark" : "github-light"; + + useEffect(() => { + if (!enabled) return; + + let cancelled = false; + setHighlightedHtml(null); + + async function highlightCode() { + try { + // Dynamically import Shiki from CDN + // @ts-expect-error - Dynamic CDN import + const { codeToHtml } = await import("https://esm.sh/shiki@3.13.0"); + + if (cancelled) return; + + const html = await codeToHtml(code, { + lang, + theme, + structure: "inline", + }); + + if (cancelled) return; + + setHighlightedHtml(html); + } catch (error) { + console.error("Failed to load Shiki:", error); + // Fallback to plain text if Shiki fails to load + if (!cancelled) { + setHighlightedHtml(code); + } + } + } + + highlightCode(); + + return () => { + cancelled = true; + }; + }, [code, enabled]); + + if (!highlightedHtml) { + return
{code}
; + } + + return ( +
+   );
+};
diff --git a/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx b/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx
index af4607c..334dfe5 100644
--- a/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx
+++ b/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx
@@ -1,4 +1,4 @@
-import { IconBug } from "@tabler/icons-react";
+import { IconBug, IconInfoCircle } from "@tabler/icons-react";
 import type { JsonSchema } from "json-schema-library";
 import { Children, type ReactElement, type ReactNode, cloneElement, isValidElement } from "react";
 import { IconButton } from "ui/components/buttons/IconButton";
@@ -12,6 +12,7 @@ import {
 import { Popover } from "ui/components/overlay/Popover";
 import { getLabel } from "./utils";
 import { twMerge } from "tailwind-merge";
+import { Tooltip } from "@mantine/core";
 
 export type FieldwrapperProps = {
    name: string;
@@ -24,7 +25,7 @@ export type FieldwrapperProps = {
    children: ReactElement | ReactNode;
    errorPlacement?: "top" | "bottom";
    description?: string;
-   descriptionPlacement?: "top" | "bottom";
+   descriptionPlacement?: "top" | "bottom" | "label";
    fieldId?: string;
    className?: string;
 };
@@ -53,11 +54,17 @@ export function FieldWrapper({
       {errors.map((e) => e.message).join(", ")}
    );
 
-   const Description = description && (
-      
-         {description}
-      
-   );
+   const Description = description ? (
+      ["top", "bottom"].includes(descriptionPlacement) ? (
+         
+            {description}
+         
+      ) : (
+         
+            
+         
+      )
+   ) : null;
 
    return (
       
                {label} {required && *}
+               {descriptionPlacement === "label" && Description}
             
          )}
          {descriptionPlacement === "top" && Description}
diff --git a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx
index 54a1bfd..9637ed1 100644
--- a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx
+++ b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx
@@ -7,34 +7,36 @@ import { useNavigate } from "ui/lib/routes";
 import { isDebug } from "core/env";
 import { Dropdown } from "ui/components/overlay/Dropdown";
 import { IconButton } from "ui/components/buttons/IconButton";
-import { TbAdjustments, TbDots, TbFilter, TbTrash } from "react-icons/tb";
+import { TbAdjustments, TbDots, TbFilter, TbTrash, TbInfoCircle, TbCodeDots } from "react-icons/tb";
 import { Button } from "ui/components/buttons/Button";
 import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
 import { routes } from "ui/lib/routes";
 import * as AppShell from "ui/layouts/AppShell/AppShell";
 import * as Formy from "ui/components/form/Formy";
-
-import { ucFirst, type s } from "bknd/utils";
+import { ucFirst, s, transformObject, isObject } from "bknd/utils";
 import type { ModuleSchemas } from "bknd";
 import {
-   ArrayField,
    CustomField,
    Field,
    FieldWrapper,
    Form,
    FormContextOverride,
    FormDebug,
-   ObjectField,
+   ObjectJsonField,
    Subscribe,
    useDerivedFieldContext,
    useFormContext,
+   useFormError,
    useFormValue,
 } from "ui/components/form/json-schema-form";
 import type { TPermission } from "auth/authorize/Permission";
 import type { RoleSchema } from "auth/authorize/Role";
-import { Indicator, SegmentedControl, Tooltip } from "@mantine/core";
+import { SegmentedControl, Tooltip } from "@mantine/core";
+import { Popover } from "ui/components/overlay/Popover";
 import { cn } from "ui/lib/utils";
-import type { PolicySchema } from "auth/authorize/Policy";
+import { JsonViewer } from "ui/components/code/JsonViewer";
+import { mountOnce, useApiQuery } from "ui/client";
+import { CodePreview } from "ui/components/code/CodePreview";
 
 export function AuthRolesEdit(props) {
    useBrowserTitle(["Auth", "Roles", props.params.role]);
@@ -67,7 +69,7 @@ const formConfig = {
    },
 };
 
-function AuthRolesEditInternal({ params }) {
+function AuthRolesEditInternal({ params }: { params: { role: string } }) {
    const [navigate] = useNavigate();
    const { config, schema: authSchema, actions } = useBkndAuth();
    const roleName = params.role;
@@ -225,11 +227,10 @@ const Permission = ({ permission, index }: { permission: TPermission; index?: nu
       if (!Array.isArray(v)) return undefined;
       return v.find((v) => v && v.permission === permission.name);
    });
-   const { setValue, deleteValue } = useFormContext();
+   const { setValue } = useFormContext();
    const [open, setOpen] = useState(false);
    const data = value as PermissionData | undefined;
    const policiesCount = data?.policies?.length ?? 0;
-   const hasContext = !!permission.context;
 
    async function handleSwitch() {
       if (data) {
@@ -270,9 +271,9 @@ const Permission = ({ permission, index }: { permission: TPermission; index?: nu
                       setOpen((o) => !o)}
                      />
                   
@@ -282,14 +283,6 @@ const Permission = ({ permission, index }: { permission: TPermission; index?: nu
             {open && (
                
- {/* */}
)} @@ -337,19 +330,68 @@ const Policies = ({ path, permission }: { path: string; permission: TPermission ); }; +const mergeSchemas = (...schemas: object[]) => { + const schema = s.allOf(schemas.filter(Boolean).map(s.fromSchema)); + return s.toTypes(schema, "Context"); +}; + +function replaceEntitiesEnum(schema: Record, entities: string[]) { + if (!isObject(schema) || !Array.isArray(entities) || entities.length === 0) return schema; + return transformObject(schema, (sub, name) => { + if (name === "properties") { + return transformObject(sub as Record, (propConfig, propKey) => { + if (propKey === "entity" && propConfig.type === "string") { + return { + ...propConfig, + enum: entities, + }; + } + return propConfig; + }); + } + return sub; + }); +} + const Policy = ({ permission, }: { permission: TPermission; }) => { const { value } = useFormValue(""); + const $bknd = useBknd(); + const $permissions = useApiQuery((api) => api.system.permissions(), { + use: [mountOnce], + }); + const entities = Object.keys($bknd.config.data.entities ?? {}); + const ctx = $permissions.data + ? mergeSchemas( + $permissions.data.context, + replaceEntitiesEnum(permission.context ?? null, entities), + ) + : undefined; + return (
- + + + + + {({ value, setValue }) => ( - + {value?.effect === "filter" && ( - + + + )}
); }; + +const CustomFieldWrapper = ({ children, name, label, description, schema }: any) => { + const errors = useFormError(name, { strict: true }); + const Errors = errors.length > 0 && ( + {errors.map((e) => e.message).join(", ")} + ); + + return ( + + +
+ {label} + {description && ( + + + + )} +
+ {schema && ( +
+ + typeof schema === "object" ? ( + + ) : ( + + ) + } + > + + +
+ )} +
+ {children} + {Errors} +
+ ); +}; diff --git a/app/src/ui/routes/tools/mcp/mcp.tsx b/app/src/ui/routes/tools/mcp/mcp.tsx index c06668b..ccde4c6 100644 --- a/app/src/ui/routes/tools/mcp/mcp.tsx +++ b/app/src/ui/routes/tools/mcp/mcp.tsx @@ -39,7 +39,7 @@ export default function ToolsMcp() {
- + {window.location.origin + mcpPath}
diff --git a/app/src/ui/routes/tools/mcp/tools.tsx b/app/src/ui/routes/tools/mcp/tools.tsx index d439fc1..6c475dd 100644 --- a/app/src/ui/routes/tools/mcp/tools.tsx +++ b/app/src/ui/routes/tools/mcp/tools.tsx @@ -12,6 +12,7 @@ import * as Formy from "ui/components/form/Formy"; import { appShellStore } from "ui/store"; import { Icon } from "ui/components/display/Icon"; import { useMcpClient } from "./hooks/use-mcp-client"; +import { Tooltip } from "@mantine/core"; export function Sidebar({ open, toggle }) { const client = useMcpClient(); @@ -48,7 +49,11 @@ export function Sidebar({ open, toggle }) { toggle={toggle} renderHeaderRight={() => (
- {error && } + {error && ( + + + + )} {tools.length} diff --git a/bun.lock b/bun.lock index ab63856..fa63422 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", - "jsonv-ts": "0.8.6", + "jsonv-ts": "0.9.1", "kysely": "0.27.6", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", @@ -1243,7 +1243,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], - "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -2529,7 +2529,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.8.6", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-z5jJ017LFOvAFFVodAIiCY024yW72RWc/K0Sct+OtuiLN+lKy+g0pI0jaz5JmuXaMIePc6HyopeeYHi8ffbYhw=="], + "jsonv-ts": ["jsonv-ts@0.9.1", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-sQZn7kdSMK9m3hLWvTLyNk2zCUmte2lVWIcK02633EwMosk/VAdRgpMyfMDMV6/ZzSMI0/SwevkUbkxdGQrWtg=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], @@ -4095,7 +4095,7 @@ "@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "@types/bun/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], "@typescript-eslint/experimental-utils/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],