From 7e9cb50152dd87808307a21713960022c11369f1 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 22 Jun 2026 12:51:47 +0800 Subject: [PATCH 01/35] feat(web): hide snippets (#37729) --- .../components/apps/__tests__/list.spec.tsx | 8 +- .../apps/app-list-header-filters.tsx | 7 - .../__tests__/selection-contextmenu.spec.tsx | 154 +----------------- .../block-selector/__tests__/main.spec.tsx | 1 + .../workflow/block-selector/main.tsx | 2 +- .../workflow/selection-contextmenu.tsx | 41 +---- 6 files changed, 8 insertions(+), 205 deletions(-) diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index f5ac48c3a46..47f328d50d6 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -421,18 +421,16 @@ describe('List', () => { expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument() }) - it('should render sort filter before search and the snippets link', () => { + it('should render sort filter before search and hide the snippets link', () => { renderList() const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' }) const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }) - const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' }) const createButton = screen.getByRole('button', { name: 'common.operation.create' }) - expect(snippetsLink).toHaveAttribute('href', '/snippets') expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() - expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() - expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(searchInput.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(screen.queryByRole('link', { name: 'app.studio.viewSnippets' })).not.toBeInTheDocument() }) it('should render app cards when apps exist', () => { diff --git a/web/app/components/apps/app-list-header-filters.tsx b/web/app/components/apps/app-list-header-filters.tsx index df32f57d7ba..ceccb7575f6 100644 --- a/web/app/components/apps/app-list-header-filters.tsx +++ b/web/app/components/apps/app-list-header-filters.tsx @@ -8,7 +8,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { TagFilter } from '@/features/tag-management/components/tag-filter' -import Link from '@/next/link' import { AppSortFilter } from './app-sort-filter' import { AppTypeFilter } from './app-type-filter' import CreatorsFilter from './creators-filter' @@ -71,12 +70,6 @@ export function AppListHeaderFilters({ />
- - {t('studio.viewSnippets', { ns: 'app' })} - {showCreateButton && ( ({ - value: ['snippets.create_and_modify'] as string[], -})) - -vi.mock('@/context/app-context', () => ({ - useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => T): T => selector({ - workspacePermissionKeys: mockWorkspacePermissionKeys.value, - }), -})) - -vi.mock('@/app/components/snippets/hooks/use-create-snippet', async () => { - const React = await vi.importActual('react') - - return { - useCreateSnippet: () => { - const [isOpen, setIsOpen] = React.useState(false) - - return { - createSnippetMutation: { isPending: false }, - handleCloseCreateSnippetDialog: () => setIsOpen(false), - handleCreateSnippet: mockHandleCreateSnippet, - handleOpenCreateSnippetDialog: () => setIsOpen(true), - isCreateSnippetDialogOpen: isOpen, - isCreatingSnippet: false, - } - }, - } -}) - -vi.mock('@/app/components/snippets/create-snippet-dialog', () => ({ - default: (props: { - isOpen: boolean - selectedGraph?: { nodes: Node[], edges: Edge[], viewport: { x: number, y: number, zoom: number } } - inputFields?: Array<{ variable: string }> - }) => { - mockCreateSnippetDialogRender(props) - - return props.isOpen ?
: null - }, -})) vi.mock('../hooks', async () => { const actual = await vi.importActual('../hooks') @@ -142,9 +98,6 @@ describe('SelectionContextmenu', () => { mockHandleNodesCopy.mockReset() mockHandleNodesDuplicate.mockReset() mockHandleNodesDelete.mockReset() - mockHandleCreateSnippet.mockReset() - mockCreateSnippetDialogRender.mockReset() - mockWorkspacePermissionKeys.value = ['snippets.create_and_modify'] }) it('should not render when selection context menu target is absent', () => { @@ -203,41 +156,7 @@ describe('SelectionContextmenu', () => { expect(store.getState().contextMenuTarget).toBeUndefined() }) - it('should open create snippet dialog with selected graph from the top menu item', async () => { - const nodes = [ - createNode({ id: 'n1', selected: true, width: 80, height: 40 }), - createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), - createNode({ id: 'n3', selected: false, position: { x: 260, y: 0 }, width: 80, height: 40 }), - ] - const edges = [ - createEdge({ source: 'n1', target: 'n2' }), - createEdge({ source: 'n2', target: 'n3' }), - ] - const { store } = renderSelectionMenu({ nodes, edges }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - fireEvent.click(await screen.findByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })) - - expect(screen.getByTestId('create-snippet-dialog')).toBeInTheDocument() - expect(store.getState().contextMenuTarget).toBeUndefined() - - const dialogProps = mockCreateSnippetDialogRender.mock.calls.at(-1)?.[0] - expect(dialogProps.selectedGraph.nodes.map((node: Node) => node.id)).toEqual(['n1', 'n2']) - expect(dialogProps.selectedGraph.nodes.every((node: Node) => node.selected === false)).toBe(true) - expect(dialogProps.selectedGraph.edges).toHaveLength(1) - expect(dialogProps.selectedGraph.viewport).toEqual({ x: 490, y: 380, zoom: 1 }) - expect(dialogProps.selectedGraph.edges[0]).toEqual(expect.objectContaining({ - source: 'n1', - target: 'n2', - selected: false, - })) - }) - - it('should hide create snippet action without snippets create-and-modify permission', async () => { - mockWorkspacePermissionKeys.value = [] + it('should hide create snippet action for selected nodes', async () => { const nodes = [ createNode({ id: 'n1', selected: true, width: 80, height: 40 }), createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), @@ -252,76 +171,7 @@ describe('SelectionContextmenu', () => { expect(screen.getByRole('menuitem', { name: /common.copy/ })).toBeInTheDocument() }) expect(screen.queryByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })).not.toBeInTheDocument() - }) - - it('should add input fields for variable references outside of the selected graph', async () => { - const nodes = [ - createNode({ - id: 'n1', - selected: true, - width: 80, - height: 40, - data: { - prompt_template: 'Use {{#source-node.topic#}} and {{#n2.answer#}}', - query_variable_selector: ['source-node', 'topic'], - env_reference: '{{#env.API_KEY#}}', - }, - }), - createNode({ - id: 'n2', - selected: true, - position: { x: 140, y: 0 }, - width: 80, - height: 40, - }), - ] - const { store } = renderSelectionMenu({ nodes }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - fireEvent.click(await screen.findByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })) - - const dialogProps = mockCreateSnippetDialogRender.mock.calls.at(-1)?.[0] - expect(dialogProps.inputFields).toEqual([ - { - label: 'topic', - variable: 'topic', - type: PipelineInputVarType.textInput, - required: true, - }, - { - label: 'API_KEY', - variable: 'API_KEY', - type: PipelineInputVarType.textInput, - required: true, - }, - ]) - expect(dialogProps.selectedGraph.nodes[0].data.prompt_template).toBe('Use {{#start.topic#}} and {{#n2.answer#}}') - expect(dialogProps.selectedGraph.nodes[0].data.query_variable_selector).toEqual(['start', 'topic']) - expect(dialogProps.selectedGraph.nodes[0].data.env_reference).toBe('{{#start.API_KEY#}}') - }) - - it.each([ - BlockEnum.Answer, - BlockEnum.End, - BlockEnum.Start, - ])('should hide create snippet when selection contains %s node', async (nodeType) => { - const nodes = [ - createNode({ id: 'n1', selected: true, width: 80, height: 40, data: { type: nodeType } }), - createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), - ] - const { store } = renderSelectionMenu({ nodes }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - await waitFor(() => { - expect(screen.getByRole('menuitem', { name: /common.copy/ })).toBeInTheDocument() - }) - expect(screen.queryByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })).not.toBeInTheDocument() + expect(screen.queryByTestId('create-snippet-dialog')).not.toBeInTheDocument() }) it('should stay hidden when only one node is selected', async () => { diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx index aaf26eedc56..fb56beda545 100644 --- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx @@ -106,6 +106,7 @@ describe('NodeSelector', () => { await user.click(trigger) const searchInput = screen.getByPlaceholderText('workflow.tabs.searchBlock') + expect(screen.queryByText('workflow.tabs.snippets')).not.toBeInTheDocument() expect(screen.getByText('LLM')).toBeInTheDocument() expect(screen.getByText('End')).toBeInTheDocument() diff --git a/web/app/components/workflow/block-selector/main.tsx b/web/app/components/workflow/block-selector/main.tsx index 595426a262b..6678f081fe9 100644 --- a/web/app/components/workflow/block-selector/main.tsx +++ b/web/app/components/workflow/block-selector/main.tsx @@ -132,7 +132,7 @@ function NodeSelector({ const defaultAllowUserInputSelection = !hasUserInputNode && !hasTriggerNode const canSelectUserInput = allowUserInputSelection ?? defaultAllowUserInputSelection const disableStartTab = flowType === FlowType.snippet - const disableSnippetsTab = flowType === FlowType.snippet + const disableSnippetsTab = true const { activeTab, resetActiveTab, diff --git a/web/app/components/workflow/selection-contextmenu.tsx b/web/app/components/workflow/selection-contextmenu.tsx index dffbd31a5e1..0478b0e12f5 100644 --- a/web/app/components/workflow/selection-contextmenu.tsx +++ b/web/app/components/workflow/selection-contextmenu.tsx @@ -12,15 +12,11 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useReactFlowStore } from 'reactflow' -import { useCreateSnippetFromSelection } from '@/app/components/snippets/hooks/use-create-snippet-from-selection' -import { canCreateAndModifySnippets } from '@/app/components/snippets/utils/permission' import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks' import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history' import { ShortcutKbd } from './shortcuts/shortcut-kbd' import { useStore, useWorkflowStore } from './store' -import { BlockEnum } from './types' const AlignType = { Bottom: 'bottom', @@ -75,14 +71,6 @@ const menuSections: MenuSection[] = [ }, ] -const unsupportedSnippetNodeTypes = new Set([ - BlockEnum.Answer, - BlockEnum.End, - BlockEnum.Start, - BlockEnum.HumanInput, - BlockEnum.KnowledgeRetrieval, -]) - const getAlignableNodes = (nodes: Node[], selectedNodes: Node[]) => { const selectedNodeIds = new Set(selectedNodes.map(node => node.id)) const childNodeIds = new Set() @@ -235,7 +223,6 @@ export function SelectionContextmenu({ }) { const { t } = useTranslation() const { getNodesReadOnly } = useNodesReadOnly() - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) const { handleNodesCopy, handleNodesDelete, handleNodesDuplicate } = useNodesInteractions() const isSelectionContextMenu = useStore(s => s.contextMenuTarget?.type === 'selection') @@ -247,20 +234,8 @@ export function SelectionContextmenu({ const selectedNodes = useReactFlowStore(state => state.getNodes().filter(node => node.selected), ) - const edges = useReactFlowStore(state => state.edges) const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { saveStateToHistory } = useWorkflowHistory() - const { - createSnippetDialog, - handleOpenCreateSnippet, - isCreateSnippetDialogOpen, - } = useCreateSnippetFromSelection({ - edges, - selectedNodes, - onClose, - }) - const canCreateSnippet = canCreateAndModifySnippets(workspacePermissionKeys) - && selectedNodes.every(node => !unsupportedSnippetNodeTypes.has(node.data.type)) const handleCopyNodes = useCallback(() => { handleNodesCopy() @@ -370,24 +345,11 @@ export function SelectionContextmenu({ }, [collaborativeWorkflow, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, onClose]) if (!isSelectionContextMenu || selectedNodes.length <= 1) - return isCreateSnippetDialogOpen ? createSnippetDialog : null + return null return ( <> - {canCreateSnippet && ( - <> - - - {t('snippet.createDialogTitle', { defaultValue: 'Create Snippet', ns: 'workflow' })} - - - - - )} ))} - {createSnippetDialog} ) } From 4f61353dc2035ce6bd755120ef44ce8f85464958 Mon Sep 17 00:00:00 2001 From: L1nSn0w Date: Mon, 22 Jun 2026 12:53:58 +0800 Subject: [PATCH 02/35] fix(cli): align difyctl help text with actual flags and error envelope (#37728) --- cli/src/help/contract.test.ts | 26 ++++++++++++++++++++++++++ cli/src/help/contract.ts | 2 +- cli/src/help/topics.test.ts | 4 ++-- cli/src/help/topics.ts | 4 ++-- 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 cli/src/help/contract.test.ts diff --git a/cli/src/help/contract.test.ts b/cli/src/help/contract.test.ts new file mode 100644 index 00000000000..272e1ff8d88 --- /dev/null +++ b/cli/src/help/contract.test.ts @@ -0,0 +1,26 @@ +import type { ErrorBody } from '@dify/contracts/api/openapi/types.gen' +import { describe, expect, it } from 'vitest' +import { HttpClientError, newError } from '@/errors/base' +import { ErrorCode } from '@/errors/codes' +import { CONTRACT } from './contract' + +describe('errorEnvelope contract', () => { + // Guard against the documented shape drifting from the real envelope: build an + // error with every optional field populated and assert each JSON key it emits + // is named in the contract string. Adding/removing an envelope field without + // updating the doc fails here. + it('documents every key the real JSON envelope can emit', () => { + const server: ErrorBody = { code: 'app_unavailable', message: 'gone', status: 404 } + const err = HttpClientError.from(newError(ErrorCode.Server4xxOther, 'boom')) + .withHint('do x') + .withRequest('GET', 'https://api.dify.ai/v1/me') + .withHttpStatus(404) + .withRawResponse('{"x":1}') + .withServerError(server) + + const env = err.toEnvelope() + + for (const key of Object.keys(env.error)) + expect(CONTRACT.errorEnvelope.shape).toContain(`"${key}"`) + }) +}) diff --git a/cli/src/help/contract.ts b/cli/src/help/contract.ts index 1d68da8b268..d4be7209280 100644 --- a/cli/src/help/contract.ts +++ b/cli/src/help/contract.ts @@ -38,7 +38,7 @@ export const CONTRACT: Contract = { description: 'On failure the error goes to stderr. Under -o json/yaml it is a structured envelope; otherwise a human line.', shape: - '{ "error": { "code": string, "message": string, "hint"?: string, "http_status"?: number, "request"?: string } }', + '{ "error": { "code": string, "message": string, "hint"?: string, "http_status"?: number, "method"?: string, "url"?: string, "raw_response"?: string, "server"?: object } }', }, hitl: { description: diff --git a/cli/src/help/topics.test.ts b/cli/src/help/topics.test.ts index f5b2d1843e9..5a326895035 100644 --- a/cli/src/help/topics.test.ts +++ b/cli/src/help/topics.test.ts @@ -50,10 +50,10 @@ describe('agent topic', () => { }) describe('external topic', () => { - it('mentions external bearer prefix and login flag', () => { + it('mentions external bearer prefix and DIFY_TOKEN onboarding', () => { const out = render('external') expect(out).toContain('dfoe_') - expect(out).toContain('--external') + expect(out).toContain('export DIFY_TOKEN') expect(out).toContain('DIFY_TOKEN') }) diff --git a/cli/src/help/topics.ts b/cli/src/help/topics.ts index 3da5c0f8dfc..6ccfd1044ee 100644 --- a/cli/src/help/topics.ts +++ b/cli/src/help/topics.ts @@ -37,8 +37,8 @@ const EXTERNAL_HELP_TEXT = `difyctl: external-SSO bearer onboarding smaller dataset: 1. Acquire a token through your SSO provider (out of band). - 2. Hand it to the CLI: - difyctl auth login --external --token "$DIFY_TOKEN" + 2. Hand it to the CLI via the DIFY_TOKEN environment variable: + export DIFY_TOKEN="" 3. List apps your subject is permitted to invoke: difyctl get app From 34c1bf1062a47a60390addf61d2f1e709d60772b Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 22 Jun 2026 13:22:10 +0800 Subject: [PATCH 03/35] feat(agent): add skill inspect API (#37726) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../console/app/agent_drive_inspector.py | 175 ++++++++- ...c2a_agent_drive_skill_metadata_refactor.py | 39 ++ api/models/agent.py | 9 +- api/openapi/markdown/console-openapi.md | 126 ++++++ .../agent/skill_standardize_service.py | 8 +- api/services/agent_drive_service.py | 362 +++++++++++++++++- .../console/app/test_agent_drive_inspector.py | 122 ++++++ .../agent/test_skill_standardize_service.py | 4 + .../services/test_agent_drive_service.py | 102 +++++ .../generated/api/console/agent/orpc.gen.ts | 100 +++-- .../generated/api/console/agent/types.gen.ts | 86 +++++ .../generated/api/console/agent/zod.gen.ts | 86 +++++ .../generated/api/console/apps/orpc.gen.ts | 360 +++++++++-------- .../generated/api/console/apps/types.gen.ts | 91 +++++ .../generated/api/console/apps/zod.gen.ts | 95 +++++ 15 files changed, 1579 insertions(+), 186 deletions(-) create mode 100644 api/migrations/versions/2026_06_18_2300-b2515f9d4c2a_agent_drive_skill_metadata_refactor.py diff --git a/api/controllers/console/app/agent_drive_inspector.py b/api/controllers/console/app/agent_drive_inspector.py index b8d1d487808..bd639955d9c 100644 --- a/api/controllers/console/app/agent_drive_inspector.py +++ b/api/controllers/console/app/agent_drive_inspector.py @@ -10,8 +10,12 @@ backend — drive data lives in the API's own DB/storage, served straight from from __future__ import annotations +import json +from collections.abc import Mapping +from typing import Any from uuid import UUID +from flask import Response from flask_restx import Resource from pydantic import BaseModel, Field @@ -49,6 +53,10 @@ class AgentDriveFileByAgentQuery(BaseModel): key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md") +class AgentDriveSkillInspectQuery(BaseModel): + node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)") + + class AgentDriveItemResponse(ResponseModel): key: str size: int | None = None @@ -56,12 +64,63 @@ class AgentDriveItemResponse(ResponseModel): hash: str | None = None file_kind: str created_at: int | None = None + is_skill: bool | None = None + skill_metadata: str | None = None class AgentDriveListResponse(ResponseModel): items: list[AgentDriveItemResponse] = Field(default_factory=list) +class AgentDriveSkillItemResponse(ResponseModel): + path: str + skill_md_key: str + archive_key: str | None = None + name: str + description: str + size: int | None = None + mime_type: str | None = None + hash: str | None = None + created_at: int | None = None + + +class AgentDriveSkillListResponse(ResponseModel): + items: list[AgentDriveSkillItemResponse] = Field(default_factory=list) + + +class AgentDriveSkillFileResponse(ResponseModel): + path: str + name: str + type: str + drive_key: str | None = None + available_in_drive: bool + + +class AgentDriveSkillMarkdownResponse(ResponseModel): + key: str + size: int | None = None + truncated: bool + binary: bool + text: str | None = None + + +class AgentDriveSkillInspectResponse(ResponseModel): + path: str + skill_md_key: str + archive_key: str | None = None + name: str + description: str + size: int | None = None + mime_type: str | None = None + hash: str | None = None + created_at: int | None = None + source: str + files: list[AgentDriveSkillFileResponse] = Field(default_factory=list) + file_tree: list[dict[str, Any]] = Field(default_factory=list) + skill_md: AgentDriveSkillMarkdownResponse + warnings: list[str] = Field(default_factory=list) + + class AgentDrivePreviewResponse(ResponseModel): key: str size: int | None = None @@ -75,7 +134,12 @@ class AgentDriveDownloadResponse(ResponseModel): register_response_schema_models( - console_ns, AgentDriveListResponse, AgentDrivePreviewResponse, AgentDriveDownloadResponse + console_ns, + AgentDriveDownloadResponse, + AgentDriveListResponse, + AgentDrivePreviewResponse, + AgentDriveSkillInspectResponse, + AgentDriveSkillListResponse, ) @@ -96,6 +160,13 @@ def _handle(exc: AgentDriveError) -> tuple[dict[str, object], int]: return {"code": exc.code, "message": exc.message}, exc.status_code +def _json_response(data: Mapping[str, Any]): + return Response( + response=json.dumps(data, ensure_ascii=False, separators=(",", ":")), + content_type="application/json; charset=utf-8", + ) + + _WORKFLOW_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT] @@ -119,6 +190,49 @@ class AgentDriveListByAgentApi(Resource): return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]} +@console_ns.route("/agent//drive/skills") +class AgentDriveSkillListByAgentApi(Resource): + @console_ns.doc("list_agent_drive_skills_by_agent") + @console_ns.doc(description="List drive-backed skills for an Agent App") + @console_ns.doc(params={"agent_id": "Agent ID"}) + @console_ns.response(200, "Drive skills", console_ns.models[AgentDriveSkillListResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @with_current_tenant_id + def get(self, tenant_id: str, agent_id: UUID): + resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + try: + items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id)) + except AgentDriveError as exc: + return _handle(exc) + return {"items": items} + + +@console_ns.route("/agent//drive/skills//inspect") +class AgentDriveSkillInspectByAgentApi(Resource): + @console_ns.doc("inspect_agent_drive_skill_by_agent") + @console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI") + @console_ns.doc(params={"agent_id": "Agent ID", "skill_path": "Skill path/slug, e.g. tender-analyzer"}) + @console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @with_current_tenant_id + def get(self, tenant_id: str, agent_id: UUID, skill_path: str): + resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + try: + return _json_response( + AgentDriveService().inspect_skill( + tenant_id=tenant_id, + agent_id=str(agent_id), + skill_path=skill_path, + ) + ) + except AgentDriveError as exc: + return _handle(exc) + + @console_ns.route("/agent//drive/files/preview") class AgentDrivePreviewByAgentApi(Resource): @console_ns.doc("preview_agent_drive_file_by_agent") @@ -182,6 +296,61 @@ class AgentDriveListApi(Resource): return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]} +@console_ns.route("/apps//agent/drive/skills") +class AgentDriveSkillListApi(Resource): + @console_ns.doc("list_agent_drive_skills") + @console_ns.doc(description="List drive-backed skills for the bound agent") + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveListQuery)}) + @console_ns.response(200, "Drive skills", console_ns.models[AgentDriveSkillListResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=_WORKFLOW_APP_MODES) + def get(self, app_model: App): + query = query_params_from_request(AgentDriveListQuery) + agent_id = _resolve_agent_id(app_model, query.node_id) + if not agent_id: + return _agent_not_bound() + try: + items = AgentDriveService().list_skills(tenant_id=app_model.tenant_id, agent_id=agent_id) + except AgentDriveError as exc: + return _handle(exc) + return {"items": items} + + +@console_ns.route("/apps//agent/drive/skills//inspect") +class AgentDriveSkillInspectApi(Resource): + @console_ns.doc("inspect_agent_drive_skill") + @console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI") + @console_ns.doc( + params={ + "app_id": "Application ID", + "skill_path": "Skill path/slug, e.g. tender-analyzer", + **query_params_from_model(AgentDriveSkillInspectQuery), + } + ) + @console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=_WORKFLOW_APP_MODES) + def get(self, app_model: App, skill_path: str): + query = query_params_from_request(AgentDriveSkillInspectQuery) + agent_id = _resolve_agent_id(app_model, query.node_id) + if not agent_id: + return _agent_not_bound() + try: + return _json_response( + AgentDriveService().inspect_skill( + tenant_id=app_model.tenant_id, + agent_id=agent_id, + skill_path=skill_path, + ) + ) + except AgentDriveError as exc: + return _handle(exc) + + @console_ns.route("/apps//agent/drive/files/preview") class AgentDrivePreviewApi(Resource): @console_ns.doc("preview_agent_drive_file") @@ -232,4 +401,8 @@ __all__ = [ "AgentDriveListByAgentApi", "AgentDrivePreviewApi", "AgentDrivePreviewByAgentApi", + "AgentDriveSkillInspectApi", + "AgentDriveSkillInspectByAgentApi", + "AgentDriveSkillListApi", + "AgentDriveSkillListByAgentApi", ] diff --git a/api/migrations/versions/2026_06_18_2300-b2515f9d4c2a_agent_drive_skill_metadata_refactor.py b/api/migrations/versions/2026_06_18_2300-b2515f9d4c2a_agent_drive_skill_metadata_refactor.py new file mode 100644 index 00000000000..9dc85d2a89b --- /dev/null +++ b/api/migrations/versions/2026_06_18_2300-b2515f9d4c2a_agent_drive_skill_metadata_refactor.py @@ -0,0 +1,39 @@ +"""agent drive skill metadata refactor + +Revision ID: b2515f9d4c2a +Revises: 4f7b2c8d9a10 +Create Date: 2026-06-18 23:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "b2515f9d4c2a" +down_revision = "4f7b2c8d9a10" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "agent_drive_files", + sa.Column("is_skill", sa.Boolean(), nullable=False, server_default=sa.text("false")), + ) + op.add_column( + "agent_drive_files", + sa.Column("skill_metadata", sa.Text().with_variant(mysql.LONGTEXT(), "mysql"), nullable=True), + ) + op.create_index( + "agent_drive_files_tenant_agent_is_skill_key_idx", + "agent_drive_files", + ["tenant_id", "agent_id", "is_skill", "key"], + ) + + +def downgrade() -> None: + op.drop_index("agent_drive_files_tenant_agent_is_skill_key_idx", table_name="agent_drive_files") + op.drop_column("agent_drive_files", "skill_metadata") + op.drop_column("agent_drive_files", "is_skill") diff --git a/api/models/agent.py b/api/models/agent.py index 1a13ccde77b..80abf810922 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -430,14 +430,17 @@ class AgentDriveFile(DefaultFieldsMixin, Base): synced. ``value_owned_by_drive`` gates physical cleanup: only drive-owned values (created by the agent runtime or Skill standardization, not shared with other business records) have their storage object + record deleted when the KV entry is - overwritten or removed; otherwise only the KV row is dropped. Lifecycle never relies - on ``UploadFile.used/used_by`` (not a reliable refcount). + overwritten or removed; otherwise only the KV row is dropped. Skills are represented + by the canonical ``/SKILL.md`` row with ``is_skill=True`` and a serialized + ``skill_metadata`` string. Lifecycle never relies on ``UploadFile.used/used_by`` + (not a reliable refcount). """ __tablename__ = "agent_drive_files" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="agent_drive_file_pkey"), UniqueConstraint("tenant_id", "agent_id", "key", name="agent_drive_file_scope_key_unique"), + Index("agent_drive_files_tenant_agent_is_skill_key_idx", "tenant_id", "agent_id", "is_skill", "key"), ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) @@ -453,6 +456,8 @@ class AgentDriveFile(DefaultFieldsMixin, Base): value_owned_by_drive: Mapped[bool] = mapped_column( sa.Boolean, nullable=False, default=False, server_default=sa.text("false") ) + is_skill: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False, server_default=sa.text("false")) + skill_metadata: Mapped[str | None] = mapped_column(LongText, nullable=True) size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True) hash: Mapped[str | None] = mapped_column(String(255), nullable=True) mime_type: Mapped[str | None] = mapped_column(String(255), nullable=True) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 123a2e6e04b..85141d63618 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -576,6 +576,37 @@ Truncated text preview of one Agent App drive value | ---- | ----------- | ------ | | 200 | Preview | **application/json**: [AgentDrivePreviewResponse](#agentdrivepreviewresponse)
| +### [GET] /agent/{agent_id}/drive/skills +List drive-backed skills for an Agent App + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | Agent ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Drive skills | **application/json**: [AgentDriveSkillListResponse](#agentdriveskilllistresponse)
| + +### [GET] /agent/{agent_id}/drive/skills/{skill_path}/inspect +Inspect one drive-backed skill for slash-menu hover/detail UI + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | Agent ID | Yes | string (uuid) | +| skill_path | path | Skill path/slug, e.g. tender-analyzer | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Drive skill inspect view | **application/json**: [AgentDriveSkillInspectResponse](#agentdriveskillinspectresponse)
| + ### [POST] /agent/{agent_id}/features Update an Agent App's presentation features (opener, follow-up, citations, ...) @@ -1454,6 +1485,40 @@ Truncated text preview of one drive value (binary-safe; SKILL.md is the main cas | ---- | ----------- | ------ | | 200 | Preview | **application/json**: [AgentDrivePreviewResponse](#agentdrivepreviewresponse)
| +### [GET] /apps/{app_id}/agent/drive/skills +List drive-backed skills for the bound agent + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | Application ID | Yes | string (uuid) | +| node_id | query | Workflow node ID (workflow composer variant) | No | string | +| prefix | query | Key prefix filter: '/' for one skill, 'files/' for files | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Drive skills | **application/json**: [AgentDriveSkillListResponse](#agentdriveskilllistresponse)
| + +### [GET] /apps/{app_id}/agent/drive/skills/{skill_path}/inspect +Inspect one drive-backed skill for slash-menu hover/detail UI + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | Application ID | Yes | string (uuid) | +| skill_path | path | Skill path/slug, e.g. tender-analyzer | Yes | string | +| node_id | query | Workflow node ID (workflow composer variant) | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Drive skill inspect view | **application/json**: [AgentDriveSkillInspectResponse](#agentdriveskillinspectresponse)
| + ### [DELETE] /apps/{app_id}/agent/files Delete one drive file by key; soul ref first, then the KV row (ENG-625 D5) @@ -12425,9 +12490,11 @@ Audit operation recorded for Agent Soul version/revision changes. | created_at | integer | | No | | file_kind | string | | Yes | | hash | string | | No | +| is_skill | boolean | | No | | key | string | | Yes | | mime_type | string | | No | | size | integer | | No | +| skill_metadata | string | | No | #### AgentDriveListResponse @@ -12445,6 +12512,65 @@ Audit operation recorded for Agent Soul version/revision changes. | text | string | | No | | truncated | boolean | | Yes | +#### AgentDriveSkillFileResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| available_in_drive | boolean | | Yes | +| drive_key | string | | No | +| name | string | | Yes | +| path | string | | Yes | +| type | string | | Yes | + +#### AgentDriveSkillInspectResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| archive_key | string | | No | +| created_at | integer | | No | +| description | string | | Yes | +| file_tree | [ object ] | | No | +| files | [ [AgentDriveSkillFileResponse](#agentdriveskillfileresponse) ] | | No | +| hash | string | | No | +| mime_type | string | | No | +| name | string | | Yes | +| path | string | | Yes | +| size | integer | | No | +| skill_md | [AgentDriveSkillMarkdownResponse](#agentdriveskillmarkdownresponse) | | Yes | +| skill_md_key | string | | Yes | +| source | string | | Yes | +| warnings | [ string ] | | No | + +#### AgentDriveSkillItemResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| archive_key | string | | No | +| created_at | integer | | No | +| description | string | | Yes | +| hash | string | | No | +| mime_type | string | | No | +| name | string | | Yes | +| path | string | | Yes | +| size | integer | | No | +| skill_md_key | string | | Yes | + +#### AgentDriveSkillListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| items | [ [AgentDriveSkillItemResponse](#agentdriveskillitemresponse) ] | | No | + +#### AgentDriveSkillMarkdownResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| binary | boolean | | Yes | +| key | string | | Yes | +| size | integer | | No | +| text | string | | No | +| truncated | boolean | | Yes | + #### AgentEnvVariableConfig | Name | Type | Description | Required | diff --git a/api/services/agent/skill_standardize_service.py b/api/services/agent/skill_standardize_service.py index b83004f3c4b..3fbcb81e61f 100644 --- a/api/services/agent/skill_standardize_service.py +++ b/api/services/agent/skill_standardize_service.py @@ -23,7 +23,7 @@ from typing import Any from core.tools.tool_file_manager import ToolFileManager from models.agent_config_entities import AgentSkillRefConfig from services.agent.skill_package_service import SkillPackageService -from services.agent_drive_service import AgentDriveService, DriveCommitItem, DriveFileRef +from services.agent_drive_service import AgentDriveService, DriveCommitItem, DriveFileRef, DriveSkillMetadata _FULL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip" _SKILL_MD_NAME = "SKILL.md" @@ -91,6 +91,12 @@ class SkillStandardizeService: key=skill_md_key, file_ref=DriveFileRef(kind="tool_file", id=md_tool_file.id), value_owned_by_drive=True, + is_skill=True, + skill_metadata=DriveSkillMetadata( + name=manifest.name, + description=manifest.description, + manifest_files=manifest.files, + ), ), DriveCommitItem( key=archive_key, diff --git a/api/services/agent_drive_service.py b/api/services/agent_drive_service.py index bb3f8ca69e3..62b6056412e 100644 --- a/api/services/agent_drive_service.py +++ b/api/services/agent_drive_service.py @@ -17,12 +17,14 @@ ToolFile records (see ``AgentDriveFile``). This service is the control plane: from __future__ import annotations +import json import logging import re import urllib.parse -from typing import Any, Literal +from typing import Any, Literal, TypedDict +from urllib.parse import unquote -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, field_validator from sqlalchemy import func, select from sqlalchemy.exc import DataError, SQLAlchemyError from sqlalchemy.orm import Session @@ -41,6 +43,8 @@ logger = logging.getLogger(__name__) _MAX_KEY_LENGTH = 512 _DRIVE_REF_PREFIX = "agent-" +_SKILL_MD_SUFFIX = "/SKILL.md" +_SKILL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip" class AgentDriveError(Exception): @@ -58,16 +62,86 @@ class AgentDriveError(Exception): class DriveFileRef(BaseModel): + model_config = ConfigDict(extra="forbid") + kind: Literal["upload_file", "tool_file"] id: str +class DriveSkillMetadata(BaseModel): + """Validated skill catalog metadata stored as a JSON string on the drive row.""" + + model_config = ConfigDict(extra="forbid") + + name: str + description: str = "" + # Safe archive member paths captured during skill standardization. The drive + # stores only canonical SKILL.md + full archive, so the UI uses this manifest + # to show the original uploaded package contents. + manifest_files: list[str] | None = None + + @field_validator("name") + @classmethod + def _validate_name(cls, value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("skill metadata name must not be blank") + return normalized + + class DriveCommitItem(BaseModel): + model_config = ConfigDict(extra="forbid") + key: str file_ref: DriveFileRef # Drive-owned values may be physically cleaned on overwrite/removal; refs to # files shared with other business records should set this False. value_owned_by_drive: bool = True + is_skill: bool = False + skill_metadata: DriveSkillMetadata | None = None + + +class AgentDriveSkillInfo(TypedDict): + path: str + skill_md_key: str + archive_key: str | None + name: str + description: str + size: int | None + mime_type: str | None + hash: str | None + created_at: int | None + + +class AgentDriveSkillFileInfo(TypedDict): + path: str + name: str + type: str + drive_key: str | None + available_in_drive: bool + + +class AgentDriveSkillInspectInfo(TypedDict): + path: str + skill_md_key: str + archive_key: str | None + name: str + description: str + size: int | None + mime_type: str | None + hash: str | None + created_at: int | None + source: str + files: list[AgentDriveSkillFileInfo] + file_tree: list[dict[str, Any]] + skill_md: dict[str, Any] + warnings: list[str] + + +def decode_drive_mention_ref(ref_id: str) -> str: + """Decode the prompt token's URL-encoded drive-key field.""" + + return unquote(ref_id or "") def parse_agent_drive_ref(drive_ref: str) -> str: @@ -132,6 +206,8 @@ class AgentDriveService: "mime_type": row.mime_type, "file_kind": row.file_kind.value, "file_id": row.file_id, + "is_skill": row.is_skill, + "skill_metadata": row.skill_metadata, "created_at": int(row.created_at.timestamp()) if row.created_at else None, } if include_download_url: @@ -217,6 +293,87 @@ class AgentDriveService: self._delete_storage(storage_key) return removed_keys + def list_skills(self, *, tenant_id: str, agent_id: str) -> list[AgentDriveSkillInfo]: + """Return the drive-backed skill catalog derived from canonical ``SKILL.md`` rows.""" + + with session_factory.create_session() as session: + self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id) + skill_rows = list( + session.scalars( + select(AgentDriveFile) + .where( + AgentDriveFile.tenant_id == tenant_id, + AgentDriveFile.agent_id == agent_id, + AgentDriveFile.is_skill.is_(True), + ) + .order_by(AgentDriveFile.key) + ) + ) + archive_keys = set( + session.scalars( + select(AgentDriveFile.key).where( + AgentDriveFile.tenant_id == tenant_id, + AgentDriveFile.agent_id == agent_id, + AgentDriveFile.key.in_([self._skill_archive_key(row.key) for row in skill_rows]), + ) + ) + ) + + skills: list[AgentDriveSkillInfo] = [] + for row in skill_rows: + metadata = self._parse_skill_metadata(row.key, row.skill_metadata) + archive_key = self._skill_archive_key(row.key) + skills.append( + { + "path": self._skill_path_from_key(row.key), + "skill_md_key": row.key, + "archive_key": archive_key if archive_key in archive_keys else None, + "name": metadata.name, + "description": metadata.description, + "size": row.size, + "mime_type": row.mime_type, + "hash": row.hash, + "created_at": int(row.created_at.timestamp()) if row.created_at else None, + } + ) + return skills + + def inspect_skill(self, *, tenant_id: str, agent_id: str, skill_path: str) -> AgentDriveSkillInspectInfo: + """Return the UI-facing skill inspect view for slash-menu hover/detail.""" + + skill_path = normalize_drive_key(skill_path) + skill_md_key = skill_path if skill_path.endswith(_SKILL_MD_SUFFIX) else f"{skill_path}{_SKILL_MD_SUFFIX}" + skill_path = self._skill_path_from_key(skill_md_key) + catalog = next( + (item for item in self.list_skills(tenant_id=tenant_id, agent_id=agent_id) if item["path"] == skill_path), + None, + ) + if catalog is None: + raise AgentDriveError("skill_not_found", "no drive-backed skill for this path", status_code=404) + + manifest_files = self._manifest_files_from_skill_metadata( + tenant_id=tenant_id, + agent_id=agent_id, + skill_md_key=skill_md_key, + ) + drive_items = self.manifest(tenant_id=tenant_id, agent_id=agent_id, prefix=f"{skill_path}/") + drive_keys = {item["key"] for item in drive_items} + preview = self.preview(tenant_id=tenant_id, agent_id=agent_id, key=skill_md_key) + files, warnings = self._skill_file_entries( + skill_path=skill_path, + skill_md_key=skill_md_key, + manifest_files=manifest_files, + drive_keys=drive_keys, + ) + return { + **catalog, + "source": "skill_md", + "files": files, + "file_tree": self._build_file_tree(files), + "skill_md": preview, + "warnings": warnings, + } + def _commit_one( self, session: Session, @@ -228,9 +385,10 @@ class AgentDriveService: pending_storage_deletes: list[str], ) -> dict[str, Any]: key = normalize_drive_key(item.key) + skill_metadata = self._validate_skill_commit_fields(key=key, item=item) file_kind = AgentDriveFileKind(item.file_ref.kind) file_id = item.file_ref.id - size, mime_type = self._validate_source( + size, mime_type, file_hash = self._validate_source( session, tenant_id=tenant_id, user_id=user_id, file_kind=file_kind, file_id=file_id ) @@ -245,6 +403,11 @@ class AgentDriveService: # Idempotent re-commit of the same value: leave it (do not clean). if existing.file_kind == file_kind and existing.file_id == file_id: existing.value_owned_by_drive = item.value_owned_by_drive + existing.is_skill = item.is_skill + existing.skill_metadata = skill_metadata + existing.size = size + existing.mime_type = mime_type + existing.hash = file_hash return self._row_dict(existing) # Overwrite: clean the previous drive-owned value if no longer referenced. if existing.value_owned_by_drive: @@ -259,7 +422,10 @@ class AgentDriveService: existing.file_kind = file_kind existing.file_id = file_id existing.value_owned_by_drive = item.value_owned_by_drive + existing.is_skill = item.is_skill + existing.skill_metadata = skill_metadata existing.size = size + existing.hash = file_hash existing.mime_type = mime_type return self._row_dict(existing) @@ -271,7 +437,10 @@ class AgentDriveService: file_kind=file_kind, file_id=file_id, value_owned_by_drive=item.value_owned_by_drive, + is_skill=item.is_skill, + skill_metadata=skill_metadata, size=size, + hash=file_hash, mime_type=mime_type, created_by=user_id, ) @@ -287,8 +456,187 @@ class AgentDriveService: "size": row.size, "mime_type": row.mime_type, "value_owned_by_drive": row.value_owned_by_drive, + "is_skill": row.is_skill, + "skill_metadata": row.skill_metadata, } + @staticmethod + def _skill_path_from_key(key: str) -> str: + if not key.endswith(_SKILL_MD_SUFFIX): + raise AgentDriveError( + "invalid_skill_key", + "skill rows must use the canonical '/SKILL.md' key", + status_code=500, + ) + path = key[: -len(_SKILL_MD_SUFFIX)] + if not path: + raise AgentDriveError( + "invalid_skill_key", + "skill rows must use the canonical '/SKILL.md' key", + status_code=500, + ) + return path + + @classmethod + def _skill_archive_key(cls, key: str) -> str: + return f"{cls._skill_path_from_key(key)}/{_SKILL_ARCHIVE_NAME}" + + @classmethod + def _validate_skill_commit_fields(cls, *, key: str, item: DriveCommitItem) -> str | None: + if not item.is_skill: + if item.skill_metadata is not None: + raise AgentDriveError( + "invalid_skill_metadata", + "skill metadata is only allowed for canonical skill rows", + status_code=400, + ) + return None + cls._skill_path_from_key(key) + if item.skill_metadata is None: + raise AgentDriveError( + "invalid_skill_metadata", + "skill metadata is required for canonical skill rows", + status_code=400, + ) + return json.dumps( + item.skill_metadata.model_dump(mode="json", exclude_none=True), + separators=(",", ":"), + sort_keys=True, + ) + + @staticmethod + def _parse_skill_metadata(key: str, raw_metadata: str | None) -> DriveSkillMetadata: + if raw_metadata is None: + raise AgentDriveError( + "invalid_skill_metadata", + f"skill row '{key}' is missing required metadata", + status_code=500, + ) + try: + return DriveSkillMetadata.model_validate(json.loads(raw_metadata)) + except (ValueError, TypeError) as exc: + raise AgentDriveError( + "invalid_skill_metadata", + f"skill row '{key}' has invalid stored metadata", + status_code=500, + ) from exc + + @staticmethod + def _manifest_files_from_skill_metadata(*, tenant_id: str, agent_id: str, skill_md_key: str) -> list[str] | None: + with session_factory.create_session() as session: + row = session.scalar( + select(AgentDriveFile).where( + AgentDriveFile.tenant_id == tenant_id, + AgentDriveFile.agent_id == agent_id, + AgentDriveFile.key == skill_md_key, + AgentDriveFile.is_skill.is_(True), + ) + ) + if row is None: + return None + try: + metadata = AgentDriveService._parse_skill_metadata(row.key, row.skill_metadata) + except Exception: + logger.warning("drive skill inspect: malformed skill metadata for %s", skill_md_key, exc_info=True) + return None + return [str(item) for item in (metadata.manifest_files or []) if str(item).strip()] or None + + @classmethod + def _skill_file_entries( + cls, + *, + skill_path: str, + skill_md_key: str, + manifest_files: list[str] | None, + drive_keys: set[str], + ) -> tuple[list[AgentDriveSkillFileInfo], list[str]]: + warnings: list[str] = [] + if manifest_files: + paths = sorted({normalize_drive_key(path) for path in manifest_files}) + else: + paths = sorted( + { + key.removeprefix(f"{skill_path}/") + for key in drive_keys + if not key.endswith(f"/{_SKILL_ARCHIVE_NAME}") + } + ) + warnings.append("manifest_files_unavailable") + + files: list[AgentDriveSkillFileInfo] = [] + for path in paths: + if path == _SKILL_ARCHIVE_NAME: + continue + drive_key = f"{skill_path}/{path}" + files.append( + { + "path": path, + "name": path.rsplit("/", 1)[-1], + "type": "file", + "drive_key": drive_key if drive_key in drive_keys else None, + "available_in_drive": drive_key in drive_keys, + } + ) + if "SKILL.md" not in {file["path"] for file in files}: + files.insert( + 0, + { + "path": "SKILL.md", + "name": "SKILL.md", + "type": "file", + "drive_key": skill_md_key, + "available_in_drive": skill_md_key in drive_keys, + }, + ) + return files, warnings + + @staticmethod + def _build_file_tree(files: list[AgentDriveSkillFileInfo]) -> list[dict[str, Any]]: + root: dict[str, Any] = {} + for file in files: + cursor = root + parts = [part for part in file["path"].split("/") if part] + path_parts: list[str] = [] + for part in parts[:-1]: + path_parts.append(part) + directory = cursor.setdefault( + part, + { + "name": part, + "path": "/".join(path_parts), + "type": "directory", + "children": {}, + }, + ) + cursor = directory["children"] + leaf_name = parts[-1] if parts else file["name"] + cursor[leaf_name] = { + "name": leaf_name, + "path": file["path"], + "type": file["type"], + "drive_key": file["drive_key"], + "available_in_drive": file["available_in_drive"], + } + + def serialize(node: dict[str, Any]) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + for item in sorted(node.values(), key=lambda value: (value["type"] != "directory", value["name"])): + if item["type"] == "directory": + children = serialize(item["children"]) + result.append( + { + "name": item["name"], + "path": item["path"], + "type": "directory", + "children": children, + } + ) + else: + result.append(item) + return result + + return serialize(root) + @staticmethod def _assert_agent_belongs_to_tenant(session: Session, *, tenant_id: str, agent_id: str) -> None: try: @@ -309,7 +657,7 @@ class AgentDriveService: user_id: str, file_kind: AgentDriveFileKind, file_id: str, - ) -> tuple[int | None, str | None]: + ) -> tuple[int | None, str | None, str | None]: """Verify the source file exists for the tenant (and user, for ToolFile). Malformed ids (e.g. a non-UUID hitting a UUID column) are treated as a @@ -328,7 +676,7 @@ class AgentDriveService: raise AgentDriveError( "source_not_found", "source ToolFile not found for this tenant/user", status_code=404 ) - return tool_file.size, tool_file.mimetype + return tool_file.size, tool_file.mimetype, None upload_file = session.scalar( select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id) ) @@ -337,7 +685,7 @@ class AgentDriveService: raise AgentDriveError("source_not_found", "source file ref is invalid", status_code=404) from exc if upload_file is None: raise AgentDriveError("source_not_found", "source UploadFile not found for this tenant", status_code=404) - return upload_file.size, upload_file.mime_type + return upload_file.size, upload_file.mime_type, upload_file.hash def _cleanup_value( self, @@ -509,6 +857,8 @@ __all__ = [ "AgentDriveService", "DriveCommitItem", "DriveFileRef", + "DriveSkillMetadata", + "decode_drive_mention_ref", "normalize_drive_key", "parse_agent_drive_ref", ] diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py b/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py index 9d1b6c4c0e9..81f6fcf36bf 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py @@ -20,6 +20,10 @@ from controllers.console.app.agent_drive_inspector import ( AgentDriveListByAgentApi, AgentDrivePreviewApi, AgentDrivePreviewByAgentApi, + AgentDriveSkillInspectApi, + AgentDriveSkillInspectByAgentApi, + AgentDriveSkillListApi, + AgentDriveSkillListByAgentApi, ) from services.agent_drive_service import AgentDriveError @@ -97,6 +101,124 @@ def test_list_resolves_workflow_node_binding_agent(): assert composer.resolve_workflow_node_agent_id.call_args.kwargs["node_id"] == "agent-node-1" +def test_skill_list_by_agent_calls_service(): + raw = _raw(AgentDriveSkillListByAgentApi.get) + with app.test_request_context("/"): + with ( + patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.AgentDriveService") as drive, + ): + drive.return_value.list_skills.return_value = [ + { + "path": "pdf-toolkit", + "skill_md_key": "pdf-toolkit/SKILL.md", + "archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip", + "name": "PDF Toolkit", + "description": "Work with PDFs.", + "size": 5, + "mime_type": "text/markdown", + "hash": None, + "created_at": 1718000000, + } + ] + body = raw(AgentDriveSkillListByAgentApi(), "tenant-1", "agent-1") + + assert body["items"][0]["path"] == "pdf-toolkit" + resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1") + assert drive.return_value.list_skills.call_args.kwargs["agent_id"] == "agent-1" + + +def test_skill_list_resolves_workflow_node_binding_agent(): + raw = _raw(AgentDriveSkillListApi.get) + with app.test_request_context("/?node_id=agent-node-1"): + with ( + patch(f"{_MOD}.AgentComposerService") as composer, + patch(f"{_MOD}.AgentDriveService") as drive, + ): + composer.resolve_workflow_node_agent_id.return_value = "wf-agent-9" + drive.return_value.list_skills.return_value = [] + body = raw(AgentDriveSkillListApi(), _APP) + + assert body == {"items": []} + assert drive.return_value.list_skills.call_args.kwargs["agent_id"] == "wf-agent-9" + + +def test_skill_inspect_by_agent_returns_strict_json_response(): + raw = _raw(AgentDriveSkillInspectByAgentApi.get) + payload = { + "path": "pdf-toolkit", + "skill_md_key": "pdf-toolkit/SKILL.md", + "archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip", + "name": "PDF Toolkit", + "description": "Work with PDFs.", + "size": 5, + "mime_type": "text/markdown", + "hash": None, + "created_at": 1718000000, + "source": "skill_md", + "files": [ + { + "path": "SKILL.md", + "name": "SKILL.md", + "type": "file", + "drive_key": "pdf-toolkit/SKILL.md", + "available_in_drive": True, + } + ], + "file_tree": [], + "skill_md": { + "key": "pdf-toolkit/SKILL.md", + "size": 5, + "truncated": False, + "binary": False, + "text": "# PDF Toolkit\nUse it.\n", + }, + "warnings": [], + } + with app.test_request_context("/"): + with ( + patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP), + patch(f"{_MOD}.AgentDriveService") as drive, + ): + drive.return_value.inspect_skill.return_value = payload + response = raw(AgentDriveSkillInspectByAgentApi(), "tenant-1", "agent-1", "pdf-toolkit") + + assert response.status_code == 200 + assert response.get_json()["skill_md"]["text"] == "# PDF Toolkit\nUse it.\n" + assert b"# PDF Toolkit\\nUse it.\\n" in response.get_data() + + +def test_skill_inspect_resolves_workflow_node_binding_agent(): + raw = _raw(AgentDriveSkillInspectApi.get) + payload = { + "path": "pdf-toolkit", + "skill_md_key": "pdf-toolkit/SKILL.md", + "archive_key": None, + "name": "PDF Toolkit", + "description": "", + "size": 5, + "mime_type": "text/markdown", + "hash": None, + "created_at": None, + "source": "skill_md", + "files": [], + "file_tree": [], + "skill_md": {"key": "pdf-toolkit/SKILL.md", "size": 5, "truncated": False, "binary": False, "text": "# hi"}, + "warnings": [], + } + with app.test_request_context("/?node_id=agent-node-1"): + with ( + patch(f"{_MOD}.AgentComposerService") as composer, + patch(f"{_MOD}.AgentDriveService") as drive, + ): + composer.resolve_workflow_node_agent_id.return_value = "wf-agent-9" + drive.return_value.inspect_skill.return_value = payload + response = raw(AgentDriveSkillInspectApi(), _APP, "pdf-toolkit") + + assert response.get_json()["path"] == "pdf-toolkit" + assert drive.return_value.inspect_skill.call_args.kwargs["agent_id"] == "wf-agent-9" + + def test_list_400_when_no_agent_bound(): raw = _raw(AgentDriveListApi.get) app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None) diff --git a/api/tests/unit_tests/services/agent/test_skill_standardize_service.py b/api/tests/unit_tests/services/agent/test_skill_standardize_service.py index 128a6c42801..29b4c7e59d6 100644 --- a/api/tests/unit_tests/services/agent/test_skill_standardize_service.py +++ b/api/tests/unit_tests/services/agent/test_skill_standardize_service.py @@ -67,6 +67,10 @@ def test_standardize_creates_two_drive_owned_toolfiles_and_commits(): assert [item.key for item in items] == ["pdf-toolkit/SKILL.md", "pdf-toolkit/.DIFY-SKILL-FULL.zip"] assert all(item.value_owned_by_drive for item in items) assert [item.file_ref.id for item in items] == ["md-tool-file", "zip-tool-file"] + assert items[0].is_skill is True + assert items[0].skill_metadata.name == "PDF Toolkit" + assert items[0].skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"] + assert items[1].is_skill is False # The returned skill ref carries stable drive paths + file ids. skill = result["skill"] diff --git a/api/tests/unit_tests/services/test_agent_drive_service.py b/api/tests/unit_tests/services/test_agent_drive_service.py index 09cde917946..afc3f08124a 100644 --- a/api/tests/unit_tests/services/test_agent_drive_service.py +++ b/api/tests/unit_tests/services/test_agent_drive_service.py @@ -23,6 +23,7 @@ from services.agent_drive_service import ( AgentDriveError, AgentDriveService, DriveCommitItem, + DriveSkillMetadata, normalize_drive_key, parse_agent_drive_ref, ) @@ -515,3 +516,104 @@ def test_manifest_items_carry_created_at_for_inspector(): _commit("files/x.txt", tf) items = AgentDriveService().manifest(tenant_id=TENANT, agent_id=AGENT) assert items[0]["created_at"] is None or isinstance(items[0]["created_at"], int) + + +# ── DIFY-2517: skill catalog / inspect ─────────────────────────────────────── + + +def _commit_skill(*, manifest_files: list[str] | None = None) -> None: + md = _seed_tool_file(name="SKILL.md") + zf = _seed_tool_file(name="full.zip") + AgentDriveService().commit( + tenant_id=TENANT, + user_id=USER, + agent_id=AGENT, + items=[ + DriveCommitItem( + key="pdf-toolkit/SKILL.md", + file_ref={"kind": "tool_file", "id": md}, + value_owned_by_drive=True, + is_skill=True, + skill_metadata=DriveSkillMetadata( + name="PDF Toolkit", + description="Work with PDFs.", + manifest_files=manifest_files, + ), + ), + DriveCommitItem( + key="pdf-toolkit/.DIFY-SKILL-FULL.zip", + file_ref={"kind": "tool_file", "id": zf}, + value_owned_by_drive=True, + ), + ], + ) + + +def test_list_skills_uses_canonical_skill_rows(): + _commit_skill(manifest_files=["SKILL.md", "scripts/run.py"]) + + skills = AgentDriveService().list_skills(tenant_id=TENANT, agent_id=AGENT) + + created_at = skills[0].pop("created_at") + assert skills == [ + { + "path": "pdf-toolkit", + "skill_md_key": "pdf-toolkit/SKILL.md", + "archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip", + "name": "PDF Toolkit", + "description": "Work with PDFs.", + "size": 5, + "mime_type": "text/plain", + "hash": None, + } + ] + assert created_at is None or isinstance(created_at, int) + + +def test_inspect_skill_returns_manifest_files_and_file_tree(): + _commit_skill(manifest_files=["SKILL.md", "references/guide.md", "scripts/run.py"]) + + with patch("services.agent_drive_service.storage") as storage_mock: + storage_mock.load_stream.return_value = iter([b"# PDF Toolkit\n"]) + result = AgentDriveService().inspect_skill(tenant_id=TENANT, agent_id=AGENT, skill_path="pdf-toolkit") + + assert result["source"] == "skill_md" + assert result["warnings"] == [] + assert [file["path"] for file in result["files"]] == ["SKILL.md", "references/guide.md", "scripts/run.py"] + assert result["files"][0]["available_in_drive"] is True + assert result["files"][1]["available_in_drive"] is False + assert result["file_tree"][0]["name"] == "references" + assert result["file_tree"][1]["name"] == "scripts" + assert result["file_tree"][2]["name"] == "SKILL.md" + assert result["skill_md"]["text"] == "# PDF Toolkit\n" + + +def test_inspect_skill_falls_back_to_drive_keys_when_manifest_missing(): + _commit_skill(manifest_files=None) + + with patch("services.agent_drive_service.storage") as storage_mock: + storage_mock.load_stream.return_value = iter([b"# PDF Toolkit\n"]) + result = AgentDriveService().inspect_skill(tenant_id=TENANT, agent_id=AGENT, skill_path="pdf-toolkit") + + assert result["warnings"] == ["manifest_files_unavailable"] + assert [file["path"] for file in result["files"]] == ["SKILL.md"] + + +def test_skill_metadata_rejects_non_canonical_rows(): + tf = _seed_tool_file(name="not-skill.md") + with pytest.raises(AgentDriveError) as exc_info: + AgentDriveService().commit( + tenant_id=TENANT, + user_id=USER, + agent_id=AGENT, + items=[ + DriveCommitItem( + key="files/not-skill.md", + file_ref={"kind": "tool_file", "id": tf}, + value_owned_by_drive=True, + is_skill=True, + skill_metadata=DriveSkillMetadata(name="Bad"), + ) + ], + ) + assert exc_info.value.code == "invalid_skill_key" diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index fbfca3be118..597649d21c2 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -29,6 +29,10 @@ import { zGetAgentByAgentIdDriveFilesPreviewResponse, zGetAgentByAgentIdDriveFilesQuery, zGetAgentByAgentIdDriveFilesResponse, + zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath, + zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse, + zGetAgentByAgentIdDriveSkillsPath, + zGetAgentByAgentIdDriveSkillsResponse, zGetAgentByAgentIdLogsByConversationIdMessagesPath, zGetAgentByAgentIdLogsByConversationIdMessagesQuery, zGetAgentByAgentIdLogsByConversationIdMessagesResponse, @@ -336,8 +340,52 @@ export const files = { preview, } +/** + * Inspect one drive-backed skill for slash-menu hover/detail UI + */ +export const get9 = oc + .route({ + description: 'Inspect one drive-backed skill for slash-menu hover/detail UI', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAgentByAgentIdDriveSkillsBySkillPathInspect', + path: '/agent/{agent_id}/drive/skills/{skill_path}/inspect', + tags: ['console'], + }) + .input(z.object({ params: zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath })) + .output(zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse) + +export const inspect = { + get: get9, +} + +export const bySkillPath = { + inspect, +} + +/** + * List drive-backed skills for an Agent App + */ +export const get10 = oc + .route({ + description: 'List drive-backed skills for an Agent App', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAgentByAgentIdDriveSkills', + path: '/agent/{agent_id}/drive/skills', + tags: ['console'], + }) + .input(z.object({ params: zGetAgentByAgentIdDriveSkillsPath })) + .output(zGetAgentByAgentIdDriveSkillsResponse) + +export const skills = { + get: get10, + bySkillPath, +} + export const drive = { files, + skills, } /** @@ -420,7 +468,7 @@ export const files2 = { post: post6, } -export const get9 = oc +export const get11 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -432,10 +480,10 @@ export const get9 = oc .output(zGetAgentByAgentIdLogSourcesResponse) export const logSources = { - get: get9, + get: get11, } -export const get10 = oc +export const get12 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -452,14 +500,14 @@ export const get10 = oc .output(zGetAgentByAgentIdLogsByConversationIdMessagesResponse) export const messages = { - get: get10, + get: get12, } export const byConversationId = { messages, } -export const get11 = oc +export const get13 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -473,14 +521,14 @@ export const get11 = oc .output(zGetAgentByAgentIdLogsResponse) export const logs = { - get: get11, + get: get13, byConversationId, } /** * Get Agent App message details by ID */ -export const get12 = oc +export const get14 = oc .route({ description: 'Get Agent App message details by ID', inputStructure: 'detailed', @@ -493,7 +541,7 @@ export const get12 = oc .output(zGetAgentByAgentIdMessagesByMessageIdResponse) export const byMessageId2 = { - get: get12, + get: get14, } export const messages2 = { @@ -503,7 +551,7 @@ export const messages2 = { /** * List workflow apps that reference this Agent App's bound Agent (read-only) */ -export const get13 = oc +export const get15 = oc .route({ description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)', inputStructure: 'detailed', @@ -516,13 +564,13 @@ export const get13 = oc .output(zGetAgentByAgentIdReferencingWorkflowsResponse) export const referencingWorkflows = { - get: get13, + get: get15, } /** * Read a text/binary preview file in an Agent App conversation sandbox */ -export const get14 = oc +export const get16 = oc .route({ description: 'Read a text/binary preview file in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -540,7 +588,7 @@ export const get14 = oc .output(zGetAgentByAgentIdSandboxFilesReadResponse) export const read = { - get: get14, + get: get16, } /** @@ -570,7 +618,7 @@ export const upload = { /** * List a directory in an Agent App conversation sandbox */ -export const get15 = oc +export const get17 = oc .route({ description: 'List a directory in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -588,7 +636,7 @@ export const get15 = oc .output(zGetAgentByAgentIdSandboxFilesResponse) export const files3 = { - get: get15, + get: get17, read, upload, } @@ -661,12 +709,12 @@ export const bySlug = { inferTools, } -export const skills = { +export const skills2 = { upload: upload2, bySlug, } -export const get16 = oc +export const get18 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -683,14 +731,14 @@ export const get16 = oc .output(zGetAgentByAgentIdStatisticsSummaryResponse) export const summary = { - get: get16, + get: get18, } export const statistics = { summary, } -export const get17 = oc +export const get19 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -702,10 +750,10 @@ export const get17 = oc .output(zGetAgentByAgentIdVersionsByVersionIdResponse) export const byVersionId = { - get: get17, + get: get19, } -export const get18 = oc +export const get20 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -717,7 +765,7 @@ export const get18 = oc .output(zGetAgentByAgentIdVersionsResponse) export const versions = { - get: get18, + get: get20, byVersionId, } @@ -733,7 +781,7 @@ export const delete3 = oc .input(z.object({ params: zDeleteAgentByAgentIdPath })) .output(zDeleteAgentByAgentIdResponse) -export const get19 = oc +export const get21 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -757,7 +805,7 @@ export const put2 = oc export const byAgentId = { delete: delete3, - get: get19, + get: get21, put: put2, chatMessages, composer, @@ -771,12 +819,12 @@ export const byAgentId = { messages: messages2, referencingWorkflows, sandbox, - skills, + skills: skills2, statistics, versions, } -export const get20 = oc +export const get22 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -800,7 +848,7 @@ export const post10 = oc .output(zPostAgentResponse) export const agent = { - get: get20, + get: get22, post: post10, inviteOptions, byAgentId, diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 64afc442406..4192dfbaf9e 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -148,6 +148,29 @@ export type AgentDrivePreviewResponse = { truncated: boolean } +export type AgentDriveSkillListResponse = { + items?: Array +} + +export type AgentDriveSkillInspectResponse = { + archive_key?: string | null + created_at?: number | null + description: string + file_tree?: Array<{ + [key: string]: unknown + }> + files?: Array + hash?: string | null + mime_type?: string | null + name: string + path: string + size?: number | null + skill_md: AgentDriveSkillMarkdownResponse + skill_md_key: string + source: string + warnings?: Array +} + export type AgentAppFeaturesPayload = { opening_statement?: string | null retriever_resource?: AgentFeatureToggleConfig | null @@ -530,9 +553,39 @@ export type AgentDriveItemResponse = { created_at?: number | null file_kind: string hash?: string | null + is_skill?: boolean | null key: string mime_type?: string | null size?: number | null + skill_metadata?: string | null +} + +export type AgentDriveSkillItemResponse = { + archive_key?: string | null + created_at?: number | null + description: string + hash?: string | null + mime_type?: string | null + name: string + path: string + size?: number | null + skill_md_key: string +} + +export type AgentDriveSkillFileResponse = { + available_in_drive: boolean + drive_key?: string | null + name: string + path: string + type: string +} + +export type AgentDriveSkillMarkdownResponse = { + binary: boolean + key: string + size?: number | null + text?: string | null + truncated: boolean } export type AgentFeatureToggleConfig = { @@ -1837,6 +1890,39 @@ export type GetAgentByAgentIdDriveFilesPreviewResponses = { export type GetAgentByAgentIdDriveFilesPreviewResponse = GetAgentByAgentIdDriveFilesPreviewResponses[keyof GetAgentByAgentIdDriveFilesPreviewResponses] +export type GetAgentByAgentIdDriveSkillsData = { + body?: never + path: { + agent_id: string + } + query?: never + url: '/agent/{agent_id}/drive/skills' +} + +export type GetAgentByAgentIdDriveSkillsResponses = { + 200: AgentDriveSkillListResponse +} + +export type GetAgentByAgentIdDriveSkillsResponse + = GetAgentByAgentIdDriveSkillsResponses[keyof GetAgentByAgentIdDriveSkillsResponses] + +export type GetAgentByAgentIdDriveSkillsBySkillPathInspectData = { + body?: never + path: { + agent_id: string + skill_path: string + } + query?: never + url: '/agent/{agent_id}/drive/skills/{skill_path}/inspect' +} + +export type GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses = { + 200: AgentDriveSkillInspectResponse +} + +export type GetAgentByAgentIdDriveSkillsBySkillPathInspectResponse + = GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses[keyof GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses] + export type PostAgentByAgentIdFeaturesData = { body: AgentAppFeaturesPayload path: { diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 7d6bd6f5eb2..5dca172d9ea 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -286,9 +286,11 @@ export const zAgentDriveItemResponse = z.object({ created_at: z.int().nullish(), file_kind: z.string(), hash: z.string().nullish(), + is_skill: z.boolean().nullish(), key: z.string(), mime_type: z.string().nullish(), size: z.int().nullish(), + skill_metadata: z.string().nullish(), }) /** @@ -298,6 +300,70 @@ export const zAgentDriveListResponse = z.object({ items: z.array(zAgentDriveItemResponse).optional(), }) +/** + * AgentDriveSkillItemResponse + */ +export const zAgentDriveSkillItemResponse = z.object({ + archive_key: z.string().nullish(), + created_at: z.int().nullish(), + description: z.string(), + hash: z.string().nullish(), + mime_type: z.string().nullish(), + name: z.string(), + path: z.string(), + size: z.int().nullish(), + skill_md_key: z.string(), +}) + +/** + * AgentDriveSkillListResponse + */ +export const zAgentDriveSkillListResponse = z.object({ + items: z.array(zAgentDriveSkillItemResponse).optional(), +}) + +/** + * AgentDriveSkillFileResponse + */ +export const zAgentDriveSkillFileResponse = z.object({ + available_in_drive: z.boolean(), + drive_key: z.string().nullish(), + name: z.string(), + path: z.string(), + type: z.string(), +}) + +/** + * AgentDriveSkillMarkdownResponse + */ +export const zAgentDriveSkillMarkdownResponse = z.object({ + binary: z.boolean(), + key: z.string(), + size: z.int().nullish(), + text: z.string().nullish(), + truncated: z.boolean(), +}) + +/** + * AgentDriveSkillInspectResponse + */ +export const zAgentDriveSkillInspectResponse = z.object({ + archive_key: z.string().nullish(), + created_at: z.int().nullish(), + description: z.string(), + file_tree: z.array(z.record(z.string(), z.unknown())).optional(), + files: z.array(zAgentDriveSkillFileResponse).optional(), + hash: z.string().nullish(), + mime_type: z.string().nullish(), + name: z.string(), + path: z.string(), + size: z.int().nullish(), + skill_md: zAgentDriveSkillMarkdownResponse, + skill_md_key: z.string(), + source: z.string(), + warnings: z.array(z.string()).optional(), +}) + /** * AgentFeatureToggleConfig */ @@ -2304,6 +2370,26 @@ export const zGetAgentByAgentIdDriveFilesPreviewQuery = z.object({ */ export const zGetAgentByAgentIdDriveFilesPreviewResponse = zAgentDrivePreviewResponse +export const zGetAgentByAgentIdDriveSkillsPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Drive skills + */ +export const zGetAgentByAgentIdDriveSkillsResponse = zAgentDriveSkillListResponse + +export const zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath = z.object({ + agent_id: z.uuid(), + skill_path: z.string(), +}) + +/** + * Drive skill inspect view + */ +export const zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse + = zAgentDriveSkillInspectResponse + export const zPostAgentByAgentIdFeaturesBody = zAgentAppFeaturesPayload export const zPostAgentByAgentIdFeaturesPath = z.object({ diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index eab3c17eb43..f24407a17d4 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -54,6 +54,12 @@ import { zGetAppsByAppIdAgentDriveFilesPreviewResponse, zGetAppsByAppIdAgentDriveFilesQuery, zGetAppsByAppIdAgentDriveFilesResponse, + zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectPath, + zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectQuery, + zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse, + zGetAppsByAppIdAgentDriveSkillsPath, + zGetAppsByAppIdAgentDriveSkillsQuery, + zGetAppsByAppIdAgentDriveSkillsResponse, zGetAppsByAppIdAgentLogsPath, zGetAppsByAppIdAgentLogsQuery, zGetAppsByAppIdAgentLogsResponse, @@ -861,8 +867,62 @@ export const files = { preview: preview2, } +/** + * Inspect one drive-backed skill for slash-menu hover/detail UI + */ +export const get8 = oc + .route({ + description: 'Inspect one drive-backed skill for slash-menu hover/detail UI', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAppsByAppIdAgentDriveSkillsBySkillPathInspect', + path: '/apps/{app_id}/agent/drive/skills/{skill_path}/inspect', + tags: ['console'], + }) + .input( + z.object({ + params: zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectPath, + query: zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectQuery.optional(), + }), + ) + .output(zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse) + +export const inspect = { + get: get8, +} + +export const bySkillPath = { + inspect, +} + +/** + * List drive-backed skills for the bound agent + */ +export const get9 = oc + .route({ + description: 'List drive-backed skills for the bound agent', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAppsByAppIdAgentDriveSkills', + path: '/apps/{app_id}/agent/drive/skills', + tags: ['console'], + }) + .input( + z.object({ + params: zGetAppsByAppIdAgentDriveSkillsPath, + query: zGetAppsByAppIdAgentDriveSkillsQuery.optional(), + }), + ) + .output(zGetAppsByAppIdAgentDriveSkillsResponse) + +export const skills = { + get: get9, + bySkillPath, +} + export const drive = { files, + skills, } /** @@ -920,7 +980,7 @@ export const files2 = { * * Get agent execution logs for an application */ -export const get8 = oc +export const get10 = oc .route({ description: 'Get agent execution logs for an application', inputStructure: 'detailed', @@ -934,7 +994,7 @@ export const get8 = oc .output(zGetAppsByAppIdAgentLogsResponse) export const logs = { - get: get8, + get: get10, } /** @@ -1021,7 +1081,7 @@ export const bySlug = { inferTools, } -export const skills = { +export const skills2 = { upload, bySlug, } @@ -1030,13 +1090,13 @@ export const agent = { drive, files: files2, logs, - skills, + skills: skills2, } /** * Get status of annotation reply action job */ -export const get9 = oc +export const get11 = oc .route({ description: 'Get status of annotation reply action job', inputStructure: 'detailed', @@ -1049,7 +1109,7 @@ export const get9 = oc .output(zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponse) export const byJobId = { - get: get9, + get: get11, } export const status = { @@ -1088,7 +1148,7 @@ export const annotationReply = { /** * Get annotation settings for an app */ -export const get10 = oc +export const get12 = oc .route({ description: 'Get annotation settings for an app', inputStructure: 'detailed', @@ -1101,7 +1161,7 @@ export const get10 = oc .output(zGetAppsByAppIdAnnotationSettingResponse) export const annotationSetting = { - get: get10, + get: get12, } /** @@ -1154,7 +1214,7 @@ export const batchImport = { /** * Get status of batch import job */ -export const get11 = oc +export const get13 = oc .route({ description: 'Get status of batch import job', inputStructure: 'detailed', @@ -1167,7 +1227,7 @@ export const get11 = oc .output(zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponse) export const byJobId2 = { - get: get11, + get: get13, } export const batchImportStatus = { @@ -1177,7 +1237,7 @@ export const batchImportStatus = { /** * Get count of message annotations for the app */ -export const get12 = oc +export const get14 = oc .route({ description: 'Get count of message annotations for the app', inputStructure: 'detailed', @@ -1190,13 +1250,13 @@ export const get12 = oc .output(zGetAppsByAppIdAnnotationsCountResponse) export const count2 = { - get: get12, + get: get14, } /** * Export all annotations for an app with CSV injection protection */ -export const get13 = oc +export const get15 = oc .route({ description: 'Export all annotations for an app with CSV injection protection', inputStructure: 'detailed', @@ -1209,13 +1269,13 @@ export const get13 = oc .output(zGetAppsByAppIdAnnotationsExportResponse) export const export_ = { - get: get13, + get: get15, } /** * Get hit histories for an annotation */ -export const get14 = oc +export const get16 = oc .route({ description: 'Get hit histories for an annotation', inputStructure: 'detailed', @@ -1233,7 +1293,7 @@ export const get14 = oc .output(zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesResponse) export const hitHistories = { - get: get14, + get: get16, } export const delete3 = oc @@ -1289,7 +1349,7 @@ export const delete4 = oc /** * Get annotations for an app with pagination */ -export const get15 = oc +export const get17 = oc .route({ description: 'Get annotations for an app with pagination', inputStructure: 'detailed', @@ -1326,7 +1386,7 @@ export const post16 = oc export const annotations = { delete: delete4, - get: get15, + get: get17, post: post16, batchImport, batchImportStatus, @@ -1392,7 +1452,7 @@ export const delete5 = oc /** * Get chat conversation details */ -export const get16 = oc +export const get18 = oc .route({ description: 'Get chat conversation details', inputStructure: 'detailed', @@ -1406,13 +1466,13 @@ export const get16 = oc export const byConversationId = { delete: delete5, - get: get16, + get: get18, } /** * Get chat conversations with pagination, filtering and summary */ -export const get17 = oc +export const get19 = oc .route({ description: 'Get chat conversations with pagination, filtering and summary', inputStructure: 'detailed', @@ -1430,14 +1490,14 @@ export const get17 = oc .output(zGetAppsByAppIdChatConversationsResponse) export const chatConversations = { - get: get17, + get: get19, byConversationId, } /** * Get suggested questions for a message */ -export const get18 = oc +export const get20 = oc .route({ description: 'Get suggested questions for a message', inputStructure: 'detailed', @@ -1450,7 +1510,7 @@ export const get18 = oc .output(zGetAppsByAppIdChatMessagesByMessageIdSuggestedQuestionsResponse) export const suggestedQuestions = { - get: get18, + get: get20, } export const byMessageId = { @@ -1483,7 +1543,7 @@ export const byTaskId = { /** * Get chat messages for a conversation with pagination */ -export const get19 = oc +export const get21 = oc .route({ description: 'Get chat messages for a conversation with pagination', inputStructure: 'detailed', @@ -1498,7 +1558,7 @@ export const get19 = oc .output(zGetAppsByAppIdChatMessagesResponse) export const chatMessages = { - get: get19, + get: get21, byMessageId, byTaskId, } @@ -1522,7 +1582,7 @@ export const delete6 = oc /** * Get completion conversation details with messages */ -export const get20 = oc +export const get22 = oc .route({ description: 'Get completion conversation details with messages', inputStructure: 'detailed', @@ -1536,13 +1596,13 @@ export const get20 = oc export const byConversationId2 = { delete: delete6, - get: get20, + get: get22, } /** * Get completion conversations with pagination and filtering */ -export const get21 = oc +export const get23 = oc .route({ description: 'Get completion conversations with pagination and filtering', inputStructure: 'detailed', @@ -1560,7 +1620,7 @@ export const get21 = oc .output(zGetAppsByAppIdCompletionConversationsResponse) export const completionConversations = { - get: get21, + get: get23, byConversationId: byConversationId2, } @@ -1615,7 +1675,7 @@ export const completionMessages = { /** * Get conversation variables for an application */ -export const get22 = oc +export const get24 = oc .route({ description: 'Get conversation variables for an application', inputStructure: 'detailed', @@ -1633,7 +1693,7 @@ export const get22 = oc .output(zGetAppsByAppIdConversationVariablesResponse) export const conversationVariables = { - get: get22, + get: get24, } /** @@ -1694,7 +1754,7 @@ export const copy = { * * Export application configuration as DSL */ -export const get23 = oc +export const get25 = oc .route({ description: 'Export application configuration as DSL', inputStructure: 'detailed', @@ -1710,13 +1770,13 @@ export const get23 = oc .output(zGetAppsByAppIdExportResponse) export const export2 = { - get: get23, + get: get25, } /** * Export user feedback data for Google Sheets */ -export const get24 = oc +export const get26 = oc .route({ description: 'Export user feedback data for Google Sheets', inputStructure: 'detailed', @@ -1734,7 +1794,7 @@ export const get24 = oc .output(zGetAppsByAppIdFeedbacksExportResponse) export const export3 = { - get: get24, + get: get26, } /** @@ -1779,7 +1839,7 @@ export const icon = { /** * Get message details by ID */ -export const get25 = oc +export const get27 = oc .route({ description: 'Get message details by ID', inputStructure: 'detailed', @@ -1792,7 +1852,7 @@ export const get25 = oc .output(zGetAppsByAppIdMessagesByMessageIdResponse) export const byMessageId2 = { - get: get25, + get: get27, } export const messages = { @@ -1864,7 +1924,7 @@ export const publishToCreatorsPlatform = { /** * Get MCP server configuration for an application */ -export const get26 = oc +export const get28 = oc .route({ description: 'Get MCP server configuration for an application', inputStructure: 'detailed', @@ -1908,7 +1968,7 @@ export const put = oc .output(zPutAppsByAppIdServerResponse) export const server = { - get: get26, + get: get28, post: post29, put, } @@ -2009,7 +2069,7 @@ export const star = { /** * Get average response time statistics for an application */ -export const get27 = oc +export const get29 = oc .route({ description: 'Get average response time statistics for an application', inputStructure: 'detailed', @@ -2027,13 +2087,13 @@ export const get27 = oc .output(zGetAppsByAppIdStatisticsAverageResponseTimeResponse) export const averageResponseTime = { - get: get27, + get: get29, } /** * Get average session interaction statistics for an application */ -export const get28 = oc +export const get30 = oc .route({ description: 'Get average session interaction statistics for an application', inputStructure: 'detailed', @@ -2051,13 +2111,13 @@ export const get28 = oc .output(zGetAppsByAppIdStatisticsAverageSessionInteractionsResponse) export const averageSessionInteractions = { - get: get28, + get: get30, } /** * Get daily conversation statistics for an application */ -export const get29 = oc +export const get31 = oc .route({ description: 'Get daily conversation statistics for an application', inputStructure: 'detailed', @@ -2075,13 +2135,13 @@ export const get29 = oc .output(zGetAppsByAppIdStatisticsDailyConversationsResponse) export const dailyConversations = { - get: get29, + get: get31, } /** * Get daily terminal/end-user statistics for an application */ -export const get30 = oc +export const get32 = oc .route({ description: 'Get daily terminal/end-user statistics for an application', inputStructure: 'detailed', @@ -2099,13 +2159,13 @@ export const get30 = oc .output(zGetAppsByAppIdStatisticsDailyEndUsersResponse) export const dailyEndUsers = { - get: get30, + get: get32, } /** * Get daily message statistics for an application */ -export const get31 = oc +export const get33 = oc .route({ description: 'Get daily message statistics for an application', inputStructure: 'detailed', @@ -2123,13 +2183,13 @@ export const get31 = oc .output(zGetAppsByAppIdStatisticsDailyMessagesResponse) export const dailyMessages = { - get: get31, + get: get33, } /** * Get daily token cost statistics for an application */ -export const get32 = oc +export const get34 = oc .route({ description: 'Get daily token cost statistics for an application', inputStructure: 'detailed', @@ -2147,13 +2207,13 @@ export const get32 = oc .output(zGetAppsByAppIdStatisticsTokenCostsResponse) export const tokenCosts = { - get: get32, + get: get34, } /** * Get tokens per second statistics for an application */ -export const get33 = oc +export const get35 = oc .route({ description: 'Get tokens per second statistics for an application', inputStructure: 'detailed', @@ -2171,13 +2231,13 @@ export const get33 = oc .output(zGetAppsByAppIdStatisticsTokensPerSecondResponse) export const tokensPerSecond = { - get: get33, + get: get35, } /** * Get user satisfaction rate statistics for an application */ -export const get34 = oc +export const get36 = oc .route({ description: 'Get user satisfaction rate statistics for an application', inputStructure: 'detailed', @@ -2195,7 +2255,7 @@ export const get34 = oc .output(zGetAppsByAppIdStatisticsUserSatisfactionRateResponse) export const userSatisfactionRate = { - get: get34, + get: get36, } export const statistics = { @@ -2212,7 +2272,7 @@ export const statistics = { /** * Get available TTS voices for a specific language */ -export const get35 = oc +export const get37 = oc .route({ description: 'Get available TTS voices for a specific language', inputStructure: 'detailed', @@ -2230,7 +2290,7 @@ export const get35 = oc .output(zGetAppsByAppIdTextToAudioVoicesResponse) export const voices = { - get: get35, + get: get37, } /** @@ -2260,7 +2320,7 @@ export const textToAudio = { * * Get app tracing configuration */ -export const get36 = oc +export const get38 = oc .route({ description: 'Get app tracing configuration', inputStructure: 'detailed', @@ -2289,7 +2349,7 @@ export const post35 = oc .output(zPostAppsByAppIdTraceResponse) export const trace = { - get: get36, + get: get38, post: post35, } @@ -2320,7 +2380,7 @@ export const delete8 = oc /** * Get tracing configuration for an application */ -export const get37 = oc +export const get39 = oc .route({ description: 'Get tracing configuration for an application', inputStructure: 'detailed', @@ -2377,7 +2437,7 @@ export const post36 = oc export const traceConfig = { delete: delete8, - get: get37, + get: get39, patch, post: post36, } @@ -2409,7 +2469,7 @@ export const triggerEnable = { /** * Get app triggers list */ -export const get38 = oc +export const get40 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2422,7 +2482,7 @@ export const get38 = oc .output(zGetAppsByAppIdTriggersResponse) export const triggers = { - get: get38, + get: get40, } /** @@ -2430,7 +2490,7 @@ export const triggers = { * * Get workflow application execution logs */ -export const get39 = oc +export const get41 = oc .route({ description: 'Get workflow application execution logs', inputStructure: 'detailed', @@ -2449,7 +2509,7 @@ export const get39 = oc .output(zGetAppsByAppIdWorkflowAppLogsResponse) export const workflowAppLogs = { - get: get39, + get: get41, } /** @@ -2457,7 +2517,7 @@ export const workflowAppLogs = { * * Get workflow archived execution logs */ -export const get40 = oc +export const get42 = oc .route({ description: 'Get workflow archived execution logs', inputStructure: 'detailed', @@ -2476,7 +2536,7 @@ export const get40 = oc .output(zGetAppsByAppIdWorkflowArchivedLogsResponse) export const workflowArchivedLogs = { - get: get40, + get: get42, } /** @@ -2484,7 +2544,7 @@ export const workflowArchivedLogs = { * * Get workflow runs count statistics */ -export const get41 = oc +export const get43 = oc .route({ description: 'Get workflow runs count statistics', inputStructure: 'detailed', @@ -2503,7 +2563,7 @@ export const get41 = oc .output(zGetAppsByAppIdWorkflowRunsCountResponse) export const count3 = { - get: get41, + get: get43, } /** @@ -2539,7 +2599,7 @@ export const tasks = { /** * Generate a download URL for an archived workflow run. */ -export const get42 = oc +export const get44 = oc .route({ description: 'Generate a download URL for an archived workflow run.', inputStructure: 'detailed', @@ -2552,7 +2612,7 @@ export const get42 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdExportResponse) export const export4 = { - get: get42, + get: get44, } /** @@ -2560,7 +2620,7 @@ export const export4 = { * * Get workflow run node execution list */ -export const get43 = oc +export const get45 = oc .route({ description: 'Get workflow run node execution list', inputStructure: 'detailed', @@ -2574,7 +2634,7 @@ export const get43 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdNodeExecutionsResponse) export const nodeExecutions = { - get: get43, + get: get45, } /** @@ -2582,7 +2642,7 @@ export const nodeExecutions = { * * Get workflow run detail */ -export const get44 = oc +export const get46 = oc .route({ description: 'Get workflow run detail', inputStructure: 'detailed', @@ -2596,7 +2656,7 @@ export const get44 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdResponse) export const byRunId = { - get: get44, + get: get46, export: export4, nodeExecutions, } @@ -2604,7 +2664,7 @@ export const byRunId = { /** * Read a text/binary preview file in a workflow Agent node sandbox */ -export const get45 = oc +export const get47 = oc .route({ description: 'Read a text/binary preview file in a workflow Agent node sandbox', inputStructure: 'detailed', @@ -2622,7 +2682,7 @@ export const get45 = oc .output(zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesReadResponse) export const read = { - get: get45, + get: get47, } /** @@ -2652,7 +2712,7 @@ export const upload2 = { /** * List a directory in a workflow Agent node sandbox */ -export const get46 = oc +export const get48 = oc .route({ description: 'List a directory in a workflow Agent node sandbox', inputStructure: 'detailed', @@ -2671,7 +2731,7 @@ export const get46 = oc .output(zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesResponse) export const files3 = { - get: get46, + get: get48, read, upload: upload2, } @@ -2697,7 +2757,7 @@ export const byWorkflowRunId = { * * Get workflow run list */ -export const get47 = oc +export const get49 = oc .route({ description: 'Get workflow run list', inputStructure: 'detailed', @@ -2716,7 +2776,7 @@ export const get47 = oc .output(zGetAppsByAppIdWorkflowRunsResponse) export const workflowRuns2 = { - get: get47, + get: get49, count: count3, tasks, byRunId, @@ -2728,7 +2788,7 @@ export const workflowRuns2 = { * * Get all users in current tenant for mentions */ -export const get48 = oc +export const get50 = oc .route({ description: 'Get all users in current tenant for mentions', inputStructure: 'detailed', @@ -2742,7 +2802,7 @@ export const get48 = oc .output(zGetAppsByAppIdWorkflowCommentsMentionUsersResponse) export const mentionUsers = { - get: get48, + get: get50, } /** @@ -2867,7 +2927,7 @@ export const delete10 = oc * * Get a specific workflow comment */ -export const get49 = oc +export const get51 = oc .route({ description: 'Get a specific workflow comment', inputStructure: 'detailed', @@ -2905,7 +2965,7 @@ export const put3 = oc export const byCommentId = { delete: delete10, - get: get49, + get: get51, put: put3, replies, resolve, @@ -2916,7 +2976,7 @@ export const byCommentId = { * * Get all comments for a workflow */ -export const get50 = oc +export const get52 = oc .route({ description: 'Get all comments for a workflow', inputStructure: 'detailed', @@ -2954,7 +3014,7 @@ export const post42 = oc .output(zPostAppsByAppIdWorkflowCommentsResponse) export const comments = { - get: get50, + get: get52, post: post42, mentionUsers, byCommentId, @@ -2963,7 +3023,7 @@ export const comments = { /** * Get workflow average app interaction statistics */ -export const get51 = oc +export const get53 = oc .route({ description: 'Get workflow average app interaction statistics', inputStructure: 'detailed', @@ -2981,13 +3041,13 @@ export const get51 = oc .output(zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsResponse) export const averageAppInteractions = { - get: get51, + get: get53, } /** * Get workflow daily runs statistics */ -export const get52 = oc +export const get54 = oc .route({ description: 'Get workflow daily runs statistics', inputStructure: 'detailed', @@ -3005,13 +3065,13 @@ export const get52 = oc .output(zGetAppsByAppIdWorkflowStatisticsDailyConversationsResponse) export const dailyConversations2 = { - get: get52, + get: get54, } /** * Get workflow daily terminals statistics */ -export const get53 = oc +export const get55 = oc .route({ description: 'Get workflow daily terminals statistics', inputStructure: 'detailed', @@ -3029,13 +3089,13 @@ export const get53 = oc .output(zGetAppsByAppIdWorkflowStatisticsDailyTerminalsResponse) export const dailyTerminals = { - get: get53, + get: get55, } /** * Get workflow daily token cost statistics */ -export const get54 = oc +export const get56 = oc .route({ description: 'Get workflow daily token cost statistics', inputStructure: 'detailed', @@ -3053,7 +3113,7 @@ export const get54 = oc .output(zGetAppsByAppIdWorkflowStatisticsTokenCostsResponse) export const tokenCosts2 = { - get: get54, + get: get56, } export const statistics2 = { @@ -3073,7 +3133,7 @@ export const workflow = { * * Get default block configuration by type */ -export const get55 = oc +export const get57 = oc .route({ description: 'Get default block configuration by type', inputStructure: 'detailed', @@ -3092,7 +3152,7 @@ export const get55 = oc .output(zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeResponse) export const byBlockType = { - get: get55, + get: get57, } /** @@ -3100,7 +3160,7 @@ export const byBlockType = { * * Get default block configurations for workflow */ -export const get56 = oc +export const get58 = oc .route({ description: 'Get default block configurations for workflow', inputStructure: 'detailed', @@ -3114,14 +3174,14 @@ export const get56 = oc .output(zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsResponse) export const defaultWorkflowBlockConfigs = { - get: get56, + get: get58, byBlockType, } /** * Get conversation variables for workflow */ -export const get57 = oc +export const get59 = oc .route({ description: 'Get conversation variables for workflow', inputStructure: 'detailed', @@ -3154,7 +3214,7 @@ export const post43 = oc .output(zPostAppsByAppIdWorkflowsDraftConversationVariablesResponse) export const conversationVariables2 = { - get: get57, + get: get59, post: post43, } @@ -3163,7 +3223,7 @@ export const conversationVariables2 = { * * Get environment variables for workflow */ -export const get58 = oc +export const get60 = oc .route({ description: 'Get environment variables for workflow', inputStructure: 'detailed', @@ -3197,7 +3257,7 @@ export const post44 = oc .output(zPostAppsByAppIdWorkflowsDraftEnvironmentVariablesResponse) export const environmentVariables = { - get: get58, + get: get60, post: post44, } @@ -3402,7 +3462,7 @@ export const loop2 = { nodes: nodes6, } -export const get59 = oc +export const get61 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3416,7 +3476,7 @@ export const get59 = oc .output(zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponse) export const candidates = { - get: get59, + get: get61, } export const post51 = oc @@ -3479,7 +3539,7 @@ export const validate = { post: post53, } -export const get60 = oc +export const get62 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3507,7 +3567,7 @@ export const put4 = oc .output(zPutAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerResponse) export const agentComposer = { - get: get60, + get: get62, put: put4, candidates, impact, @@ -3518,7 +3578,7 @@ export const agentComposer = { /** * Get last run result for draft workflow node */ -export const get61 = oc +export const get63 = oc .route({ description: 'Get last run result for draft workflow node', inputStructure: 'detailed', @@ -3531,7 +3591,7 @@ export const get61 = oc .output(zGetAppsByAppIdWorkflowsDraftNodesByNodeIdLastRunResponse) export const lastRun = { - get: get61, + get: get63, } /** @@ -3606,7 +3666,7 @@ export const delete11 = oc /** * Get variables for a specific node */ -export const get62 = oc +export const get64 = oc .route({ description: 'Get variables for a specific node', inputStructure: 'detailed', @@ -3620,7 +3680,7 @@ export const get62 = oc export const variables = { delete: delete11, - get: get62, + get: get64, } export const byNodeId8 = { @@ -3665,7 +3725,7 @@ export const run10 = { /** * Server-Sent Events stream of inspector deltas for a draft workflow run. */ -export const get63 = oc +export const get65 = oc .route({ description: 'Server-Sent Events stream of inspector deltas for a draft workflow run.', inputStructure: 'detailed', @@ -3678,13 +3738,13 @@ export const get63 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsEventsResponse) export const events = { - get: get63, + get: get65, } /** * Full value for one declared output, including signed download URL for files. */ -export const get64 = oc +export const get66 = oc .route({ description: 'Full value for one declared output, including signed download URL for files.', inputStructure: 'detailed', @@ -3701,7 +3761,7 @@ export const get64 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdByOutputNamePreviewResponse) export const preview4 = { - get: get64, + get: get66, } export const byOutputName = { @@ -3711,7 +3771,7 @@ export const byOutputName = { /** * One node's declared outputs for a draft workflow run. */ -export const get65 = oc +export const get67 = oc .route({ description: 'One node\'s declared outputs for a draft workflow run.', inputStructure: 'detailed', @@ -3724,14 +3784,14 @@ export const get65 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdResponse) export const byNodeId9 = { - get: get65, + get: get67, byOutputName, } /** * Snapshot of every node's declared outputs for a draft workflow run. */ -export const get66 = oc +export const get68 = oc .route({ description: 'Snapshot of every node\'s declared outputs for a draft workflow run.', inputStructure: 'detailed', @@ -3744,7 +3804,7 @@ export const get66 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsResponse) export const nodeOutputs = { - get: get66, + get: get68, events, byNodeId: byNodeId9, } @@ -3760,7 +3820,7 @@ export const runs = { /** * Get system variables for workflow */ -export const get67 = oc +export const get69 = oc .route({ description: 'Get system variables for workflow', inputStructure: 'detailed', @@ -3773,7 +3833,7 @@ export const get67 = oc .output(zGetAppsByAppIdWorkflowsDraftSystemVariablesResponse) export const systemVariables = { - get: get67, + get: get69, } /** @@ -3873,7 +3933,7 @@ export const delete12 = oc /** * Get a specific workflow variable */ -export const get68 = oc +export const get70 = oc .route({ description: 'Get a specific workflow variable', inputStructure: 'detailed', @@ -3907,7 +3967,7 @@ export const patch2 = oc export const byVariableId = { delete: delete12, - get: get68, + get: get70, patch: patch2, reset, } @@ -3933,7 +3993,7 @@ export const delete13 = oc * * Get draft workflow variables */ -export const get69 = oc +export const get71 = oc .route({ description: 'Get draft workflow variables', inputStructure: 'detailed', @@ -3953,7 +4013,7 @@ export const get69 = oc export const variables2 = { delete: delete13, - get: get69, + get: get71, byVariableId, } @@ -3962,7 +4022,7 @@ export const variables2 = { * * Get draft workflow for an application */ -export const get70 = oc +export const get72 = oc .route({ description: 'Get draft workflow for an application', inputStructure: 'detailed', @@ -3999,7 +4059,7 @@ export const post59 = oc .output(zPostAppsByAppIdWorkflowsDraftResponse) export const draft2 = { - get: get70, + get: get72, post: post59, conversationVariables: conversationVariables2, environmentVariables, @@ -4020,7 +4080,7 @@ export const draft2 = { * * Get published workflow for an application */ -export const get71 = oc +export const get73 = oc .route({ description: 'Get published workflow for an application', inputStructure: 'detailed', @@ -4054,14 +4114,14 @@ export const post60 = oc .output(zPostAppsByAppIdWorkflowsPublishResponse) export const publish = { - get: get71, + get: get73, post: post60, } /** * Server-Sent Events stream of inspector deltas for a published workflow run. */ -export const get72 = oc +export const get74 = oc .route({ description: 'Server-Sent Events stream of inspector deltas for a published workflow run.', inputStructure: 'detailed', @@ -4074,13 +4134,13 @@ export const get72 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsEventsResponse) export const events2 = { - get: get72, + get: get74, } /** * Full value for one declared output of a published run. */ -export const get73 = oc +export const get75 = oc .route({ description: 'Full value for one declared output of a published run.', inputStructure: 'detailed', @@ -4101,7 +4161,7 @@ export const get73 = oc ) export const preview5 = { - get: get73, + get: get75, } export const byOutputName2 = { @@ -4111,7 +4171,7 @@ export const byOutputName2 = { /** * One node's declared outputs for a published workflow run. */ -export const get74 = oc +export const get76 = oc .route({ description: 'One node\'s declared outputs for a published workflow run.', inputStructure: 'detailed', @@ -4124,14 +4184,14 @@ export const get74 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdResponse) export const byNodeId10 = { - get: get74, + get: get76, byOutputName: byOutputName2, } /** * Snapshot of every node's declared outputs for a published workflow run. */ -export const get75 = oc +export const get77 = oc .route({ description: 'Snapshot of every node\'s declared outputs for a published workflow run.', inputStructure: 'detailed', @@ -4144,7 +4204,7 @@ export const get75 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsResponse) export const nodeOutputs2 = { - get: get75, + get: get77, events: events2, byNodeId: byNodeId10, } @@ -4164,7 +4224,7 @@ export const published = { /** * Get webhook trigger for a node */ -export const get76 = oc +export const get78 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4182,7 +4242,7 @@ export const get76 = oc .output(zGetAppsByAppIdWorkflowsTriggersWebhookResponse) export const webhook = { - get: get76, + get: get78, } export const triggers2 = { @@ -4258,7 +4318,7 @@ export const byWorkflowId = { * * Get all published workflows for an application */ -export const get77 = oc +export const get79 = oc .route({ description: 'Get all published workflows for an application', inputStructure: 'detailed', @@ -4277,7 +4337,7 @@ export const get77 = oc .output(zGetAppsByAppIdWorkflowsResponse) export const workflows3 = { - get: get77, + get: get79, defaultWorkflowBlockConfigs, draft: draft2, publish, @@ -4310,7 +4370,7 @@ export const delete15 = oc * * Get application details */ -export const get78 = oc +export const get80 = oc .route({ description: 'Get application details', inputStructure: 'detailed', @@ -4343,7 +4403,7 @@ export const put6 = oc export const byAppId2 = { delete: delete15, - get: get78, + get: get80, put: put6, advancedChat, agent, @@ -4412,7 +4472,7 @@ export const byApiKeyId = { * * Get all API keys for an app */ -export const get79 = oc +export const get81 = oc .route({ description: 'Get all API keys for an app', inputStructure: 'detailed', @@ -4445,7 +4505,7 @@ export const post62 = oc .output(zPostAppsByResourceIdApiKeysResponse) export const apiKeys = { - get: get79, + get: get81, post: post62, byApiKeyId, } @@ -4457,7 +4517,7 @@ export const byResourceId = { /** * Refresh MCP server configuration and regenerate server code */ -export const get80 = oc +export const get82 = oc .route({ description: 'Refresh MCP server configuration and regenerate server code', inputStructure: 'detailed', @@ -4470,7 +4530,7 @@ export const get80 = oc .output(zGetAppsByServerIdServerRefreshResponse) export const refresh = { - get: get80, + get: get82, } export const server2 = { @@ -4486,7 +4546,7 @@ export const byServerId = { * * Get list of applications with pagination and filtering */ -export const get81 = oc +export const get83 = oc .route({ description: 'Get list of applications with pagination and filtering', inputStructure: 'detailed', @@ -4519,7 +4579,7 @@ export const post63 = oc .output(zPostAppsResponse) export const apps = { - get: get81, + get: get83, post: post63, imports, starred, diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 904252c77eb..a3ab6a37c56 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -193,6 +193,29 @@ export type AgentDrivePreviewResponse = { truncated: boolean } +export type AgentDriveSkillListResponse = { + items?: Array +} + +export type AgentDriveSkillInspectResponse = { + archive_key?: string | null + created_at?: number | null + description: string + file_tree?: Array<{ + [key: string]: unknown + }> + files?: Array + hash?: string | null + mime_type?: string | null + name: string + path: string + size?: number | null + skill_md: AgentDriveSkillMarkdownResponse + skill_md_key: string + source: string + warnings?: Array +} + export type AgentDriveDeleteResponse = { config_version_id?: string | null removed_keys?: Array @@ -1274,9 +1297,39 @@ export type AgentDriveItemResponse = { created_at?: number | null file_kind: string hash?: string | null + is_skill?: boolean | null key: string mime_type?: string | null size?: number | null + skill_metadata?: string | null +} + +export type AgentDriveSkillItemResponse = { + archive_key?: string | null + created_at?: number | null + description: string + hash?: string | null + mime_type?: string | null + name: string + path: string + size?: number | null + skill_md_key: string +} + +export type AgentDriveSkillFileResponse = { + available_in_drive: boolean + drive_key?: string | null + name: string + path: string + type: string +} + +export type AgentDriveSkillMarkdownResponse = { + binary: boolean + key: string + size?: number | null + text?: string | null + truncated: boolean } export type AgentDriveFileResponse = { @@ -3125,6 +3178,44 @@ export type GetAppsByAppIdAgentDriveFilesPreviewResponses = { export type GetAppsByAppIdAgentDriveFilesPreviewResponse = GetAppsByAppIdAgentDriveFilesPreviewResponses[keyof GetAppsByAppIdAgentDriveFilesPreviewResponses] +export type GetAppsByAppIdAgentDriveSkillsData = { + body?: never + path: { + app_id: string + } + query?: { + node_id?: string + prefix?: string + } + url: '/apps/{app_id}/agent/drive/skills' +} + +export type GetAppsByAppIdAgentDriveSkillsResponses = { + 200: AgentDriveSkillListResponse +} + +export type GetAppsByAppIdAgentDriveSkillsResponse + = GetAppsByAppIdAgentDriveSkillsResponses[keyof GetAppsByAppIdAgentDriveSkillsResponses] + +export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectData = { + body?: never + path: { + app_id: string + skill_path: string + } + query?: { + node_id?: string + } + url: '/apps/{app_id}/agent/drive/skills/{skill_path}/inspect' +} + +export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses = { + 200: AgentDriveSkillInspectResponse +} + +export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse + = GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses[keyof GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses] + export type DeleteAppsByAppIdAgentFilesData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 4a6f397bcbd..be116b5c795 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -918,9 +918,11 @@ export const zAgentDriveItemResponse = z.object({ created_at: z.int().nullish(), file_kind: z.string(), hash: z.string().nullish(), + is_skill: z.boolean().nullish(), key: z.string(), mime_type: z.string().nullish(), size: z.int().nullish(), + skill_metadata: z.string().nullish(), }) /** @@ -930,6 +932,70 @@ export const zAgentDriveListResponse = z.object({ items: z.array(zAgentDriveItemResponse).optional(), }) +/** + * AgentDriveSkillItemResponse + */ +export const zAgentDriveSkillItemResponse = z.object({ + archive_key: z.string().nullish(), + created_at: z.int().nullish(), + description: z.string(), + hash: z.string().nullish(), + mime_type: z.string().nullish(), + name: z.string(), + path: z.string(), + size: z.int().nullish(), + skill_md_key: z.string(), +}) + +/** + * AgentDriveSkillListResponse + */ +export const zAgentDriveSkillListResponse = z.object({ + items: z.array(zAgentDriveSkillItemResponse).optional(), +}) + +/** + * AgentDriveSkillFileResponse + */ +export const zAgentDriveSkillFileResponse = z.object({ + available_in_drive: z.boolean(), + drive_key: z.string().nullish(), + name: z.string(), + path: z.string(), + type: z.string(), +}) + +/** + * AgentDriveSkillMarkdownResponse + */ +export const zAgentDriveSkillMarkdownResponse = z.object({ + binary: z.boolean(), + key: z.string(), + size: z.int().nullish(), + text: z.string().nullish(), + truncated: z.boolean(), +}) + +/** + * AgentDriveSkillInspectResponse + */ +export const zAgentDriveSkillInspectResponse = z.object({ + archive_key: z.string().nullish(), + created_at: z.int().nullish(), + description: z.string(), + file_tree: z.array(z.record(z.string(), z.unknown())).optional(), + files: z.array(zAgentDriveSkillFileResponse).optional(), + hash: z.string().nullish(), + mime_type: z.string().nullish(), + name: z.string(), + path: z.string(), + size: z.int().nullish(), + skill_md: zAgentDriveSkillMarkdownResponse, + skill_md_key: z.string(), + source: z.string(), + warnings: z.array(z.string()).optional(), +}) + /** * AgentDriveFileResponse */ @@ -3916,6 +3982,35 @@ export const zGetAppsByAppIdAgentDriveFilesPreviewQuery = z.object({ */ export const zGetAppsByAppIdAgentDriveFilesPreviewResponse = zAgentDrivePreviewResponse +export const zGetAppsByAppIdAgentDriveSkillsPath = z.object({ + app_id: z.uuid(), +}) + +export const zGetAppsByAppIdAgentDriveSkillsQuery = z.object({ + node_id: z.string().optional(), + prefix: z.string().optional().default(''), +}) + +/** + * Drive skills + */ +export const zGetAppsByAppIdAgentDriveSkillsResponse = zAgentDriveSkillListResponse + +export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectPath = z.object({ + app_id: z.uuid(), + skill_path: z.string(), +}) + +export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectQuery = z.object({ + node_id: z.string().optional(), +}) + +/** + * Drive skill inspect view + */ +export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse + = zAgentDriveSkillInspectResponse + export const zDeleteAppsByAppIdAgentFilesPath = z.object({ app_id: z.uuid(), }) From 6a5ddc751c970c8ab399440be5532f1b3cadc8bf Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:35:33 +0800 Subject: [PATCH 04/35] chore(deps): upgrade npm dependencies (#37731) --- package.json | 2 +- packages/dify-ui/package.json | 1 + .../src/button/__tests__/index.spec.tsx | 2 +- packages/dify-ui/tsconfig.json | 2 +- packages/dify-ui/vitest.config.ts | 1 + pnpm-lock.yaml | 3188 +++++++++-------- pnpm-workspace.yaml | 91 +- 7 files changed, 1671 insertions(+), 1616 deletions(-) diff --git a/package.json b/package.json index b9cb1274a2b..1b9c8c67954 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dify", "type": "module", "private": true, - "packageManager": "pnpm@11.6.0", + "packageManager": "pnpm@11.8.0", "devEngines": { "runtime": { "name": "node", diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index 0d2e4d47359..9a05be1fc3c 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -193,6 +193,7 @@ "@typescript/native-preview": "catalog:", "@vitejs/plugin-react": "catalog:", "@vitest/browser": "catalog:", + "@vitest/browser-playwright": "catalog:", "@vitest/coverage-v8": "catalog:", "class-variance-authority": "catalog:", "playwright": "catalog:", diff --git a/packages/dify-ui/src/button/__tests__/index.spec.tsx b/packages/dify-ui/src/button/__tests__/index.spec.tsx index 08d622eb9ec..2fe1022d08d 100644 --- a/packages/dify-ui/src/button/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/button/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import type * as React from 'react' +import { userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' -import { userEvent } from 'vitest/browser' import { Button } from '../index' const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement diff --git a/packages/dify-ui/tsconfig.json b/packages/dify-ui/tsconfig.json index 11474f08645..55080c646c8 100644 --- a/packages/dify-ui/tsconfig.json +++ b/packages/dify-ui/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@dify/tsconfig/react.json", "compilerOptions": { - "types": ["vite-plus/test/globals", "@vitest/browser/matchers"] + "types": ["vite-plus/test/globals", "vite-plus/test/matchers"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"], "exclude": ["node_modules", "dist", "storybook-static", "coverage"] diff --git a/packages/dify-ui/vitest.config.ts b/packages/dify-ui/vitest.config.ts index dfda908c563..d3ce7d88c02 100644 --- a/packages/dify-ui/vitest.config.ts +++ b/packages/dify-ui/vitest.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ '@base-ui/react/form', '@base-ui/react/merge-props', '@base-ui/react/use-render', + 'vite-plus/test/browser', ], }, test: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f5f3f59363..5257534fa49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ catalogs: specifier: 0.98.2 version: 0.98.2 '@hono/node-server': - specifier: 2.0.4 - version: 2.0.4 + specifier: 2.0.5 + version: 2.0.5 '@iconify-json/heroicons': specifier: 1.2.3 version: 1.2.3 @@ -109,8 +109,8 @@ catalogs: specifier: 1.14.6 version: 1.14.6 '@playwright/test': - specifier: 1.60.0 - version: 1.60.0 + specifier: 1.61.0 + version: 1.61.0 '@remixicon/react': specifier: 4.9.0 version: 4.9.0 @@ -118,35 +118,35 @@ catalogs: specifier: 4.2.0 version: 4.2.0 '@sentry/react': - specifier: 10.57.0 - version: 10.57.0 + specifier: 10.59.0 + version: 10.59.0 '@storybook/addon-a11y': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/addon-docs': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/addon-links': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/addon-onboarding': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/addon-themes': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/addon-vitest': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/nextjs-vite': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/react': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@storybook/react-vite': - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 '@streamdown/math': specifier: 1.0.2 version: 1.0.2 @@ -181,8 +181,8 @@ catalogs: specifier: 5.101.0 version: 5.101.0 '@tanstack/react-virtual': - specifier: 3.14.2 - version: 3.14.2 + specifier: 3.14.3 + version: 3.14.3 '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -196,14 +196,14 @@ catalogs: specifier: 14.6.1 version: 14.6.1 '@tsslint/cli': - specifier: 3.1.3 - version: 3.1.3 + specifier: 3.1.4 + version: 3.1.4 '@tsslint/compat-eslint': - specifier: 3.1.3 - version: 3.1.3 + specifier: 3.1.4 + version: 3.1.4 '@tsslint/config': - specifier: 3.1.3 - version: 3.1.3 + specifier: 3.1.4 + version: 3.1.4 '@types/js-cookie': specifier: 3.0.6 version: 3.0.6 @@ -217,8 +217,8 @@ catalogs: specifier: 0.6.4 version: 0.6.4 '@types/node': - specifier: 25.9.3 - version: 25.9.3 + specifier: 25.9.4 + version: 25.9.4 '@types/qs': specifier: 6.15.1 version: 6.15.1 @@ -232,14 +232,14 @@ catalogs: specifier: 1.15.9 version: 1.15.9 '@typescript-eslint/eslint-plugin': - specifier: 8.61.0 - version: 8.61.0 + specifier: 8.61.1 + version: 8.61.1 '@typescript-eslint/parser': - specifier: 8.61.0 - version: 8.61.0 + specifier: 8.61.1 + version: 8.61.1 '@typescript/native-preview': - specifier: 7.0.0-dev.20260613.1 - version: 7.0.0-dev.20260613.1 + specifier: 7.0.0-dev.20260620.1 + version: 7.0.0-dev.20260620.1 '@vitejs/plugin-react': specifier: 6.0.2 version: 6.0.2 @@ -247,11 +247,14 @@ catalogs: specifier: 0.5.27 version: 0.5.27 '@vitest/browser': - specifier: 4.1.8 - version: 4.1.8 + specifier: 4.1.9 + version: 4.1.9 + '@vitest/browser-playwright': + specifier: 4.1.9 + version: 4.1.9 '@vitest/coverage-v8': - specifier: 4.1.8 - version: 4.1.8 + specifier: 4.1.9 + version: 4.1.9 abcjs: specifier: 6.6.3 version: 6.6.3 @@ -280,8 +283,8 @@ catalogs: specifier: 1.1.1 version: 1.1.1 code-inspector-plugin: - specifier: 1.6.0 - version: 1.6.0 + specifier: 1.6.1 + version: 1.6.1 concurrently: specifier: ^10.0.3 version: 10.0.3 @@ -289,8 +292,8 @@ catalogs: specifier: 4.0.2 version: 4.0.2 cron-parser: - specifier: 5.5.0 - version: 5.5.0 + specifier: 5.6.0 + version: 5.6.0 dayjs: specifier: 1.11.21 version: 1.11.21 @@ -298,8 +301,8 @@ catalogs: specifier: 10.6.0 version: 10.6.0 dompurify: - specifier: 3.4.10 - version: 3.4.10 + specifier: 3.4.11 + version: 3.4.11 echarts: specifier: 6.1.0 version: 6.1.0 @@ -349,11 +352,11 @@ catalogs: specifier: 0.5.3 version: 0.5.3 eslint-plugin-sonarjs: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.1.0 + version: 4.1.0 eslint-plugin-storybook: - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 eventsource-parser: specifier: 3.1.0 version: 3.1.0 @@ -361,20 +364,20 @@ catalogs: specifier: 3.1.3 version: 3.1.3 foxact: - specifier: 0.3.5 - version: 0.3.5 + specifier: 0.3.7 + version: 0.3.7 fuse.js: specifier: 7.4.2 version: 7.4.2 happy-dom: - specifier: 20.10.3 - version: 20.10.3 + specifier: 20.10.6 + version: 20.10.6 hast-util-to-jsx-runtime: specifier: 2.3.6 version: 2.3.6 hono: - specifier: 4.12.25 - version: 4.12.25 + specifier: 4.12.26 + version: 4.12.26 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -418,8 +421,8 @@ catalogs: specifier: 0.17.0 version: 0.17.0 knip: - specifier: 6.16.1 - version: 6.16.1 + specifier: 6.17.1 + version: 6.17.1 ky: specifier: 2.0.2 version: 2.0.2 @@ -433,8 +436,8 @@ catalogs: specifier: 1.0.4 version: 1.0.4 loro-crdt: - specifier: 1.13.2 - version: 1.13.2 + specifier: 1.13.5 + version: 1.13.5 mermaid: specifier: 11.15.0 version: 11.15.0 @@ -472,8 +475,8 @@ catalogs: specifier: 3.28.1 version: 3.28.1 playwright: - specifier: 1.60.0 - version: 1.60.0 + specifier: 1.61.0 + version: 1.61.0 postcss: specifier: 8.5.15 version: 8.5.15 @@ -529,8 +532,8 @@ catalogs: specifier: 0.0.1 version: 0.0.1 sharp: - specifier: 0.35.1 - version: 0.35.1 + specifier: 0.35.2 + version: 0.35.2 shiki: specifier: 4.2.0 version: 4.2.0 @@ -544,8 +547,8 @@ catalogs: specifier: 1.0.8 version: 1.0.8 storybook: - specifier: 10.4.4 - version: 10.4.4 + specifier: 10.4.6 + version: 10.4.6 streamdown: specifier: 2.5.0 version: 2.5.0 @@ -559,8 +562,8 @@ catalogs: specifier: 4.3.1 version: 4.3.1 tldts: - specifier: 7.4.2 - version: 7.4.2 + specifier: 7.4.3 + version: 7.4.3 tsx: specifier: 4.22.4 version: 4.22.4 @@ -571,8 +574,8 @@ catalogs: specifier: 3.19.3 version: 3.19.3 undici: - specifier: 7.27.2 - version: 7.27.2 + specifier: 7.28.0 + version: 7.28.0 unist-util-visit: specifier: 5.1.0 version: 5.1.0 @@ -580,17 +583,17 @@ catalogs: specifier: 2.0.0 version: 2.0.0 uuid: - specifier: 14.0.0 - version: 14.0.0 + specifier: 14.0.1 + version: 14.0.1 vinext: - specifier: 0.1.2 - version: 0.1.2 + specifier: 0.1.6 + version: 0.1.6 vite-plugin-inspect: specifier: 12.0.0-beta.3 version: 12.0.0-beta.3 vite-plus: - specifier: 0.1.24 - version: 0.1.24 + specifier: 0.2.1 + version: 0.2.1 vitest-browser-react: specifier: 2.2.0 version: 2.2.0 @@ -608,29 +611,29 @@ catalogs: version: 5.0.14 overrides: + '@babel/core@<=7.29.0': ^7.29.1 '@lexical/code': npm:lexical-code-no-prism@0.41.0 canvas: ^3.2.3 esbuild@<0.27.2: 0.27.2 esbuild@>=0.17.0 <0.28.1: ^0.28.1 esbuild@>=0.27.3 <0.28.1: ^0.28.1 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 + js-yaml@<=4.1.1: ^4.1.2 picomatch@>=4.0.0 <4.0.4: 4.0.4 - postcss@<8.5.10: ^8.5.10 postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4 postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4 - rollup@>=4.0.0 <4.59.0: 4.61.1 + postcss@<8.5.10: ^8.5.10 + rollup@>=4.0.0 <4.59.0: 4.62.2 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 solid-js: 1.9.13 string-width: ~8.2.1 - vite: npm:@voidzero-dev/vite-plus-core@0.1.24 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 + tar@<=7.5.15: ^7.5.16 + vite: npm:@voidzero-dev/vite-plus-core@0.2.1 + vitest: 4.1.9 ws@>=8.0.0 <8.20.1: ^8.21.0 yaml@>=2.0.0 <2.8.3: 2.9.0 yauzl@<3.2.1: 3.2.1 - '@babel/core@<=7.29.0': ^7.29.1 - js-yaml@<=4.1.1: ^4.1.2 - tar@<=7.5.15: ^7.5.16 importers: @@ -638,7 +641,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 9.0.0(b376e15be293d4e014f0f69f32d1fb4a) + version: 9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)(vitest@4.1.9) concurrently: specifier: 'catalog:' version: 10.0.3 @@ -658,11 +661,11 @@ importers: specifier: runtime:^22.22.1 version: runtime:22.22.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) cli: dependencies: @@ -707,7 +710,7 @@ importers: version: 1.0.8 undici: specifier: 'catalog:' - version: 7.27.2 + version: 7.28.0 zod: specifier: 'catalog:' version: 4.4.3 @@ -717,7 +720,7 @@ importers: version: link:../packages/tsconfig '@hono/node-server': specifier: 'catalog:' - version: 2.0.4(hono@4.12.25) + version: 2.0.5(hono@4.12.26) '@types/js-yaml': specifier: 'catalog:' version: 4.0.9 @@ -726,31 +729,31 @@ importers: version: 1.0.4 '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24) + version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) eslint: specifier: 'catalog:' version: 10.5.0(jiti@2.7.0) hono: specifier: 'catalog:' - version: 4.12.25 + version: 4.12.26 typescript: specifier: 'catalog:' version: 6.0.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: 4.1.9 + version: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) e2e: devDependencies: @@ -762,13 +765,13 @@ importers: version: link:../packages/tsconfig '@playwright/test': specifier: 'catalog:' - version: 1.60.0 + version: 1.61.0 '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 tsx: specifier: 'catalog:' version: 4.22.4 @@ -776,11 +779,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) packages/contracts: dependencies: @@ -802,10 +805,10 @@ importers: version: 4.0.9 '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 eslint: specifier: 'catalog:' version: 10.5.0(jiti@2.7.0) @@ -817,13 +820,13 @@ importers: version: 6.0.3 vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) packages/dev-proxy: dependencies: '@hono/node-server': specifier: 'catalog:' - version: 2.0.4(hono@4.12.25) + version: 2.0.5(hono@4.12.26) c12: specifier: 'catalog:' version: 4.0.0-beta.5(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.2.0)(jiti@2.7.0)(magicast@0.5.2) @@ -832,26 +835,26 @@ importers: version: 5.0.0 hono: specifier: 'catalog:' - version: 4.12.25 + version: 4.12.26 devDependencies: '@dify/tsconfig': specifier: workspace:* version: link:../tsconfig '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: 4.1.9 + version: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) packages/dify-ui: dependencies: @@ -867,7 +870,7 @@ importers: version: 1.6.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.2.1(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 5.2.1(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@dify/tsconfig': specifier: workspace:* version: link:../tsconfig @@ -879,31 +882,31 @@ importers: version: 1.2.10 '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-links': specifier: 'catalog:' - version: 10.4.4(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.4(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(vitest@4.1.9) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 4.3.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) '@tanstack/react-hotkeys': specifier: 'catalog:' version: 0.10.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-virtual': specifier: 'catalog:' - version: 3.14.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/react': specifier: 'catalog:' version: 19.2.17 @@ -912,22 +915,25 @@ importers: version: 19.2.3(@types/react@19.2.17) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) '@vitest/browser': specifier: 'catalog:' - version: 4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24) + version: 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/browser-playwright': + specifier: 'catalog:' + version: 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(playwright@1.61.0)(vitest@4.1.9) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24) + version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) class-variance-authority: specifier: 'catalog:' version: 0.7.1 playwright: specifier: 'catalog:' - version: 1.60.0 + version: 1.61.0 react: specifier: 'catalog:' version: 19.2.7 @@ -936,7 +942,7 @@ importers: version: 19.2.7(react@19.2.7) storybook: specifier: 'catalog:' - version: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) tailwindcss: specifier: 'catalog:' version: 4.3.1 @@ -944,17 +950,17 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: 4.1.9 + version: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) vitest-browser-react: specifier: 'catalog:' - version: 2.2.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-test@0.1.24)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 2.2.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9) packages/iconify-collections: devDependencies: @@ -969,7 +975,7 @@ importers: dependencies: '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 typescript: specifier: 'catalog:' version: 6.0.3 @@ -979,13 +985,13 @@ importers: version: link:../tsconfig '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) packages/tsconfig: {} @@ -999,19 +1005,19 @@ importers: version: 10.0.1(eslint@10.5.0(jiti@2.7.0)) '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@typescript-eslint/eslint-plugin': specifier: 'catalog:' - version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + version: 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24) + version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) eslint: specifier: 'catalog:' version: 10.5.0(jiti@2.7.0) @@ -1019,14 +1025,14 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: 4.1.9 + version: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) web: dependencies: @@ -1092,7 +1098,7 @@ importers: version: 4.9.0(react@19.2.7) '@sentry/react': specifier: 'catalog:' - version: 10.57.0(react@19.2.7) + version: 10.59.0(react@19.2.7) '@streamdown/math': specifier: 'catalog:' version: 1.0.2(react@19.2.7) @@ -1119,7 +1125,7 @@ importers: version: 5.101.0(react@19.2.7) '@tanstack/react-virtual': specifier: 'catalog:' - version: 3.14.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) abcjs: specifier: 'catalog:' version: 6.6.3 @@ -1137,7 +1143,7 @@ importers: version: 4.0.2 cron-parser: specifier: 'catalog:' - version: 5.5.0 + version: 5.6.0 dayjs: specifier: 'catalog:' version: 1.11.21 @@ -1146,7 +1152,7 @@ importers: version: 10.6.0 dompurify: specifier: 'catalog:' - version: 3.4.10 + version: 3.4.11 echarts: specifier: 'catalog:' version: 6.1.0 @@ -1176,7 +1182,7 @@ importers: version: 3.1.3 foxact: specifier: 'catalog:' - version: 0.3.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 0.3.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fuse.js: specifier: 'catalog:' version: 7.4.2 @@ -1233,7 +1239,7 @@ importers: version: 0.45.0 loro-crdt: specifier: 'catalog:' - version: 1.13.2 + version: 1.13.5 mermaid: specifier: 'catalog:' version: 11.15.0 @@ -1251,13 +1257,13 @@ importers: version: 1.0.0 next: specifier: 'catalog:' - version: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) nuqs: specifier: 'catalog:' - version: 2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) pinyin-pro: specifier: 'catalog:' version: 3.28.1 @@ -1311,7 +1317,7 @@ importers: version: 0.0.1 sharp: specifier: 'catalog:' - version: 0.35.1 + version: 0.35.2 shiki: specifier: 'catalog:' version: 4.2.0 @@ -1332,7 +1338,7 @@ importers: version: 2.3.1 tldts: specifier: 'catalog:' - version: 7.4.2 + version: 7.4.3 unist-util-visit: specifier: 'catalog:' version: 5.1.0 @@ -1341,7 +1347,7 @@ importers: version: 2.0.0(react@19.2.7)(scheduler@0.27.0) uuid: specifier: 'catalog:' - version: 14.0.0 + version: 14.0.1 zod: specifier: 'catalog:' version: 4.4.3 @@ -1354,10 +1360,10 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@types/node@25.9.3)(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(happy-dom@20.10.3)(jiti@2.7.0)(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)(vitest@4.1.9) '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.2.1(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 5.2.1(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@dify/contracts': specifier: workspace:* version: link:../packages/contracts @@ -1405,28 +1411,28 @@ importers: version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-links': specifier: 'catalog:' - version: 10.4.4(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-onboarding': specifier: 'catalog:' - version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + version: 10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3) + version: 10.4.6(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3) '@storybook/react': specifier: 'catalog:' - version: 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) '@tailwindcss/postcss': specifier: 'catalog:' version: 4.3.1 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 4.3.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' version: 5.101.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) @@ -1444,13 +1450,13 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@tsslint/cli': specifier: 'catalog:' - version: 3.1.3(@tsslint/compat-eslint@3.1.3(typescript@6.0.3))(typescript@6.0.3) + version: 3.1.4(@tsslint/compat-eslint@3.1.4(typescript@6.0.3))(typescript@6.0.3) '@tsslint/compat-eslint': specifier: 'catalog:' - version: 3.1.3(typescript@6.0.3) + version: 3.1.4(typescript@6.0.3) '@tsslint/config': specifier: 'catalog:' - version: 3.1.3(@tsslint/compat-eslint@3.1.3(typescript@6.0.3)) + version: 3.1.4(@tsslint/compat-eslint@3.1.4(typescript@6.0.3)) '@types/js-cookie': specifier: 'catalog:' version: 3.0.6 @@ -1462,7 +1468,7 @@ importers: version: 0.6.4 '@types/node': specifier: 'catalog:' - version: 25.9.3 + version: 25.9.4 '@types/qs': specifier: 'catalog:' version: 6.15.1 @@ -1477,25 +1483,25 @@ importers: version: 1.15.9 '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260613.1 + version: 7.0.0-dev.20260620.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 0.5.27(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24) + version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) code-inspector-plugin: specifier: 'catalog:' - version: 1.6.0(supports-color@10.2.2) + version: 1.6.1(supports-color@10.2.2) eslint: specifier: 'catalog:' version: 10.5.0(jiti@2.7.0) @@ -1504,7 +1510,7 @@ importers: version: 0.11.0(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.6.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tailwindcss@4.3.1)(typescript@6.0.3) + version: 4.6.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tailwindcss@4.3.1)(typescript@6.0.3) eslint-plugin-hyoban: specifier: 'catalog:' version: 0.14.1(eslint@10.5.0(jiti@2.7.0)) @@ -1522,16 +1528,16 @@ importers: version: 0.5.3(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-sonarjs: specifier: 'catalog:' - version: 4.0.3(eslint@10.5.0(jiti@2.7.0)) + version: 4.1.0(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.4.4(eslint@10.5.0(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + version: 10.4.6(eslint@10.5.0(jiti@2.7.0))(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) happy-dom: specifier: 'catalog:' - version: 20.10.3 + version: 20.10.6 knip: specifier: 'catalog:' - version: 6.16.1 + version: 6.17.1 postcss: specifier: 'catalog:' version: 8.5.15 @@ -1540,7 +1546,7 @@ importers: version: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) storybook: specifier: 'catalog:' - version: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) tailwindcss: specifier: 'catalog:' version: 4.3.1 @@ -1555,22 +1561,22 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3) + version: 0.1.6(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.24 - version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@0.2.1 + version: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.0-beta.3(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) + version: 12.0.0-beta.3(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) vite-plus: specifier: 'catalog:' - version: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.24 - version: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + specifier: 4.1.9 + version: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) vitest-canvas-mock: specifier: 'catalog:' - version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.24) + version: 1.1.4(vitest@4.1.9) packages: @@ -1738,10 +1744,6 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} @@ -1750,10 +1752,6 @@ packages: resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} @@ -1768,10 +1766,6 @@ packages: peerDependencies: '@babel/core': ^7.29.1 - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -1792,10 +1786,6 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -1805,26 +1795,14 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -1883,23 +1861,23 @@ packages: resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} engines: {node: '>= 20.12.0'} - '@code-inspector/core@1.6.0': - resolution: {integrity: sha512-RMJA9RQVpU12L5Df32lhOC7kh0CwBbydaqr/dQmTEX9rjcr2cyEAalWSaglcMber6+iOHDZSiyaFIJCGKOSV3w==} + '@code-inspector/core@1.6.1': + resolution: {integrity: sha512-jlRjItyW7AlcEAi3oqxq11NsGdtfvG2ZOCs8mJQAlRhIKGEIWSECgN5oNzuOICOF+eRVHxUfBL4gd0FiFfaDuQ==} - '@code-inspector/esbuild@1.6.0': - resolution: {integrity: sha512-IDSOrQUaKDecklfd5wNqgU3j4TSlcqLspWw5+xGS8n5cFACy2kA4aM+vE4xWK3OXiooLB9sOGbVcOaZzda2hhA==} + '@code-inspector/esbuild@1.6.1': + resolution: {integrity: sha512-inZSNq+ZsbgZX48u/O9kwVNnr6RiMpBTlOhmfVx/bxCIs+GkGalVuY6AZ5+epg5QiNbL9RdHBkckEZpPAFDHgg==} - '@code-inspector/mako@1.6.0': - resolution: {integrity: sha512-99fPSBfbEiYMpZWHc2NSShzxDecNgn8ykziqeYjSqnNZTdCjhlEmMR/KymOMoI/Ott1wLM3H5fqa7Lpehx9NxQ==} + '@code-inspector/mako@1.6.1': + resolution: {integrity: sha512-mk8qfMOhEaGwvxruIudv2edZ5xaREaoMDmaiou4qEkfxfpiQ+8oKKGCQhNw8ML2wgVLUEeizCQO60IWpBJnWxg==} - '@code-inspector/turbopack@1.6.0': - resolution: {integrity: sha512-hhJGozLBa53NNi4Apn27Di0DAdfGzjFB5/iVsklQHKOxnGSJ7VviPO01Bi2W6sfOc6tY27wvsys1I1kc42KOLw==} + '@code-inspector/turbopack@1.6.1': + resolution: {integrity: sha512-4jY89hyU4p7DtTqEyr4qYnnaQSGvtZvrWr8PEaYYR0EDK+qHDl6ndupZBYU2zzKMv87gnWqZ271rPA+0YXfEBg==} - '@code-inspector/vite@1.6.0': - resolution: {integrity: sha512-P406JrDxZ8iGr26X3YzRwuR/jeYtrAhhjJu9BH0FpXRVcMolyp3a4kvMYzZ2IfulAxNMvcEAZsfr4DHLBFNktg==} + '@code-inspector/vite@1.6.1': + resolution: {integrity: sha512-ZlfS23t9naunoA8xBAlaEgEAFWq6YV1frKgE42fXZUNYOmcZlpaV/8EdqBJt/JZu6MJkceEVmgGRSJc5kYXIag==} - '@code-inspector/webpack@1.6.0': - resolution: {integrity: sha512-TVEYQ/hgWpyNTibUQh/nf/01ipDdP9/VGJJhL2eVpyBKlP4k8ipKusXaCEbC4hvUQsak6T11rwPQIXIf+WONRQ==} + '@code-inspector/webpack@1.6.1': + resolution: {integrity: sha512-pgrqzBja1FBk0WWXodmbfOnpLX4rbofBPk4INHc9UWnDlv1Ypfhn9pX7N5zJ0pBRHjrBF6ow4xfm0GGb7eUG2w==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -2362,8 +2340,8 @@ packages: '@hey-api/types@0.1.4': resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} - '@hono/node-server@2.0.4': - resolution: {integrity: sha512-Ut3y0dMMPWy6bZ2kVfx25EOVbZlm15dhF4mOsezMlhpNHy+4MkU1qN9Y6lnruYi4wPmFzimGX2X7LF/FwHli4A==} + '@hono/node-server@2.0.5': + resolution: {integrity: sha512-yQFvDmyDo3y6rEOJZDUYPJ49DIKTPpIk4kGvm40xx4Ejne0Pu9a1+exxPN+C1UppWK/WGZX9F++/Xs231tE86g==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -2412,8 +2390,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-darwin-arm64@0.35.1': - resolution: {integrity: sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==} + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] @@ -2424,14 +2402,14 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-darwin-x64@0.35.1': - resolution: {integrity: sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==} + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.1': - resolution: {integrity: sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==} + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} engines: {node: '>=20.9.0'} os: [freebsd] @@ -2440,8 +2418,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.0': - resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] @@ -2450,8 +2428,8 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.0': - resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} cpu: [x64] os: [darwin] @@ -2461,8 +2439,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm64@1.3.0': - resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} cpu: [arm64] os: [linux] libc: [glibc] @@ -2473,8 +2451,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.0': - resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} cpu: [arm] os: [linux] libc: [glibc] @@ -2485,8 +2463,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.0': - resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} cpu: [ppc64] os: [linux] libc: [glibc] @@ -2497,8 +2475,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.0': - resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} cpu: [riscv64] os: [linux] libc: [glibc] @@ -2509,8 +2487,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.0': - resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} cpu: [s390x] os: [linux] libc: [glibc] @@ -2521,8 +2499,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.0': - resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} cpu: [x64] os: [linux] libc: [glibc] @@ -2533,8 +2511,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': - resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} cpu: [arm64] os: [linux] libc: [musl] @@ -2545,8 +2523,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.0': - resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} cpu: [x64] os: [linux] libc: [musl] @@ -2558,8 +2536,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm64@0.35.1': - resolution: {integrity: sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==} + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -2572,8 +2550,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.1': - resolution: {integrity: sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==} + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] @@ -2586,8 +2564,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.1': - resolution: {integrity: sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==} + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] @@ -2600,8 +2578,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.1': - resolution: {integrity: sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==} + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] @@ -2614,8 +2592,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.1': - resolution: {integrity: sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==} + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] @@ -2628,8 +2606,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.1': - resolution: {integrity: sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==} + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -2642,8 +2620,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-arm64@0.35.1': - resolution: {integrity: sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==} + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -2656,8 +2634,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.1': - resolution: {integrity: sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==} + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -2668,12 +2646,12 @@ packages: engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-wasm32@0.35.1': - resolution: {integrity: sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==} + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.1': - resolution: {integrity: sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==} + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} engines: {node: '>=20.9.0'} cpu: [wasm32] @@ -2683,8 +2661,8 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-arm64@0.35.1': - resolution: {integrity: sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==} + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] @@ -2695,8 +2673,8 @@ packages: cpu: [ia32] os: [win32] - '@img/sharp-win32-ia32@0.35.1': - resolution: {integrity: sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==} + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] @@ -2707,8 +2685,8 @@ packages: cpu: [x64] os: [win32] - '@img/sharp-win32-x64@0.35.1': - resolution: {integrity: sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==} + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -2847,7 +2825,7 @@ packages: '@mdx-js/rollup@3.1.1': resolution: {integrity: sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.2 '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} @@ -3096,8 +3074,8 @@ packages: cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm-eabi@0.133.0': - resolution: {integrity: sha512-l/44caGse+VpnY9gx0yvvc5QnnG3yG1FO3KZgYvNL1GZrfK86zIwAOgGEVlxDyRymzrU/KHiblPFpevKOmJmUA==} + '@oxc-parser/binding-android-arm-eabi@0.135.0': + resolution: {integrity: sha512-sHeZItACNcA5WRAWqF6ixriR4GkZDyY10gVgnZU7pXku1DjHFATSqnwZM809jl0gXPHxb6fKzYQCK7bNK5cACQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] @@ -3114,8 +3092,8 @@ packages: cpu: [arm64] os: [android] - '@oxc-parser/binding-android-arm64@0.133.0': - resolution: {integrity: sha512-KUHmPMziLBp4u+zbrLdB7iWS7KshuZe+RAp7ELnY9SI9nNXBZ+dp8fiBqWOxhXqn+FQg3a4UcQhwmsJOKV8Jjg==} + '@oxc-parser/binding-android-arm64@0.135.0': + resolution: {integrity: sha512-wPte+SzgzWWFgMSF8YZDNM+tBXtJg0AXBi7+tU3yS2z1f2Af9kRLZLKuJojADmuD/cZexmnMHHC3SDItTW77Iw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -3132,8 +3110,8 @@ packages: cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-arm64@0.133.0': - resolution: {integrity: sha512-q8dWmnU/8ea2tga9w2f1PinQ5rcMPDUGkF64T189b65YMjUomET4oy5oRldOr4AwOQkneOG/Zttnz1Dvrc62wg==} + '@oxc-parser/binding-darwin-arm64@0.135.0': + resolution: {integrity: sha512-BmKz3lHIsqVos+9aPcdYCT9MG3APoUyM43KlEFhJMWNVDOGG8FKyiFz81Bc+mGz2o0hpuQ3PfXLfVWJrKXjo2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -3150,8 +3128,8 @@ packages: cpu: [x64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.133.0': - resolution: {integrity: sha512-cOKeIELIB2bJnCKwqx4Rdj+1Lss/U6uCbLxRySZrhyOOQa1flKhwZFjEHRHxk8fU1NKmhK5OnTdPQ4CpjuFuVw==} + '@oxc-parser/binding-darwin-x64@0.135.0': + resolution: {integrity: sha512-dM8BS+8+Br1fNvmh2QZbGiHaYttwLebRa6J4Uz9vuFzMNmvsdRYwf7993ptOaV0JTrR63AaoVLjX7nhWbijxjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -3168,8 +3146,8 @@ packages: cpu: [x64] os: [freebsd] - '@oxc-parser/binding-freebsd-x64@0.133.0': - resolution: {integrity: sha512-OpaSv4pW3KgFrMYQxTaS0aOE4T1DQF3qZE/4B6uqqv1KgPWWd4UQhJALi8PJPX1RRV5K7ThKXRfF7qGg2+3l1A==} + '@oxc-parser/binding-freebsd-x64@0.135.0': + resolution: {integrity: sha512-xlZnvvJdR9bGu2pOhvR5hMuKPHCE6Sa9owK5A484mzjHdm75VRV5nCs5w/jkmGODMMTFc+KN7EnZqEieM813kw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -3186,8 +3164,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-gnueabihf@0.133.0': - resolution: {integrity: sha512-JGK1wlGrGwxBIlVSF7KWTX1/ru6BEtf28fRROztDRkLfiW+Kxa4onnriezMIiogfn9hVw2KzYcKiLjkLR2ns8A==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.135.0': + resolution: {integrity: sha512-PSR8LmBK/H/PQRiN8g7RebQgZX/ntVCrdT/JBfNxE5ezdHG1s2i4rbazsRJYD83TTI1MmgTpC0MGL42PLtskQQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3204,8 +3182,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.133.0': - resolution: {integrity: sha512-yuZO533Ftonxn/iyoqQzURzLQHMspvsIyfiCSNi1t/ER4eIQaR0SsmUOUm5b/lmSig7IWIUa5/BrbEkAPwcilQ==} + '@oxc-parser/binding-linux-arm-musleabihf@0.135.0': + resolution: {integrity: sha512-I85GJXzfUsigkkk7Ngdz95C217M4FdUi1Z2HrX5UyPmURobwQZ7m2bbUvwFkz4VGZd+lymFGKHvDZ3RQC9qOzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3224,8 +3202,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-gnu@0.133.0': - resolution: {integrity: sha512-hvpbqT5pN2rR+3+xtWeizwfR/aZ0vGceg6TqYMl+ToxMpk9/tmnX7kSvQnfEUkoua8mhogzvIKsAkn0wxgblBA==} + '@oxc-parser/binding-linux-arm64-gnu@0.135.0': + resolution: {integrity: sha512-zqEY0npz0g0aGZj/8a5BclunjVDytsBQHYtIC10Gd26HcrLwbVF6YDbqRQjunMGYdSo97u6xOBl05aTDI2diDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3245,8 +3223,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-arm64-musl@0.133.0': - resolution: {integrity: sha512-wJQGamIosQBoJHW9+S5XxrtKRo3eyJxsnS1XCPrqN0LHi8uw1pTqqTfn3t/NVuvbBg7Pumn4ez9Eidgcn0xbEg==} + '@oxc-parser/binding-linux-arm64-musl@0.135.0': + resolution: {integrity: sha512-mWAfprP819gQ2qYst1RxgTI8b/z0b29OpoKfRflIXLHde2dZLihQD4g47Onuvtpo5GPIkMYPRlX9QoeZfs/GnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3266,8 +3244,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-ppc64-gnu@0.133.0': - resolution: {integrity: sha512-Koaz32/O5+abIfrNGdyndgRvdOZ9jEf5/z3Ep9h3h2QWpdDiUQpVwgH0OcMXCs+l9aXxPLtkupqyVig9W6FDKw==} + '@oxc-parser/binding-linux-ppc64-gnu@0.135.0': + resolution: {integrity: sha512-gri8c2AOmJKJwOux2KTHFBfUaXoJURuVMKhmKEi/2hTF55cQteTDV2XNfTiE5oCC+Tnem1Y4/MWzcyDadtsSag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -3287,8 +3265,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.133.0': - resolution: {integrity: sha512-R4vOjWzxhnNWHnVLeiB6jNuIifdy9vcMXZGPc7StXcxBovI+U2zg1QhZ9o8OjV80oGivs1lX5NfPLzk4IPqlRA==} + '@oxc-parser/binding-linux-riscv64-gnu@0.135.0': + resolution: {integrity: sha512-Y2tkupCG5wo0SxH2rMLG4d4Kmv6DaM3sBp+GuM5lox0S8Za6VxKgQrY2Mut088QQxKkEE89n/4CCCgmw2o0e3Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -3308,8 +3286,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-riscv64-musl@0.133.0': - resolution: {integrity: sha512-iwgBNUTHiMdxARLYuM0SBlnYeb19iw1Ea5M+4ERZupCsBMLArti6FyZ6UfFjJxIiTDr2oW2DGQFxlQVQ/dW9rA==} + '@oxc-parser/binding-linux-riscv64-musl@0.135.0': + resolution: {integrity: sha512-xDRJq6i6WTynjeP+ISbDpyH4p9BaJ0wuQcL0lCSDkt9qOXC9dmwpOu1VG/TlwmPI3KpYntmO9nJCuc3TMTsNBA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -3329,8 +3307,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-s390x-gnu@0.133.0': - resolution: {integrity: sha512-ZwZNo8FZmB/gVfboQl+wXilBigGl+6nQQs+nITOeAP/HcAOjiHl6XZJL9F/KXNEspODQcbjAiyjUbeCJd9a0fA==} + '@oxc-parser/binding-linux-s390x-gnu@0.135.0': + resolution: {integrity: sha512-V4MoUuiCRNvihxhIufRxvK+ka013V4joTSK0FAGA1KEjLuNprfH6N/Qw2uxQEVIFuNYMhD/hV6xJ/ptbzlKdHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -3350,8 +3328,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.133.0': - resolution: {integrity: sha512-govCvWx1dBlED3uu4qXctxpRcouu9I8Kn+DBktGCl760JtlGJzc9l/OmPJKlYWSbrRqKkMZehNeZ/4Wfma7uSA==} + '@oxc-parser/binding-linux-x64-gnu@0.135.0': + resolution: {integrity: sha512-JCFZ7zM7KXOKoPAbK/ZB4wY0M1jxRECiem2UQuiXLjzGqS9+hno7mtX+qyK2F7HWK2xPhyJb+frpcOtk5DKOtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3371,8 +3349,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-x64-musl@0.133.0': - resolution: {integrity: sha512-ssTlpXD5Mq9uCssDJPzlRWqBt4Y7Zzd9i+XZhWmK/9Y6KUIuAxVYTYiI8lxcGWi0+3/Cz4A8q9UrD4NK9Y2j7g==} + '@oxc-parser/binding-linux-x64-musl@0.135.0': + resolution: {integrity: sha512-9jSVS1b3hOV7sdKH4aA2DFfnTz0RgQd0v2BefR+LYbH8yIlmSM22JJZbAAjVeVXmFgUAk3zJQ1tpE/Nd+Vi2YQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3390,8 +3368,8 @@ packages: cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-openharmony-arm64@0.133.0': - resolution: {integrity: sha512-51aByfXhPtLEdWG4a2Ihdw6cPWV1ei1AarALpFdDP8MLWDLE2NuUMgbo3DERR2Kt8fT/ok1GUvBiLxVGke9uUQ==} + '@oxc-parser/binding-openharmony-arm64@0.135.0': + resolution: {integrity: sha512-M857ZLBSdn1Uy/SJJz5zh0qGu67B4P9omCgXGBU2LLqTzraX6ZjVNaKq5yW1PDw/LgJXDXR/dbZfgmB310f11Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -3406,8 +3384,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-parser/binding-wasm32-wasi@0.133.0': - resolution: {integrity: sha512-2e16tkKp+wDO2GTAmXfxbBcCmGEaFPIJEIRBBmVKNVXSc8/fJsSIaBGyFTPHM9ST5GNWgJcYIt94rDTks+PLwA==} + '@oxc-parser/binding-wasm32-wasi@0.135.0': + resolution: {integrity: sha512-2w6DVcntQZX9U5RhXtgiWb3FLWFB5EcwI1U8yr3htOCJUJjagN4BFUHz/Y/d9ZsumndZ6ByxxWEtbUZNE1bfFw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] @@ -3423,8 +3401,8 @@ packages: cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-arm64-msvc@0.133.0': - resolution: {integrity: sha512-KPTNDKbxH1cglrqTyVeXHb4Pk4oksz8EcE1/v8zqU7N4UXbiHfA/IwtXZ2U77fnRAWBbgVkl/lZbL7o3hRdejg==} + '@oxc-parser/binding-win32-arm64-msvc@0.135.0': + resolution: {integrity: sha512-rX1U8+IH2Z37EJjDXKa1iifvUQAdba+vZ4Ewj1iaG5eA/QaSybzclCOwtWa0/5BuUQnnK/T2JHUEFrwhL6Ck2Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -3441,8 +3419,8 @@ packages: cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.133.0': - resolution: {integrity: sha512-Una1bNYv9zCavQrfnDR9wuZVB3itLjCEH4Oz7i6CwAJN/Xq9b+zbbcxmvdkKvvJt4Ngc/MBmIYlbLo3zS4TQ0A==} + '@oxc-parser/binding-win32-ia32-msvc@0.135.0': + resolution: {integrity: sha512-9FAisBbH1QICGAjlJobiuKGd/jOuVmyqniWdQMwTa5SkCl6hhuotBCJf1n46B0flYbSOR5TzfV9HZCWSyb3c/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] @@ -3459,14 +3437,14 @@ packages: cpu: [x64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.133.0': - resolution: {integrity: sha512-kjBhCiOGSYTwDJQuuZa7a94JbP8htWu7J0X1KwH74kV2K5eYf6eyJRYmkpCDvr0XEL8tMxYI4WU1VekblFCLgg==} + '@oxc-parser/binding-win32-x64-msvc@0.135.0': + resolution: {integrity: sha512-wYF+A2AzJ2n7ul6q+Z2G/ia0S2+8cUp0AgWZzoFvF4WmUcl1P7p+o6se1Gdr5wGnWuF0iAMIkGddrjCarNr2yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-project/runtime@0.133.0': - resolution: {integrity: sha512-PkvjA1Lq5++V5S1E6Patr92ZVcieE6EalDr1VJTqv4BnjZdOUC4W3p8k1wMXSd5/2aFP4b/A6N5sg2Bkzcr9vQ==} + '@oxc-project/runtime@0.136.0': + resolution: {integrity: sha512-u0EutjK5y6NHJkl5jNJCs8zbup1z6A/UEWgajrYzqcEU3UX05HjqybhMQOLhSM0eKGISyM6WfSMMuklYSmH2wA==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.127.0': @@ -3475,8 +3453,11 @@ packages: '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.135.0': + resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} + + '@oxc-project/types@0.136.0': + resolution: {integrity: sha512-39Al/B3v9esnHCX7S8l9Se2+s2tb9b2jcMd+bZ2L659VG73kNyGPpPrL5Zi/p0ty7p4pTTU2/Dd+g27hv94XCg==} '@oxc-resolver/binding-android-arm-eabi@11.20.0': resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} @@ -3581,124 +3562,124 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.52.0': - resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==} + '@oxfmt/binding-android-arm-eabi@0.55.0': + resolution: {integrity: sha512-+rFDOqQe5LOWgxrAJaZgLRudr6GQm0wGI6gtu7vVkrdLGjNMUSGbAlaCr8j7F2H2Er97vYQCU8WDb30onqMM1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.52.0': - resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==} + '@oxfmt/binding-android-arm64@0.55.0': + resolution: {integrity: sha512-ctulLq8s3x8Zmvw6+iccB09TIKERAklRSmbJ10gk8mlAn05qZxoyo52dj3Hi9IJcmDSwF54fQaTVh2CbL6PInw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.52.0': - resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==} + '@oxfmt/binding-darwin-arm64@0.55.0': + resolution: {integrity: sha512-xDQczLH9pw/RBk1h/GH0qcGMm8hQtmtVHBNLSH3lk1gEIR09hZ4L+mJQl4VqiVAvPK9VG9PYrWWuSQLt7xTbiA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.52.0': - resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==} + '@oxfmt/binding-darwin-x64@0.55.0': + resolution: {integrity: sha512-JaNoFCkF2CJdGgpPSMbuO9HVyXyoNGIhMHPvp6NYAjeVKw9XEYc0HcUWJLPQa3Q69WV5wMa9m5jPMJPtbLtcRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.52.0': - resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==} + '@oxfmt/binding-freebsd-x64@0.55.0': + resolution: {integrity: sha512-DNbszhpg6S2MIzax5azdHFTTBIVkR5xr8yyRZuA4yoDAwOkzIp3tmldgKZM2+VlT+hJIG0xUksA+elISzMEAfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': - resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.55.0': + resolution: {integrity: sha512-2snoaoRfFFyGnbOcKUK36rREBYxe/Xgz3uHbiA5zbCB/s6R4DQj4mHqYAaWWhgizCUSDxV8cE9zAZ0XleNpKGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': - resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.55.0': + resolution: {integrity: sha512-q1aktHF/WRpSK81BX1dE/9vWrS2jGw1Nax2kb4DBLGAewubCLcoNyp4Zl/NSMgbv3vUS46Z33wIQkBVYOP3PYg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.52.0': - resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==} + '@oxfmt/binding-linux-arm64-gnu@0.55.0': + resolution: {integrity: sha512-VD0y36aENezl/3tsclA/4G53Cc7iV+7Uoh7gz4yvcOTaEYBtJpQsE6PKDGTtUtOvGS4kv51ybfXY/nWZejO5IA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.52.0': - resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==} + '@oxfmt/binding-linux-arm64-musl@0.55.0': + resolution: {integrity: sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': - resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==} + '@oxfmt/binding-linux-ppc64-gnu@0.55.0': + resolution: {integrity: sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': - resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==} + '@oxfmt/binding-linux-riscv64-gnu@0.55.0': + resolution: {integrity: sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.52.0': - resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==} + '@oxfmt/binding-linux-riscv64-musl@0.55.0': + resolution: {integrity: sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.52.0': - resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==} + '@oxfmt/binding-linux-s390x-gnu@0.55.0': + resolution: {integrity: sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.52.0': - resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==} + '@oxfmt/binding-linux-x64-gnu@0.55.0': + resolution: {integrity: sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.52.0': - resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==} + '@oxfmt/binding-linux-x64-musl@0.55.0': + resolution: {integrity: sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.52.0': - resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==} + '@oxfmt/binding-openharmony-arm64@0.55.0': + resolution: {integrity: sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.52.0': - resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==} + '@oxfmt/binding-win32-arm64-msvc@0.55.0': + resolution: {integrity: sha512-7p9FB5R32tw2KyyNX3wpQrR2WHwEHvMEiBlGXxeTCaRMCVNx3UtFMAUbaQ/pRNWIrEUZmYhJ6tcUH52uPTRYjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.52.0': - resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==} + '@oxfmt/binding-win32-ia32-msvc@0.55.0': + resolution: {integrity: sha512-ZYqj3fDnOT1IaVGMP5kpmkQl4F3tQIm2ZyAxvqkJYmI0xgWWak4ss4XYwv3VDfM+TWXeC9K4uQ/wW5jm/5XABA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.52.0': - resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==} + '@oxfmt/binding-win32-x64-msvc@0.55.0': + resolution: {integrity: sha512-eEYT5tivGnGbPHuOHuQpi6CGLObhh0re/5jcNQHihD2GRYkTM85dyi5a19zjP8Q00t1uqAx+/QGLUGdHeqzWyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3733,139 +3714,140 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.67.0': - resolution: {integrity: sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw==} + '@oxlint/binding-android-arm-eabi@1.70.0': + resolution: {integrity: sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.67.0': - resolution: {integrity: sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ==} + '@oxlint/binding-android-arm64@1.70.0': + resolution: {integrity: sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.67.0': - resolution: {integrity: sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg==} + '@oxlint/binding-darwin-arm64@1.70.0': + resolution: {integrity: sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.67.0': - resolution: {integrity: sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg==} + '@oxlint/binding-darwin-x64@1.70.0': + resolution: {integrity: sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.67.0': - resolution: {integrity: sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg==} + '@oxlint/binding-freebsd-x64@1.70.0': + resolution: {integrity: sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.67.0': - resolution: {integrity: sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g==} + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + resolution: {integrity: sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.67.0': - resolution: {integrity: sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw==} + '@oxlint/binding-linux-arm-musleabihf@1.70.0': + resolution: {integrity: sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.67.0': - resolution: {integrity: sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg==} + '@oxlint/binding-linux-arm64-gnu@1.70.0': + resolution: {integrity: sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.67.0': - resolution: {integrity: sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==} + '@oxlint/binding-linux-arm64-musl@1.70.0': + resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.67.0': - resolution: {integrity: sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==} + '@oxlint/binding-linux-ppc64-gnu@1.70.0': + resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.67.0': - resolution: {integrity: sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==} + '@oxlint/binding-linux-riscv64-gnu@1.70.0': + resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.67.0': - resolution: {integrity: sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==} + '@oxlint/binding-linux-riscv64-musl@1.70.0': + resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.67.0': - resolution: {integrity: sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==} + '@oxlint/binding-linux-s390x-gnu@1.70.0': + resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.67.0': - resolution: {integrity: sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==} + '@oxlint/binding-linux-x64-gnu@1.70.0': + resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.67.0': - resolution: {integrity: sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==} + '@oxlint/binding-linux-x64-musl@1.70.0': + resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.67.0': - resolution: {integrity: sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==} + '@oxlint/binding-openharmony-arm64@1.70.0': + resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.67.0': - resolution: {integrity: sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA==} + '@oxlint/binding-win32-arm64-msvc@1.70.0': + resolution: {integrity: sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.67.0': - resolution: {integrity: sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg==} + '@oxlint/binding-win32-ia32-msvc@1.70.0': + resolution: {integrity: sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.67.0': - resolution: {integrity: sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ==} + '@oxlint/binding-win32-x64-msvc@1.70.0': + resolution: {integrity: sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/plugins@1.61.0': - resolution: {integrity: sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ==} + '@oxlint/plugins@1.68.0': + resolution: {integrity: sha512-titLmukUt/h8ho7Svlf0xSBjoy2ccZKrXjpXpZCj+v6V4CJccC2KyP45BLSCMx8YIpifMyiDyUptM4+5sruKbQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.60.0': - resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + '@playwright/test@1.61.0': + resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} engines: {node: '>=18'} + hasBin: true '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -4122,7 +4104,7 @@ packages: resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.2 peerDependenciesMeta: rollup: optional: true @@ -4131,41 +4113,41 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.61.1 + rollup: 4.62.2 peerDependenciesMeta: rollup: optional: true - '@sentry-internal/browser-utils@10.57.0': - resolution: {integrity: sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A==} + '@sentry/browser-utils@10.59.0': + resolution: {integrity: sha512-DpJIrNi0Hsj/YONTZ8km1wv7ue2NzrTmFKJ3lRW8Q2nS/mrMeP3LCvjreLtKheTouB4go58NxM68AEFbXk4rPg==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.57.0': - resolution: {integrity: sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ==} + '@sentry/browser@10.59.0': + resolution: {integrity: sha512-d0o0oc78KNWCZ6yq9gREf9BPXWza/Wj9W+nCm0CvPD5k2IbouD/eJsm2Mb+d53YfEm12L+SePfgOx01KbbVx4A==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.57.0': - resolution: {integrity: sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A==} + '@sentry/core@10.59.0': + resolution: {integrity: sha512-QeG7XZL5j6CkToYCE7OwCerb/r742Tjj9p1BBohBKcypYTPRuqfD+A3FeUj7pk5CGO6Vj1/gOAmdbuuNbR51dQ==} engines: {node: '>=18'} - '@sentry-internal/replay@10.57.0': - resolution: {integrity: sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg==} + '@sentry/feedback@10.59.0': + resolution: {integrity: sha512-Sa/06LlG/mYR4z8/JlxOwvkcOHJnaMW6JyTMKNsgEoR1SHZULIpirjUuHIp4P+7R1mqX/KGTj8I7SQ3adA0TxQ==} engines: {node: '>=18'} - '@sentry/browser@10.57.0': - resolution: {integrity: sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA==} - engines: {node: '>=18'} - - '@sentry/core@10.57.0': - resolution: {integrity: sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==} - engines: {node: '>=18'} - - '@sentry/react@10.57.0': - resolution: {integrity: sha512-6QThwQ4XWQ2rwKZEVQ9P9WKl7JlowC7S5LpAvmMdrwlfJBpLDFOsM7tycnIvbXTXf0ZOOuLFPa4L4YYbdyNGmA==} + '@sentry/react@10.59.0': + resolution: {integrity: sha512-B3MmXooGLnTu0gyL8hxElCu1+WVEXjIGoDPvGqn5qMHZNSmB2oPTAt1/UutuxAc6EWu2UKIHnyPowNeY2MPH3g==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x + '@sentry/replay-canvas@10.59.0': + resolution: {integrity: sha512-bJkqvxQiat27P7+hUYC/m545Ct/uVxZCjh+PeCFdRcuHnZZ92e6Z7XPLf0LqAR6tR1U7wDUa4V5ugrMIEz+vpQ==} + engines: {node: '>=18'} + + '@sentry/replay@10.59.0': + resolution: {integrity: sha512-fpHL25JErsSfTyusuyZ3Q5JmRZeWYWynJO3PNiQH6A/58EqaCp8U5y8LCJ+CSPaeuBMka3S3Qn6U4eXcvkDMRA==} + engines: {node: '>=18'} + '@shikijs/core@4.2.0': resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} engines: {node: '>=20'} @@ -4200,6 +4182,7 @@ packages: '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} + hasBin: true '@sindresorhus/base62@1.0.0': resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} @@ -4214,50 +4197,50 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-a11y@10.4.4': - resolution: {integrity: sha512-/eUCx/6Ozq5grauwm/NqKtlW0oJ26b6GNesXrMuFID8WLg/qLEKf79Awfz9XrmyWxe7loD40K952r7AA5Oc23A==} + '@storybook/addon-a11y@10.4.6': + resolution: {integrity: sha512-XCJy+f0DFOiCgUU9knRDlLDxVFI+AAQ3/wE/NF85zB9iDPPS2DwkSN+mas3zDgHt66zhN8Cq3/UiyCDUweV9Zw==} peerDependencies: - storybook: ^10.4.4 + storybook: ^10.4.6 - '@storybook/addon-docs@10.4.4': - resolution: {integrity: sha512-yPshCvtmQTq52T2sXuXgjy7B/QbhA/WIZxLYggptNjBL8BJMvbOfp9bAfCKh7+KpRWGqDZ6Y6tWL1Q48Wj3vtw==} + '@storybook/addon-docs@10.4.6': + resolution: {integrity: sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 peerDependenciesMeta: '@types/react': optional: true - '@storybook/addon-links@10.4.4': - resolution: {integrity: sha512-sWydPWLgduT24p/NJ/hXHcHsPlAyzQP+cOtCGliSI989K9yBP/TOL3A8sz7LIDfukI9DVAsylPhJ1jDSiAEI1w==} + '@storybook/addon-links@10.4.6': + resolution: {integrity: sha512-VGfERTsGRFmfvNP3SKprFWkC6Od5kXzSutT5PSZjQ/O9NnCdHhd/RILxFDN2TzZn9ywDc7t5b4AldKmSYCv3EQ==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 peerDependenciesMeta: '@types/react': optional: true react: optional: true - '@storybook/addon-onboarding@10.4.4': - resolution: {integrity: sha512-ZTWGm8VXQUTepV4aEmIgHxdY7JMtn57H3uYnM3HD+qR8fmAcpLPoJ9ffXaMWUwsjK6SeferQyDTRN3q3Jd5+mg==} + '@storybook/addon-onboarding@10.4.6': + resolution: {integrity: sha512-BzTu5LW5Ygwv0BbmQM0QohGS5Uoa8GjSxPcZFCQ1iQfizUukKnd72UIsPDZBKjVytO0DbZ3YoM1Ww5+KL6ZXEg==} peerDependencies: - storybook: ^10.4.4 + storybook: ^10.4.6 - '@storybook/addon-themes@10.4.4': - resolution: {integrity: sha512-VH443z7o/JO5K9QFVuB9IzwaMu0jEiq4ybpzTlAmt0ZUEqNBuM+ESBvkVMkZ5QeNghKrs/J9yvum2g2t94YR4Q==} + '@storybook/addon-themes@10.4.6': + resolution: {integrity: sha512-80d622oB9xWZs3VH4uywkLOA5L2DAx04lVouvCM4XH+pLnJElidoylOLm3i3ByvlGkRjCbB27OUVsW94IgyDrw==} peerDependencies: - storybook: ^10.4.4 + storybook: ^10.4.6 - '@storybook/addon-vitest@10.4.4': - resolution: {integrity: sha512-VPpBwf1Elr+0g33am8ZE6aHhLB+r1TPxUsnDuCVNhxGjRxMFyQkAE8+jPJFPvS/YIUGMbVXarzaV7PcI/sJuVQ==} + '@storybook/addon-vitest@10.4.6': + resolution: {integrity: sha512-VvskHge0GZy86LG6kcY5Ww34z8rDV8JBxqSdUpcJVsWfIvyX6MfAbqI76LlereSyBIJGZJZsqaLwRXsQoVY+0Q==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.4.4 - vitest: ^3.0.0 || ^4.0.0 + storybook: ^10.4.6 + vitest: 4.1.9 peerDependenciesMeta: '@vitest/browser': optional: true @@ -4268,18 +4251,18 @@ packages: vitest: optional: true - '@storybook/builder-vite@10.4.4': - resolution: {integrity: sha512-VyuZ4mEvhhVXjJa1qXMWKH8ohnas0rgEuJDf6u4aJ54XeENFebPUEAHde1Qo2PflJ4rUdVdXieOZzKbYwP5RAQ==} + '@storybook/builder-vite@10.4.6': + resolution: {integrity: sha512-BHBtD81HiXUiDQz/CaFynLtWmm7AFUQn8VnXuHipZ8KlnUANopa4yqdVuy/Gwz8ub254uFI5NMZsW/KlgWNgNg==} peerDependencies: - storybook: ^10.4.4 + storybook: ^10.4.6 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.4.4': - resolution: {integrity: sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w==} + '@storybook/csf-plugin@10.4.6': + resolution: {integrity: sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==} peerDependencies: esbuild: ^0.28.1 - rollup: 4.61.1 - storybook: ^10.4.4 + rollup: 4.62.2 + storybook: ^10.4.6 vite: '*' webpack: '*' peerDependenciesMeta: @@ -4301,15 +4284,15 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/nextjs-vite@10.4.4': - resolution: {integrity: sha512-pmihRfZSNLG6w+jQuqwv+vlC8JWGWfBnlHIFrle9HpNxqJDeKxKfjjDAjjgB+LUgbFz348TXbjzkZjJc5hFTOQ==} + '@storybook/nextjs-vite@10.4.6': + resolution: {integrity: sha512-o8vkNqJPY0oq5qGAcwjiyZoZUsfhk7eIU1mjgtYbNoJhA1/NshU7pIaFSTfFjMRs1i43Df8hqS5JmyjP8r+bUg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 next: ^14.1.0 || ^15.0.0 || ^16.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 typescript: '*' vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: @@ -4320,36 +4303,36 @@ packages: typescript: optional: true - '@storybook/react-dom-shim@10.4.4': - resolution: {integrity: sha512-y6SObmoW78AydE6VfKQSUmCkuqiaMPy9LgMpMdMEyWfJ/pSxBDMIKycr9dlRMJP1cvNgByaJgrusWtA46ndSQw==} + '@storybook/react-dom-shim@10.4.6': + resolution: {integrity: sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@storybook/react-vite@10.4.4': - resolution: {integrity: sha512-hXw1c9Jq2eFzwmJ3u9phmszbHoPjwPLYjcR1Grd6Xbe2g3bReGH35urm/fTZ0HNdjXAgQlUaXp2bWw6vz0BHQw==} + '@storybook/react-vite@10.4.6': + resolution: {integrity: sha512-0arEQtybqGYXHbXpTot+Wv9YtG+V5Vp43QayXavPKQ20M8mpEzhyCPKd0EhqMGSC1Z1UEt0hm365WUBhI9LfKA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.4.4': - resolution: {integrity: sha512-6K5/uHrvjswrueyVpUt6IWGuSgYCMtMOYyVs86XJZYqKBV3Pv7nGsGNH7YSMLAVQBZW4CQqm2etd5Op0GHY9Kg==} + '@storybook/react@10.4.6': + resolution: {integrity: sha512-9Y7YecrVFe1/01KYjfOLxVqTg2Aq+IO6TEv6sC2U0PfD0AWCSCmQ91QqgBpN/XW4aFFWoiZNinyXMUlU8zxy2w==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.4.4 + storybook: ^10.4.6 typescript: '>= 4.9.x' peerDependenciesMeta: '@types/react': @@ -4566,8 +4549,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-virtual@3.14.2': - resolution: {integrity: sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==} + '@tanstack/react-virtual@3.14.3': + resolution: {integrity: sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4575,8 +4558,8 @@ packages: '@tanstack/store@0.11.0': resolution: {integrity: sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==} - '@tanstack/virtual-core@3.17.0': - resolution: {integrity: sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==} + '@tanstack/virtual-core@3.17.1': + resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==} '@teppeis/multimaps@3.0.0': resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} @@ -4611,20 +4594,20 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tsslint/cli@3.1.3': - resolution: {integrity: sha512-W+SZhIewVWG2aF51TkPv4EwY2PCYQzsUgRXmjtamYRs06QWJn7X67bf+ZroZMGTg2eeajy6+LKGWV39KGsDFOQ==} + '@tsslint/cli@3.1.4': + resolution: {integrity: sha512-svoLfFkoWmdsDrIRLllFnrxydfMjKKZ1UBjv7Sua1KjFkx6VaJ88+YGYqNiTbB/dDcU10qnSMXRavNTJ0fjBkQ==} engines: {node: '>=22.6.0'} hasBin: true peerDependencies: typescript: '*' - '@tsslint/compat-eslint@3.1.3': - resolution: {integrity: sha512-3dH4nocZS/WY+a8N0TGawISEmAMxWOqEqX0YD18eTbOo9YBHVAhrQmPFA2AoEeBxR5+N8xbVYxVJ1b6K8yE8CA==} + '@tsslint/compat-eslint@3.1.4': + resolution: {integrity: sha512-aMSOnAHC/sJFGM21I1/rFzwNLyfbW2BVRivsP1xDgRwrt3+WfLT/eVU2HMEtS8J6XVPNkQ4bKD4Uk1lRpYaNlA==} peerDependencies: typescript: '*' - '@tsslint/config@3.1.3': - resolution: {integrity: sha512-gquCvrctDv1n2T+gdja0jnoDm+U/mwwYU01iaNglt+MHO6VAZRyTJaxtu72IMTPwo73pOfiummFTHaLdifIXJQ==} + '@tsslint/config@3.1.4': + resolution: {integrity: sha512-6VcUimc170M1v3b0vmOhRW7NI/b7DqXB5Wpo+1wCNLprDTN1HwjsbfdFNm/nsd0jXWChNr/cJhmsnVp4xHDCKw==} engines: {node: '>=22.6.0'} hasBin: true peerDependencies: @@ -4636,12 +4619,12 @@ packages: tsl: optional: true - '@tsslint/core@3.1.3': - resolution: {integrity: sha512-X/XiTj3w4tZhy4yh/zvTR/Z0Nb68Ul/uiQGsdKE/ClUCK0Iu7wDpuO7PKH+yj16eg02+l7kD6ojhJKL7aXibaA==} + '@tsslint/core@3.1.4': + resolution: {integrity: sha512-C4bUPiZ6ZWemh/GIuH6RH+frGHLH7wh3HI3d4cEQiOUoICmFidEkdfq5YDI/Bfdeqol5Ezyp5GzyHaXq+wU2fA==} engines: {node: '>=22.6.0'} - '@tsslint/types@3.1.3': - resolution: {integrity: sha512-KskQ8bFj3DY0Esg8Fe9K3qZxiW7HOV3c+HuqwVGXR5s1LMmIooB2uG4Bb0Tpj7p5GMedhmfQjz3JoAWDa27FWA==} + '@tsslint/types@3.1.4': + resolution: {integrity: sha512-YMLUQwG/cGlvoCEg1WVAq0EmmoHOzJ/WkenKWP7tFfQnTk4wIyZgOMIQkvYBcREPin7OVbLlaAEOH9eTr8SUUA==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -4814,6 +4797,9 @@ packages: '@types/node@25.9.3': resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/node@25.9.4': + resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4866,6 +4852,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.61.0': resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4873,22 +4867,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.61.0': resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.61.0': resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.61.0': resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.61.0': resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4896,16 +4913,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.61.0': resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.61.0': resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.61.0': resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4913,54 +4947,65 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.61.0': resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-znTFp/M0SpN91/FM3FrsTycdTZr+bLszmHObm2LaR5sgo1c2wiK4qYsMnrhLeP0vSMD2LuiNg5J21501h7Aj6g==} + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-1qwEjLW1JCRkDYrS8OyWdfXoL9bNHV28kP3ouQxytZmNpghWSMKZutsxDjVVbnlsbydBdYqqZMttPuTUlm3y0A==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-7zXTq3SVw6XCLdm06VRIhMzayG032Ky14xCZ3isnlcm1KD/p4Ev4XL9TN9fZmIV0bVPGMQr4qGS7a2+te8x7lg==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-/Z5Jx8V22RTuHSVR/QCbCT2eFq6NoYc5oLah5/yDoymZaTlmvuwuc50N6y9YXF0zAh+z4QGoyAqd561cAGSNOA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-yYiDEMT4ok7Wd1VtbyB+0YqBdTkU/yzE36edZCbfA/Ljt6L1xZMEn0a7XGlmRtRfwPGHfjtFRFsxrA2nc7jHnw==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-rz7JoRJE5zaL2RoTOHLUliz6UkDqjUngJEF1/GiSR032v8k8tp8GTezxFoosaCssQSf80cjA5wYup5zrCp4zIg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-xpA7nXiRE3fwvTUK2ibhkPr7hn6D4i3pB9WjlcX7CSPB2pYGMdHAsL55EAFlR3eNKyNM/geoLfqq4L4PIeOWsQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-wuqpmYeMkOvHEx+log3bi913JRacYRo7DwkABMbD5wQMEy2GCdp3461aQ5sGrRD/lL4rylZJxE5B5aQqyhkQYA==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-mZO9hIwXek9VO+N+gs6IKDOf0fPnrQg/SvaOsCDcDwsk7a60yPbacyY7IiXFnxhNgZDLsYqs8j9ojDGl6+Tp5g==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-8zhjBFu0RuL16iFr0TlUU8RRMdmvUY+NuSSxaVeitxmvORFoy0EwIaz+ZK9+xRhfHCIcugi6mEcTqEPv48X3oQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-JtMTHRt7auXTCvvOtcn7aX1gdCPByQl4c5vc8e1d+8PcqtMqeSpH1j0Vp06F9vugTA+YMIzFsJQA91sUK1oHeg==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-Bax5HY/6NUcbk7h+SLJIbRL8TvGRLP1mbvI9T4ah6wfYKRp3rwBlXHP3N+BEYdzOFTUlzBJLknxcjtxWd5cFPw==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-aCZaFWHDt5n8B++F2FrTK2pF7muAFfkvi8qkUnC0N0SNBGdVnqlO28IbdjScIfI2JFOvgZtUoYWeATGWdrLXBA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-+revF2e9JCBx1rLQCX+ny8KuZ0Tl5de7+YpYd4PUNKgF5yPi9n2+TmXtci8/3yBl7QRyLWG1td28RMa/8eIynQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260613.1': - resolution: {integrity: sha512-RJ3YrYNoshFGVY862CwZ/WcICvMW0u9XtezI8AAU81I71Cr3/IJoHPUGsGKUr2gVcPfKRQiaI8YTndFzUGvSug==} + '@typescript/native-preview@7.0.0-dev.20260620.1': + resolution: {integrity: sha512-W5XiTTbIGZSAJUMJqynchbfcjPcoNn29U8dnd+FK+x+Mj9VT9NNTymZ0YAIf+hU2uYAxx3AGe6/U/2OF2TpQwg==} engines: {node: '>=16.20.0'} hasBin: true @@ -5022,16 +5067,27 @@ packages: react-server-dom-webpack: optional: true - '@vitest/browser@4.1.8': - resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==} + '@vitest/browser-playwright@4.1.9': + resolution: {integrity: sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw==} peerDependencies: - vitest: 4.1.8 + playwright: '*' + vitest: 4.1.9 - '@vitest/coverage-v8@4.1.8': - resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} + '@vitest/browser-preview@4.1.9': + resolution: {integrity: sha512-a4/OrkMDb/WUnE4OOB/4FJbK3rYVO7YykqtUgcTKG4p2a0R3XcjPVu7SLRHFBs2+NIYhv5yxp1Lz3dbdGBjIow==} peerDependencies: - '@vitest/browser': 4.1.8 - vitest: 4.1.8 + vitest: 4.1.9 + + '@vitest/browser@4.1.9': + resolution: {integrity: sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==} + peerDependencies: + vitest: 4.1.9 + + '@vitest/coverage-v8@4.1.9': + resolution: {integrity: sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==} + peerDependencies: + '@vitest/browser': 4.1.9 + vitest: 4.1.9 peerDependenciesMeta: '@vitest/browser': optional: true @@ -5043,17 +5099,23 @@ packages: '@typescript-eslint/eslint-plugin': '*' eslint: '>=8.57.0' typescript: '>=5.0.0' + vitest: 4.1.9 peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true typescript: optional: true + vitest: + optional: true '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@4.1.8': - resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5066,28 +5128,34 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.8': - resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.1.8': - resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.8': - resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} - '@voidzero-dev/vite-plus-core@0.1.24': - resolution: {integrity: sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-core@0.2.1': + resolution: {integrity: sha512-iWdtOlLezgYcDqIzxZx1yOUhY93vUB+ob+mRYBNr7/3Hf80uRyTQbqVD1WtsYaANbzeUi81SQ1ZoUraXHO+u8A==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.22.1 - '@tsdown/exe': 0.22.1 + '@tsdown/css': 0.22.3 + '@tsdown/exe': 0.22.3 '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.18 esbuild: ^0.28.1 @@ -5144,86 +5212,55 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.24': - resolution: {integrity: sha512-Hpo9W9piSFlEsJzGkwzfDXhJGrnYByxHXF7NVQZ7g+SLOprddtlfTeM8t+gq9dxcuq0RzM8ddMAhDQP/K3fZQA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-darwin-arm64@0.2.1': + resolution: {integrity: sha512-9AfN/5LKRks8gbTaHPiQHT0L4yboy2xB6x6vvCRWxQMWxPS6/ZJLf5kUIZeE7I1z33AEyLKKkDscsZZVMgMLgg==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.24': - resolution: {integrity: sha512-SwnnnZrEFBiU5iKlh/CZAVwn0RFt/Udrvt3kFLtdRxMtN5bKaqTFVA2H8Y/FPCWp1QX9bs4V9ZIAeXAk06zLkw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-darwin-x64@0.2.1': + resolution: {integrity: sha512-Q1vyimRbf4M82qIQSWRyr7NJaH9ag5G7vVEfGVVJlQHNprI+Q8zj2Phcs/PGf6QcyjcL8UclLznQTHU9NgnKZw==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.24': - resolution: {integrity: sha512-ImM3eqDki4DpRuHjW6dEh4St8zvbcfOMR7KQZJX42ArriCLQ/QdaYhDRRbcDi27XsOBqRxm2eqUUEymPrYIHpA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.1': + resolution: {integrity: sha512-WHW3DziqedRfhJ2upq6kC4y/pmdQWYt322DVB7+4Xb4oOa/CT9GtnSrWIiXVJ4PSO42v54+YsSTKPH2HC5RbtA==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.24': - resolution: {integrity: sha512-gj4mzbob/ls8Zs7iTuF9Gr0EFFF7tdpDiPxDPBkH8tJP5OkHABlzWUwJhU+9xxcUbTaXqpHDw68Mil7jm5dpMg==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.1': + resolution: {integrity: sha512-vUY7hYycZW0qEevpl7ImzZJFnOEKRYCaCOX4TBW0vk6MJZ+zj/xW7e0LOggzJcz2wbYAgLDqp5h+b8wV9dguDA==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.24': - resolution: {integrity: sha512-x7IYK7lI+WuF1n3jSzEYU6FgJxPX/R0rDmTTsOutooGGCU7uShZvfZqIoiTXK0eFnJU5ij5BfBgenenUfsaT/A==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.1': + resolution: {integrity: sha512-tFxpToEaykBGxMQHp8M/qmr1yruRRED+c9gA1h9kmplqot04OxuqzRCWu/IiIvMJ0v3JFdOP3gqkyjXLLJhxIA==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.24': - resolution: {integrity: sha512-JCy2w0eSVUlWQlggK5T47MnL+j0o4EY7hLskINVI8gi+aixQF4xnYBDobz0lbxkqz3/IfiLyXUx6TcU3thcsGQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.1': + resolution: {integrity: sha512-2scSS7wEbLO2758fqr1/bAULg7nLCFa5V8LO2b5w3g1CrTYdMTDt2WX1ghPesIi+70pYGydRbXo6iaaN43zfMg==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-test@0.1.24': - resolution: {integrity: sha512-9NiG6UadG0iOaPL1AMsO5sDKkx6MADHw4/mMOmHWZUhhUwqzfVtnnptMK37vD71e6KyR7yAscx19FrtOWWtjvA==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/coverage-istanbul': 4.1.8 - '@vitest/coverage-v8': 4.1.8 - '@vitest/ui': 4.1.8 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.24': - resolution: {integrity: sha512-G+/lhLKVjyn3FmgXX8jeWgq7RcE5O1kdR7QyFayQOdlMX/ZRkvUwQD7bFaqhKzgJM6Oj3a1FH3HQPYk5QOYuCQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.1': + resolution: {integrity: sha512-3+5FJYhi9SqBszjngI2LBmvoiqEwxJWyQ5UsOUtNz6/d+yDrDw+tOgHLl4OKIh5aVNZeIGXzxvP6h24kcEqIyg==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.24': - resolution: {integrity: sha512-b0e5XohEV1w/RdzAtv8/Hm6tvHPXouPtBNsljjW/lDJZq3NCLND5s6lqe8H4IenrgmKSoqakHWtlqJqM36cFbw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.1': + resolution: {integrity: sha512-5sOEwEoU5PW7ObmJ5VCakU09Oh14rYCoLQJkFqvOph6PK30lN5iqWGk0KigEyfcd7Zv+fZg9EmcERDol/3Xl9w==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [win32] @@ -5537,6 +5574,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.1: resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} engines: {node: '>=10'} @@ -5650,8 +5691,8 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - code-inspector-plugin@1.6.0: - resolution: {integrity: sha512-pH5sQoL5VGKXo92yimfzdF8yHWGU3hC8uA8kovLR3EB0WSJ7TfC+AZaPryLI2n5F2dSWUgaCv5zuoNvDvKHQBQ==} + code-inspector-plugin@1.6.1: + resolution: {integrity: sha512-XsGzQ2Kkrn2xdZtwviORKOVY4H8ixe95VSxkjUfzdWGhl9z3UVA2tci8o/1juJm+vaIxcVjlyefJOoTKjqhQcw==} collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -5729,8 +5770,8 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - cron-parser@5.5.0: - resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==} + cron-parser@5.6.0: + resolution: {integrity: sha512-6159NDv4eDOjXYDmMTkvUtaVcIrNZI779ydTMOfDdi9fSjhPqmySx0icCHX2+nOyXgNSw6sFCsgLKxYs/2KPuQ==} engines: {node: '>=18'} cross-spawn@7.0.6: @@ -6069,8 +6110,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.10: - resolution: {integrity: sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -6224,6 +6265,7 @@ packages: esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} + hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -6437,16 +6479,16 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-sonarjs@4.0.3: - resolution: {integrity: sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g==} + eslint-plugin-sonarjs@4.1.0: + resolution: {integrity: sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==} peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-storybook@10.4.4: - resolution: {integrity: sha512-RqEDQJRaeTdfSDRO9W2sKui4oLax6cSuKU/6vFXbiyEmyaACktkQce9DYEf2i5+MZ07y/X/1G0UZeY+WrPQlIg==} + eslint-plugin-storybook@10.4.6: + resolution: {integrity: sha512-CfGSXn6zFspeYTU8R7v797MOmJFj8xc6MWf/oGuRwbKeMoSwnliR+OlXSjMZYM1D6gfmwiuH1VX58LSHdn+ZPg==} peerDependencies: eslint: '>=8' - storybook: ^10.4.4 + storybook: ^10.4.6 eslint-plugin-toml@1.3.1: resolution: {integrity: sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ==} @@ -6585,6 +6627,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -6685,8 +6731,8 @@ packages: engines: {node: '>=18.3.0'} hasBin: true - foxact@0.3.5: - resolution: {integrity: sha512-pcn1Ro445aOooB9Vk6NfbUMpiMHOgZ/Nki9MmVslVfRYjvmySLtssgALYyzspt7j2dNbpxxCyClaYSL2I4iJTg==} + foxact@0.3.7: + resolution: {integrity: sha512-U9+boW5SGK8TMuJ/21s7yfszXJCqVoUj8K9Gqmpe/uqiOG4wtPILxsONqrh0IizteGzGwiDa7oiPX2hn5AJtog==} peerDependencies: react: '*' react-dom: '*' @@ -6839,8 +6885,8 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - happy-dom@20.10.3: - resolution: {integrity: sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw==} + happy-dom@20.10.6: + resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} engines: {node: '>=20.0.0'} has-ansi@6.0.2: @@ -6923,8 +6969,8 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - hono@4.12.25: - resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} + hono@4.12.26: + resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -6990,6 +7036,7 @@ packages: image-size@2.0.2: resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} engines: {node: '>=16.x'} + hasBin: true immer@11.1.8: resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} @@ -7331,8 +7378,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.16.1: - resolution: {integrity: sha512-TKMn1rxgH6h9vXR9Y0B+Cq7AdPTr9EI02IwoT65NzqYUkvoDQAaJ/aPybiFpAhZ1px6cNYYwXf86iHkBgzCo9w==} + knip@6.17.1: + resolution: {integrity: sha512-HcQsZSQ4Ymhuay4BVzJtM5pFZNDSomYYqcNCZOSITPQh9g18a09DqziWAxSt2G+BH9wGlG+0ZjWpEnaFlnKseQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7493,8 +7540,8 @@ packages: loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - loro-crdt@1.13.2: - resolution: {integrity: sha512-Br9tZZk9x/HP83By9RvOCqzWh8v8tnOhVlR6/ibYNtLSmysO7ZgwzjNpqsCABqaSOcGC7TBkx5sG8tfosdJMQA==} + loro-crdt@1.13.5: + resolution: {integrity: sha512-U6L88EbSdv8TeFcXyKew0BySRJnOCPsgU8p38PhzGjQlfD8TC1pLRk1J7X5OYecr8Z9ijD1J13JEf+qIvw3ztg==} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -8133,15 +8180,15 @@ packages: resolution: {integrity: sha512-+0LAPHaqtfQlvWdpaAa09SmOaZZgP8C552xosEkGJ4+ruEwP1Vgx+sqBgcBCNfR6KDCmagGOZTde8wmAvcI/Hg==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.133.0: - resolution: {integrity: sha512-661RSx+ZcjBmjBYid+Fpp/2F5EbtildpeoZh5HdgnGs+jZ03nqQEQW8yGkt4BGyOC3OMPDQQRl8M5kqD2/g6jw==} + oxc-parser@0.135.0: + resolution: {integrity: sha512-/DaPStu0s2zzNSRRniKyTPM6Z/o+DapOp2JYNKDL8AsgaBGPK2IdZyB87SQjVH+xeQPz+Qr9mrjglfkYgtbVRA==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.20.0: resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} - oxfmt@0.52.0: - resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} + oxfmt@0.55.0: + resolution: {integrity: sha512-jSj2wCTakwgPMxkfiVZX0jf+nX+Nz6xlyAZjqNE0qXTFdCBPYlP6JAN+ODjmealw7DXBjOzYbdsqwBMAZnPZ6A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -8155,9 +8202,10 @@ packages: oxlint-tsgolint@0.23.0: resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} + hasBin: true - oxlint@1.67.0: - resolution: {integrity: sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==} + oxlint@1.70.0: + resolution: {integrity: sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -8278,23 +8326,21 @@ packages: pinyin-pro@3.28.1: resolution: {integrity: sha512-oqz8ulwRgtUXRi0vbqEfGNly19zpyCxYrjhkk5TibGcgSW6eNwS5woajCXRwqURi8Ehc2yOFTiB4uNoZ+NJOnA==} - pixelmatch@7.1.0: - resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} - hasBin: true - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + playwright-core@1.61.0: + resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} engines: {node: '>=18'} + hasBin: true - playwright@1.60.0: - resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + playwright@1.61.0: + resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} engines: {node: '>=18'} + hasBin: true pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} @@ -8787,8 +8833,8 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.35.1: - resolution: {integrity: sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} engines: {node: '>=20.9.0'} shebang-command@2.0.0: @@ -8807,6 +8853,9 @@ packages: resolution: {integrity: sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==} engines: {node: '>=20'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -8882,6 +8931,9 @@ packages: resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} engines: {node: '>=20.16.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -8903,8 +8955,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@10.4.4: - resolution: {integrity: sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA==} + storybook@10.4.6: + resolution: {integrity: sha512-6wkA6LxfDSSilloITsrFOJfsnw0mDUP2h8Ls+lRt8oRsudtz2RWFhLv+Toiwg6NW7hUpdTDc2hzR7DztJid6+A==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -9089,6 +9141,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -9105,11 +9161,12 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@7.4.2: - resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} + tldts-core@7.4.3: + resolution: {integrity: sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==} - tldts@7.4.2: - resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} + tldts@7.4.3: + resolution: {integrity: sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==} + hasBin: true to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -9158,6 +9215,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -9235,8 +9293,8 @@ packages: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} - unbash@3.0.0: - resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} + unbash@4.0.1: + resolution: {integrity: sha512-1ajSo3813sDoVIHx4inJdUS4l5L2ic5cFiddemPiyjb/PZEoBAhFwHtbaEdRDFxbAKy7FCG7s5ww3/uCFawuIA==} engines: {node: '>=14'} unbox-primitive@1.1.0: @@ -9246,14 +9304,14 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici@7.26.0: - resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==} - engines: {node: '>=20.18.1'} - undici@7.27.2: resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} engines: {node: '>=20.18.1'} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} @@ -9383,8 +9441,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true valibot@1.4.1: resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==} @@ -9406,8 +9465,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@0.1.2: - resolution: {integrity: sha512-ITrJGE7UwOanhz2Vfhkn44BSOskOrPxISjhJ3PbuFd8vEXzMNy6O2mOsFqWAatnFC4fMEanLVth5LAcxwkdH2A==} + vinext@0.1.6: + resolution: {integrity: sha512-uNqbo86PdaKcHCWpGcYOimQ2g1sS86um0InilA8bGjz51oktCssX6jWnQdOKYfOzYYOYYTW9y4N8vfC4vyIx6w==} engines: {node: '>=22'} hasBin: true peerDependencies: @@ -9449,9 +9508,18 @@ packages: storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.24: - resolution: {integrity: sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ==} - engines: {node: ^20.19.0 || >=22.12.0} + vite-plus@0.2.1: + resolution: {integrity: sha512-q5q/Y38UkWFsNg1JO+RyRdPUqoewaSqIlMyK2p83GKNUvf4D38Ntb3PToRTDZbTRh7mWt+B+d0DQBv4nCDpMcQ==} + engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + peerDependenciesMeta: + '@vitest/browser-playwright': + optional: true + '@vitest/browser-webdriverio': + optional: true vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} @@ -9481,7 +9549,7 @@ packages: '@types/react-dom': ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - vitest: ^4.0.0 + vitest: 4.1.9 peerDependenciesMeta: '@types/react': optional: true @@ -9491,7 +9559,48 @@ packages: vitest-canvas-mock@1.1.4: resolution: {integrity: sha512-4boWHY+STwAxGl1+uwakNNoQky5EjPLC8HuponXNoAscYyT1h/F7RUvTkl4IyF/MiWr3V8Q626je3Iel3eArqA==} peerDependencies: - vitest: ^3.0.0 || ^4.0.0 + vitest: 4.1.9 + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} @@ -9559,6 +9668,11 @@ packages: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -9854,17 +9968,73 @@ snapshots: idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@types/node@25.9.3)(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(happy-dom@20.10.3)(jiti@2.7.0)(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': + '@antfu/eslint-config@9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)(vitest@4.1.9)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.4.0 - '@e18e/eslint-plugin': 0.4.1(eslint@10.5.0(jiti@2.7.0))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@e18e/eslint-plugin': 0.4.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint/markdown': 8.0.1 + '@stylistic/eslint-plugin': 5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@vitest/eslint-plugin': 1.6.17(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)(vitest@4.1.9) + ansis: 4.3.0 + cac: 7.0.0 + eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) + eslint-config-flat-gitignore: 2.3.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-flat-config-utils: 3.2.0 + eslint-merge-processors: 2.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-antfu: 3.2.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-import-lite: 0.6.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-jsdoc: 62.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-plugin-jsonc: 3.1.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-n: 18.0.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + eslint-plugin-no-only-tests: 3.4.0 + eslint-plugin-perfectionist: 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + eslint-plugin-pnpm: 1.6.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-regexp: 3.1.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-toml: 1.3.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-plugin-unicorn: 64.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-vue: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)) + eslint-plugin-yml: 3.3.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-processor-vue-blocks: 2.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + globals: 17.6.0 + local-pkg: 1.1.2 + parse-gitignore: 2.0.0 + toml-eslint-parser: 1.0.3 + vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + yaml-eslint-parser: 2.0.0 + optionalDependencies: + '@eslint-react/eslint-plugin': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@next/eslint-plugin-next': 16.2.9 + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + eslint-plugin-react-refresh: 0.5.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + transitivePeerDependencies: + - '@eslint/json' + - '@typescript-eslint/rule-tester' + - '@typescript-eslint/typescript-estree' + - '@typescript-eslint/utils' + - '@vue/compiler-sfc' + - oxlint + - supports-color + - ts-declaration-location + - typescript + - vitest + + '@antfu/eslint-config@9.0.0(@eslint-react/eslint-plugin@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@next/eslint-plugin-next@16.2.9)(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)))(eslint-plugin-react-refresh@0.5.3(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)(vitest@4.1.9)': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@clack/prompts': 1.4.0 + '@e18e/eslint-plugin': 0.4.1(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.5.0(jiti@2.7.0)) '@eslint/markdown': 8.0.1 '@stylistic/eslint-plugin': 5.10.0(eslint@10.5.0(jiti@2.7.0)) - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) - '@vitest/eslint-plugin': 1.6.17(@types/node@25.9.3)(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint@10.5.0(jiti@2.7.0))(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@vitest/eslint-plugin': 1.6.17(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.9) ansis: 4.3.0 cac: 7.0.0 eslint: 10.5.0(jiti@2.7.0) @@ -9872,7 +10042,7 @@ snapshots: eslint-flat-config-utils: 3.2.0 eslint-merge-processors: 2.0.0(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-antfu: 3.2.3(eslint@10.5.0(jiti@2.7.0)) - eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)) + eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-import-lite: 0.6.0(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-jsdoc: 62.9.0(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-jsonc: 3.1.2(eslint@10.5.0(jiti@2.7.0)) @@ -9899,126 +10069,16 @@ snapshots: eslint-plugin-jsx-a11y: 6.10.2(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-react-refresh: 0.5.3(eslint@10.5.0(jiti@2.7.0)) transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - '@eslint/json' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - '@typescript-eslint/rule-tester' - '@typescript-eslint/typescript-estree' - '@typescript-eslint/utils' - - '@vitejs/devtools' - - '@vitest/coverage-istanbul' - - '@vitest/coverage-v8' - - '@vitest/ui' - '@vue/compiler-sfc' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - oxlint - - publint - - sass - - sass-embedded - - stylus - - sugarss - supports-color - - terser - ts-declaration-location - - tsx - typescript - - unplugin-unused - - unrun - - utf-8-validate - - vite - - yaml - - '@antfu/eslint-config@9.0.0(b376e15be293d4e014f0f69f32d1fb4a)': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@clack/prompts': 1.4.0 - '@e18e/eslint-plugin': 0.4.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) - '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - '@eslint/markdown': 8.0.1 - '@stylistic/eslint-plugin': 5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@vitest/eslint-plugin': 1.6.17(@types/node@25.9.3)(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - ansis: 4.3.0 - cac: 7.0.0 - eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-config-flat-gitignore: 2.3.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-flat-config-utils: 3.2.0 - eslint-merge-processors: 2.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-antfu: 3.2.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-command: 3.5.2(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-import-lite: 0.6.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-jsdoc: 62.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-jsonc: 3.1.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-n: 18.0.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-perfectionist: 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - eslint-plugin-pnpm: 1.6.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-regexp: 3.1.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-toml: 1.3.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-unicorn: 64.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-vue: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)) - eslint-plugin-yml: 3.3.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-processor-vue-blocks: 2.0.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - globals: 17.6.0 - local-pkg: 1.1.2 - parse-gitignore: 2.0.0 - toml-eslint-parser: 1.0.3 - vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) - yaml-eslint-parser: 2.0.0 - optionalDependencies: - '@eslint-react/eslint-plugin': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@next/eslint-plugin-next': 16.2.9 - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - eslint-plugin-react-refresh: 0.5.3(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - - '@eslint/json' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - - '@typescript-eslint/rule-tester' - - '@typescript-eslint/typescript-estree' - - '@typescript-eslint/utils' - - '@vitejs/devtools' - - '@vitest/coverage-istanbul' - - '@vitest/coverage-v8' - - '@vitest/ui' - - '@vue/compiler-sfc' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - - oxlint - - publint - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - ts-declaration-location - - tsx - - typescript - - unplugin-unused - - unrun - - utf-8-validate - - vite - - yaml + - vitest '@antfu/install-pkg@1.1.0': dependencies: @@ -10061,14 +10121,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -10085,8 +10137,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} - '@babel/helper-globals@7.29.7': {} '@babel/helper-module-imports@7.29.7': @@ -10105,8 +10155,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -10120,40 +10168,18 @@ snapshots: '@babel/template': 7.29.7 '@babel/types': 7.29.7 - '@babel/parser@7.29.2': - dependencies: - '@babel/types': 7.29.0 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 '@babel/runtime@7.29.2': {} - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -10166,11 +10192,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -10207,12 +10228,12 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@chromatic-com/storybook@5.2.1(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@chromatic-com/storybook@5.2.1(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 16.10.0 jsonfile: 6.2.0 - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -10231,7 +10252,7 @@ snapshots: fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 - '@code-inspector/core@1.6.0(supports-color@10.2.2)': + '@code-inspector/core@1.6.1(supports-color@10.2.2)': dependencies: '@vue/compiler-dom': 3.5.31 chalk: 4.1.2 @@ -10241,35 +10262,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@code-inspector/esbuild@1.6.0': + '@code-inspector/esbuild@1.6.1': dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) + '@code-inspector/core': 1.6.1(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@code-inspector/mako@1.6.0': + '@code-inspector/mako@1.6.1': dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) + '@code-inspector/core': 1.6.1(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@code-inspector/turbopack@1.6.0': + '@code-inspector/turbopack@1.6.1': dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) - '@code-inspector/webpack': 1.6.0 + '@code-inspector/core': 1.6.1(supports-color@10.2.2) + '@code-inspector/webpack': 1.6.1 transitivePeerDependencies: - supports-color - '@code-inspector/vite@1.6.0': + '@code-inspector/vite@1.6.1': dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) + '@code-inspector/core': 1.6.1(supports-color@10.2.2) chalk: 4.1.1 transitivePeerDependencies: - supports-color - '@code-inspector/webpack@1.6.0': + '@code-inspector/webpack@1.6.1': dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) + '@code-inspector/core': 1.6.1(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -10388,23 +10409,23 @@ snapshots: perfect-debounce: 2.1.0 tinyexec: 1.2.3 - '@e18e/eslint-plugin@0.4.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@e18e/eslint-plugin@0.4.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: empathic: 2.0.0 module-replacements: 3.0.0-beta.7 semver: 7.8.4 optionalDependencies: eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) - oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + oxlint: 1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - '@e18e/eslint-plugin@0.4.1(eslint@10.5.0(jiti@2.7.0))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@e18e/eslint-plugin@0.4.1(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: empathic: 2.0.0 module-replacements: 3.0.0-beta.7 semver: 7.8.4 optionalDependencies: eslint: 10.5.0(jiti@2.7.0) - oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + oxlint: 1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.3.1)': dependencies: @@ -10567,9 +10588,9 @@ snapshots: '@eslint-react/ast@5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) string-ts: 2.3.1 typescript: 6.0.3 @@ -10579,9 +10600,9 @@ snapshots: '@eslint-react/ast@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) string-ts: 2.3.1 typescript: 6.0.3 @@ -10595,9 +10616,9 @@ snapshots: '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10612,9 +10633,9 @@ snapshots: '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10652,7 +10673,7 @@ snapshots: '@eslint-react/eslint@5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: @@ -10661,7 +10682,7 @@ snapshots: '@eslint-react/eslint@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: @@ -10673,8 +10694,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10688,8 +10709,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10699,7 +10720,7 @@ snapshots: '@eslint-react/shared@5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': dependencies: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10711,7 +10732,7 @@ snapshots: '@eslint-react/shared@5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10723,9 +10744,9 @@ snapshots: dependencies: '@eslint-react/ast': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10737,9 +10758,9 @@ snapshots: dependencies: '@eslint-react/ast': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -10923,9 +10944,9 @@ snapshots: '@hey-api/types@0.1.4': {} - '@hono/node-server@2.0.4(hono@4.12.25)': + '@hono/node-server@2.0.5(hono@4.12.26)': dependencies: - hono: 4.12.25 + hono: 4.12.26 '@humanfs/core@0.19.1': {} @@ -10988,9 +11009,9 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-arm64@0.35.1': + '@img/sharp-darwin-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true '@img/sharp-darwin-x64@0.34.5': @@ -10998,74 +11019,74 @@ snapshots: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.35.1': + '@img/sharp-darwin-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.1 optional: true - '@img/sharp-freebsd-wasm32@0.35.1': + '@img/sharp-freebsd-wasm32@0.35.2': dependencies: - '@img/sharp-wasm32': 0.35.1 + '@img/sharp-wasm32': 0.35.2 optional: true '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.3.0': + '@img/sharp-libvips-darwin-arm64@1.3.1': optional: true '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.0': + '@img/sharp-libvips-darwin-x64@1.3.1': optional: true '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.0': + '@img/sharp-libvips-linux-arm64@1.3.1': optional: true '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.0': + '@img/sharp-libvips-linux-arm@1.3.1': optional: true '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.0': + '@img/sharp-libvips-linux-ppc64@1.3.1': optional: true '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.0': + '@img/sharp-libvips-linux-riscv64@1.3.1': optional: true '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.0': + '@img/sharp-libvips-linux-s390x@1.3.1': optional: true '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.0': + '@img/sharp-libvips-linux-x64@1.3.1': optional: true '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': optional: true '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.0': + '@img/sharp-libvips-linuxmusl-x64@1.3.1': optional: true '@img/sharp-linux-arm64@0.34.5': @@ -11073,9 +11094,9 @@ snapshots: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.35.1': + '@img/sharp-linux-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.1 optional: true '@img/sharp-linux-arm@0.34.5': @@ -11083,9 +11104,9 @@ snapshots: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm@0.35.1': + '@img/sharp-linux-arm@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.1 optional: true '@img/sharp-linux-ppc64@0.34.5': @@ -11093,9 +11114,9 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.35.1': + '@img/sharp-linux-ppc64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.1 optional: true '@img/sharp-linux-riscv64@0.34.5': @@ -11103,9 +11124,9 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-riscv64@0.35.1': + '@img/sharp-linux-riscv64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.1 optional: true '@img/sharp-linux-s390x@0.34.5': @@ -11113,9 +11134,9 @@ snapshots: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.35.1': + '@img/sharp-linux-s390x@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.1 optional: true '@img/sharp-linux-x64@0.34.5': @@ -11123,9 +11144,9 @@ snapshots: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.35.1': + '@img/sharp-linux-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.1 optional: true '@img/sharp-linuxmusl-arm64@0.34.5': @@ -11133,9 +11154,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.1': + '@img/sharp-linuxmusl-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 optional: true '@img/sharp-linuxmusl-x64@0.34.5': @@ -11143,9 +11164,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.1': + '@img/sharp-linuxmusl-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 optional: true '@img/sharp-wasm32@0.34.5': @@ -11153,43 +11174,43 @@ snapshots: '@emnapi/runtime': 1.11.1 optional: true - '@img/sharp-wasm32@0.35.1': + '@img/sharp-wasm32@0.35.2': dependencies: '@emnapi/runtime': 1.11.1 optional: true - '@img/sharp-webcontainers-wasm32@0.35.1': + '@img/sharp-webcontainers-wasm32@0.35.2': dependencies: - '@img/sharp-wasm32': 0.35.1 + '@img/sharp-wasm32': 0.35.2 optional: true '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-arm64@0.35.1': + '@img/sharp-win32-arm64@0.35.2': optional: true '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-ia32@0.35.1': + '@img/sharp-win32-ia32@0.35.2': optional: true '@img/sharp-win32-x64@0.34.5': optional: true - '@img/sharp-win32-x64@0.35.1': + '@img/sharp-win32-x64@0.35.2': optional: true '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3)': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' optionalDependencies: typescript: 6.0.3 @@ -11649,7 +11670,7 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.132.0': optional: true - '@oxc-parser/binding-android-arm-eabi@0.133.0': + '@oxc-parser/binding-android-arm-eabi@0.135.0': optional: true '@oxc-parser/binding-android-arm64@0.127.0': @@ -11658,7 +11679,7 @@ snapshots: '@oxc-parser/binding-android-arm64@0.132.0': optional: true - '@oxc-parser/binding-android-arm64@0.133.0': + '@oxc-parser/binding-android-arm64@0.135.0': optional: true '@oxc-parser/binding-darwin-arm64@0.127.0': @@ -11667,7 +11688,7 @@ snapshots: '@oxc-parser/binding-darwin-arm64@0.132.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.133.0': + '@oxc-parser/binding-darwin-arm64@0.135.0': optional: true '@oxc-parser/binding-darwin-x64@0.127.0': @@ -11676,7 +11697,7 @@ snapshots: '@oxc-parser/binding-darwin-x64@0.132.0': optional: true - '@oxc-parser/binding-darwin-x64@0.133.0': + '@oxc-parser/binding-darwin-x64@0.135.0': optional: true '@oxc-parser/binding-freebsd-x64@0.127.0': @@ -11685,7 +11706,7 @@ snapshots: '@oxc-parser/binding-freebsd-x64@0.132.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.133.0': + '@oxc-parser/binding-freebsd-x64@0.135.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': @@ -11694,7 +11715,7 @@ snapshots: '@oxc-parser/binding-linux-arm-gnueabihf@0.132.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.133.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.135.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': @@ -11703,7 +11724,7 @@ snapshots: '@oxc-parser/binding-linux-arm-musleabihf@0.132.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.133.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.135.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.127.0': @@ -11712,7 +11733,7 @@ snapshots: '@oxc-parser/binding-linux-arm64-gnu@0.132.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.133.0': + '@oxc-parser/binding-linux-arm64-gnu@0.135.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.127.0': @@ -11721,7 +11742,7 @@ snapshots: '@oxc-parser/binding-linux-arm64-musl@0.132.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.133.0': + '@oxc-parser/binding-linux-arm64-musl@0.135.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': @@ -11730,7 +11751,7 @@ snapshots: '@oxc-parser/binding-linux-ppc64-gnu@0.132.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.133.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.135.0': optional: true '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': @@ -11739,7 +11760,7 @@ snapshots: '@oxc-parser/binding-linux-riscv64-gnu@0.132.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.133.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.135.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.127.0': @@ -11748,7 +11769,7 @@ snapshots: '@oxc-parser/binding-linux-riscv64-musl@0.132.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.133.0': + '@oxc-parser/binding-linux-riscv64-musl@0.135.0': optional: true '@oxc-parser/binding-linux-s390x-gnu@0.127.0': @@ -11757,7 +11778,7 @@ snapshots: '@oxc-parser/binding-linux-s390x-gnu@0.132.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.133.0': + '@oxc-parser/binding-linux-s390x-gnu@0.135.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.127.0': @@ -11766,7 +11787,7 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu@0.132.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.133.0': + '@oxc-parser/binding-linux-x64-gnu@0.135.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.127.0': @@ -11775,7 +11796,7 @@ snapshots: '@oxc-parser/binding-linux-x64-musl@0.132.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.133.0': + '@oxc-parser/binding-linux-x64-musl@0.135.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.127.0': @@ -11784,7 +11805,7 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.132.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.133.0': + '@oxc-parser/binding-openharmony-arm64@0.135.0': optional: true '@oxc-parser/binding-wasm32-wasi@0.127.0': @@ -11801,7 +11822,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@oxc-parser/binding-wasm32-wasi@0.133.0': + '@oxc-parser/binding-wasm32-wasi@0.135.0': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 @@ -11814,7 +11835,7 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.132.0': optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.133.0': + '@oxc-parser/binding-win32-arm64-msvc@0.135.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.127.0': @@ -11823,7 +11844,7 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc@0.132.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.133.0': + '@oxc-parser/binding-win32-ia32-msvc@0.135.0': optional: true '@oxc-parser/binding-win32-x64-msvc@0.127.0': @@ -11832,16 +11853,18 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.132.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.133.0': + '@oxc-parser/binding-win32-x64-msvc@0.135.0': optional: true - '@oxc-project/runtime@0.133.0': {} + '@oxc-project/runtime@0.136.0': {} '@oxc-project/types@0.127.0': {} '@oxc-project/types@0.132.0': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.135.0': {} + + '@oxc-project/types@0.136.0': {} '@oxc-resolver/binding-android-arm-eabi@11.20.0': optional: true @@ -11904,61 +11927,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.20.0': optional: true - '@oxfmt/binding-android-arm-eabi@0.52.0': + '@oxfmt/binding-android-arm-eabi@0.55.0': optional: true - '@oxfmt/binding-android-arm64@0.52.0': + '@oxfmt/binding-android-arm64@0.55.0': optional: true - '@oxfmt/binding-darwin-arm64@0.52.0': + '@oxfmt/binding-darwin-arm64@0.55.0': optional: true - '@oxfmt/binding-darwin-x64@0.52.0': + '@oxfmt/binding-darwin-x64@0.55.0': optional: true - '@oxfmt/binding-freebsd-x64@0.52.0': + '@oxfmt/binding-freebsd-x64@0.55.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.55.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': + '@oxfmt/binding-linux-arm-musleabihf@0.55.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.52.0': + '@oxfmt/binding-linux-arm64-gnu@0.55.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.52.0': + '@oxfmt/binding-linux-arm64-musl@0.55.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': + '@oxfmt/binding-linux-ppc64-gnu@0.55.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': + '@oxfmt/binding-linux-riscv64-gnu@0.55.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.52.0': + '@oxfmt/binding-linux-riscv64-musl@0.55.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.52.0': + '@oxfmt/binding-linux-s390x-gnu@0.55.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.52.0': + '@oxfmt/binding-linux-x64-gnu@0.55.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.52.0': + '@oxfmt/binding-linux-x64-musl@0.55.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.52.0': + '@oxfmt/binding-openharmony-arm64@0.55.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.52.0': + '@oxfmt/binding-win32-arm64-msvc@0.55.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.52.0': + '@oxfmt/binding-win32-ia32-msvc@0.55.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.52.0': + '@oxfmt/binding-win32-x64-msvc@0.55.0': optional: true '@oxlint-tsgolint/darwin-arm64@0.23.0': @@ -11979,70 +12002,70 @@ snapshots: '@oxlint-tsgolint/win32-x64@0.23.0': optional: true - '@oxlint/binding-android-arm-eabi@1.67.0': + '@oxlint/binding-android-arm-eabi@1.70.0': optional: true - '@oxlint/binding-android-arm64@1.67.0': + '@oxlint/binding-android-arm64@1.70.0': optional: true - '@oxlint/binding-darwin-arm64@1.67.0': + '@oxlint/binding-darwin-arm64@1.70.0': optional: true - '@oxlint/binding-darwin-x64@1.67.0': + '@oxlint/binding-darwin-x64@1.70.0': optional: true - '@oxlint/binding-freebsd-x64@1.67.0': + '@oxlint/binding-freebsd-x64@1.70.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.67.0': + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.67.0': + '@oxlint/binding-linux-arm-musleabihf@1.70.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.67.0': + '@oxlint/binding-linux-arm64-gnu@1.70.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.67.0': + '@oxlint/binding-linux-arm64-musl@1.70.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.67.0': + '@oxlint/binding-linux-ppc64-gnu@1.70.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.67.0': + '@oxlint/binding-linux-riscv64-gnu@1.70.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.67.0': + '@oxlint/binding-linux-riscv64-musl@1.70.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.67.0': + '@oxlint/binding-linux-s390x-gnu@1.70.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.67.0': + '@oxlint/binding-linux-x64-gnu@1.70.0': optional: true - '@oxlint/binding-linux-x64-musl@1.67.0': + '@oxlint/binding-linux-x64-musl@1.70.0': optional: true - '@oxlint/binding-openharmony-arm64@1.67.0': + '@oxlint/binding-openharmony-arm64@1.70.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.67.0': + '@oxlint/binding-win32-arm64-msvc@1.70.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.67.0': + '@oxlint/binding-win32-ia32-msvc@1.70.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.67.0': + '@oxlint/binding-win32-x64-msvc@1.70.0': optional: true - '@oxlint/plugins@1.61.0': {} + '@oxlint/plugins@1.68.0': {} '@pkgr/core@0.2.9': {} - '@playwright/test@1.60.0': + '@playwright/test@1.61.0': dependencies: - playwright: 1.60.0 + playwright: 1.61.0 '@polka/url@1.0.0-next.29': {} @@ -12306,40 +12329,40 @@ snapshots: estree-walker: 2.0.2 picomatch: 4.0.4 - '@sentry-internal/browser-utils@10.57.0': + '@sentry/browser-utils@10.59.0': dependencies: - '@sentry/core': 10.57.0 + '@sentry/core': 10.59.0 - '@sentry-internal/feedback@10.57.0': + '@sentry/browser@10.59.0': dependencies: - '@sentry/core': 10.57.0 + '@sentry/browser-utils': 10.59.0 + '@sentry/core': 10.59.0 + '@sentry/feedback': 10.59.0 + '@sentry/replay': 10.59.0 + '@sentry/replay-canvas': 10.59.0 - '@sentry-internal/replay-canvas@10.57.0': + '@sentry/core@10.59.0': {} + + '@sentry/feedback@10.59.0': dependencies: - '@sentry-internal/replay': 10.57.0 - '@sentry/core': 10.57.0 + '@sentry/core': 10.59.0 - '@sentry-internal/replay@10.57.0': + '@sentry/react@10.59.0(react@19.2.7)': dependencies: - '@sentry-internal/browser-utils': 10.57.0 - '@sentry/core': 10.57.0 - - '@sentry/browser@10.57.0': - dependencies: - '@sentry-internal/browser-utils': 10.57.0 - '@sentry-internal/feedback': 10.57.0 - '@sentry-internal/replay': 10.57.0 - '@sentry-internal/replay-canvas': 10.57.0 - '@sentry/core': 10.57.0 - - '@sentry/core@10.57.0': {} - - '@sentry/react@10.57.0(react@19.2.7)': - dependencies: - '@sentry/browser': 10.57.0 - '@sentry/core': 10.57.0 + '@sentry/browser': 10.59.0 + '@sentry/core': 10.59.0 react: 19.2.7 + '@sentry/replay-canvas@10.59.0': + dependencies: + '@sentry/core': 10.59.0 + '@sentry/replay': 10.59.0 + + '@sentry/replay@10.59.0': + dependencies: + '@sentry/browser-utils': 10.59.0 + '@sentry/core': 10.59.0 + '@shikijs/core@4.2.0': dependencies: '@shikijs/primitive': 4.2.0 @@ -12393,21 +12416,21 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-a11y@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.12.0 - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - '@storybook/addon-docs@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-docs@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) - '@storybook/csf-plugin': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@storybook/csf-plugin': 10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@storybook/react-dom-shim': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) ts-dedent: 2.2.0 optionalDependencies: '@types/react': 19.2.17 @@ -12418,53 +12441,55 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.4.4(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-links@10.4.6(@types/react@19.2.17)(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.17 react: 19.2.7 - '@storybook/addon-onboarding@10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-onboarding@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - '@storybook/addon-themes@10.4.4(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-themes@10.4.6(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) ts-dedent: 2.2.0 - '@storybook/addon-vitest@10.4.4(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/addon-vitest@10.4.6(@vitest/browser-playwright@4.1.9)(@vitest/browser@4.1.9)(@vitest/runner@4.1.9)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(vitest@4.1.9)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24) - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/browser-playwright': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(playwright@1.61.0)(vitest@4.1.9) + '@vitest/runner': 4.1.9 + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/builder-vite@10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: - '@storybook/csf-plugin': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@storybook/csf-plugin': 10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/csf-plugin@10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.1 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' '@storybook/global@5.0.0': {} @@ -12473,18 +12498,18 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@storybook/nextjs-vite@10.4.4(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)': + '@storybook/nextjs-vite@10.4.6(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@storybook/builder-vite': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) - '@storybook/react': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) - '@storybook/react-vite': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) - next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@storybook/builder-vite': 10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + '@storybook/react-vite': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3) + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) @@ -12497,30 +12522,30 @@ snapshots: - supports-color - webpack - '@storybook/react-dom-shim@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': + '@storybook/react-dom-shim@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))': dependencies: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@storybook/react-vite@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)': + '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 10.4.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) - '@storybook/react': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) + '@storybook/builder-vite': 10.4.6(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.7 react-docgen: 8.0.3 react-dom: 19.2.7(react@19.2.7) resolve: 1.22.11 - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -12530,15 +12555,15 @@ snapshots: - typescript - webpack - '@storybook/react@10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)': + '@storybook/react@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.4.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) + '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))) react: 19.2.7 react-docgen: 8.0.3 react-docgen-typescript: 2.4.0(typescript@6.0.3) react-dom: 19.2.7(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) @@ -12669,12 +12694,12 @@ snapshots: postcss-selector-parser: 6.1.4 tailwindcss: 4.3.1 - '@tailwindcss/vite@4.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.1 '@tailwindcss/oxide': 4.3.1 tailwindcss: 4.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' '@tanstack/devtools-event-client@0.4.3': {} @@ -12728,21 +12753,21 @@ snapshots: react-dom: 19.2.7(react@19.2.7) use-sync-external-store: 1.6.0(react@19.2.7) - '@tanstack/react-virtual@3.14.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@tanstack/react-virtual@3.14.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@tanstack/virtual-core': 3.17.0 + '@tanstack/virtual-core': 3.17.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) '@tanstack/store@0.11.0': {} - '@tanstack/virtual-core@3.17.0': {} + '@tanstack/virtual-core@3.17.1': {} '@teppeis/multimaps@3.0.0': {} '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -12774,11 +12799,11 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tsslint/cli@3.1.3(@tsslint/compat-eslint@3.1.3(typescript@6.0.3))(typescript@6.0.3)': + '@tsslint/cli@3.1.4(@tsslint/compat-eslint@3.1.4(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@tsslint/config': 3.1.3(@tsslint/compat-eslint@3.1.3(typescript@6.0.3)) - '@tsslint/core': 3.1.3 - '@tsslint/types': 3.1.3 + '@tsslint/config': 3.1.4(@tsslint/compat-eslint@3.1.4(typescript@6.0.3)) + '@tsslint/core': 3.1.4 + '@tsslint/types': 3.1.4 '@volar/language-core': 2.4.28 '@volar/language-hub': 0.0.1 '@volar/typescript': 2.4.28 @@ -12788,25 +12813,25 @@ snapshots: - '@tsslint/compat-eslint' - tsl - '@tsslint/compat-eslint@3.1.3(typescript@6.0.3)': + '@tsslint/compat-eslint@3.1.4(typescript@6.0.3)': dependencies: - '@tsslint/types': 3.1.3 + '@tsslint/types': 3.1.4 esquery: 1.7.0 typescript: 6.0.3 - '@tsslint/config@3.1.3(@tsslint/compat-eslint@3.1.3(typescript@6.0.3))': + '@tsslint/config@3.1.4(@tsslint/compat-eslint@3.1.4(typescript@6.0.3))': dependencies: - '@tsslint/types': 3.1.3 + '@tsslint/types': 3.1.4 minimatch: 10.2.5 optionalDependencies: - '@tsslint/compat-eslint': 3.1.3(typescript@6.0.3) + '@tsslint/compat-eslint': 3.1.4(typescript@6.0.3) - '@tsslint/core@3.1.3': + '@tsslint/core@3.1.4': dependencies: - '@tsslint/types': 3.1.3 + '@tsslint/types': 3.1.4 minimatch: 10.2.5 - '@tsslint/types@3.1.3': {} + '@tsslint/types@3.1.4': {} '@tybys/wasm-util@0.10.1': dependencies: @@ -12817,24 +12842,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/chai@5.2.3': dependencies: @@ -13005,12 +13030,17 @@ snapshots: '@types/node@25.9.3': dependencies: undici-types: 7.24.6 + optional: true + + '@types/node@25.9.4': + dependencies: + undici-types: 7.24.6 '@types/normalize-package-data@2.4.4': {} '@types/papaparse@5.5.2': dependencies: - '@types/node': 25.9.3 + '@types/node': 25.9.4 '@types/qs@6.15.1': {} @@ -13036,7 +13066,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.3 + '@types/node': 25.9.4 '@types/yauzl@2.10.3': dependencies: @@ -13045,12 +13075,12 @@ snapshots: '@types/zen-observable@0.8.3': {} - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.61.0 eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) @@ -13061,12 +13091,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.61.0 eslint: 10.5.0(jiti@2.7.0) @@ -13077,7 +13107,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.61.1 + eslint: 10.5.0(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/types': 8.61.0 @@ -13089,7 +13135,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/types': 8.61.0 @@ -13101,6 +13147,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.5.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.61.0(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@6.0.3) @@ -13110,16 +13168,34 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.61.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + debug: 4.4.3(supports-color@10.2.2) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.61.0': dependencies: '@typescript-eslint/types': 8.61.0 '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/scope-manager@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/tsconfig-utils@8.61.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.61.0 '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) @@ -13131,7 +13207,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.61.0 '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) @@ -13143,8 +13219,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@typescript-eslint/type-utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.5.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.61.0': {} + '@typescript-eslint/types@8.61.1': {} + '@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.61.0(typescript@6.0.3) @@ -13154,7 +13257,22 @@ snapshots: debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.8.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -13182,41 +13300,68 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + eslint: 10.5.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.61.0': dependencies: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260613.1': + '@typescript-eslint/visitor-keys@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + eslint-visitor-keys: 5.0.1 + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260613.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260613.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260613.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260613.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260613.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260613.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260620.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260613.1': + '@typescript/native-preview@7.0.0-dev.20260620.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260613.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260613.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260613.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260613.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260613.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260613.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260613.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260620.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260620.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260620.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260620.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260620.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260620.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260620.1 '@ungap/structured-clone@1.3.0': {} @@ -13224,13 +13369,13 @@ snapshots: dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@unpic/react@1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@unpic/core': 1.0.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@upsetjs/venn.js@2.0.0': optionalDependencies: @@ -13246,7 +13391,7 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/devtools-kit@0.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3)': + '@vitejs/devtools-kit@0.3.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3)': dependencies: '@devframes/hub': 0.5.2(devframe@0.5.2(typescript@6.0.3)) birpc: 4.0.0 @@ -13256,7 +13401,7 @@ snapshots: pathe: 2.0.3 perfect-debounce: 2.1.0 tinyexec: 1.2.3 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -13264,12 +13409,12 @@ snapshots: - typescript - utf-8-validate - '@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - '@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.1.0 @@ -13280,21 +13425,46 @@ snapshots: srvx: 0.11.15 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) optionalDependencies: react-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24)': + '@vitest/browser-playwright@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(playwright@1.61.0)(vitest@4.1.9)': + dependencies: + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + playwright: 1.61.0 + tinyrainbow: 3.1.0 + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - '@vitest/utils': 4.1.8 + '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@vitest/utils': 4.1.9 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -13302,10 +13472,10 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24)': + '@vitest/coverage-v8@4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.8 + '@vitest/utils': 4.1.9 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -13314,89 +13484,33 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) optionalDependencies: - '@vitest/browser': 4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(@voidzero-dev/vite-plus-test@0.1.24) + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) - '@vitest/eslint-plugin@1.6.17(@types/node@25.9.3)(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': + '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)(vitest@4.1.9)': dependencies: '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) typescript: 6.0.3 + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - - '@vitejs/devtools' - - '@vitest/coverage-istanbul' - - '@vitest/coverage-v8' - - '@vitest/ui' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - supports-color - - terser - - tsx - - unplugin-unused - - unrun - - utf-8-validate - - vite - - yaml - '@vitest/eslint-plugin@1.6.17(@types/node@25.9.3)(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(eslint@10.5.0(jiti@2.7.0))(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': + '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.9)': dependencies: '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - - '@vitejs/devtools' - - '@vitest/coverage-istanbul' - - '@vitest/coverage-v8' - - '@vitest/ui' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - supports-color - - terser - - tsx - - unplugin-unused - - unrun - - utf-8-validate - - vite - - yaml '@vitest/expect@3.2.4': dependencies: @@ -13406,27 +13520,48 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': + '@vitest/expect@4.1.9': dependencies: - '@vitest/spy': 4.1.8 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.8': + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.1.8': {} + '@vitest/spy@4.1.9': {} '@vitest/utils@3.2.4': dependencies: @@ -13434,20 +13569,20 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.8': + '@vitest/utils@4.1.9': dependencies: - '@vitest/pretty-format': 4.1.8 + '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': dependencies: - '@oxc-project/runtime': 0.133.0 - '@oxc-project/types': 0.133.0 + '@oxc-project/runtime': 0.136.0 + '@oxc-project/types': 0.136.0 lightningcss: 1.32.0 postcss: 8.5.15 optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 25.9.4 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 @@ -13455,70 +13590,28 @@ snapshots: typescript: 6.0.3 yaml: 2.9.0 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.24': + '@voidzero-dev/vite-plus-darwin-arm64@0.2.1': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.24': + '@voidzero-dev/vite-plus-darwin-x64@0.2.1': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.24': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.2.1': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.24': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.1': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.24': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.1': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.24': + '@voidzero-dev/vite-plus-linux-x64-musl@0.2.1': optional: true - '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - es-module-lexer: 1.7.0 - obug: 2.1.1 - pixelmatch: 7.1.0 - pngjs: 7.0.0 - sirv: 3.0.2 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.2.3 - tinyglobby: 0.2.16 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - ws: 8.21.0 - optionalDependencies: - '@types/node': 25.9.3 - '@vitest/coverage-v8': 4.1.8(@vitest/browser@4.1.8)(@voidzero-dev/vite-plus-test@0.1.24) - happy-dom: 20.10.3 - transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@tsdown/css' - - '@tsdown/exe' - - '@vitejs/devtools' - - bufferutil - - esbuild - - jiti - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - typescript - - unplugin-unused - - unrun - - utf-8-validate - - yaml - - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.24': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.1': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.24': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.2.1': optional: true '@volar/language-core@2.4.28': @@ -13537,7 +13630,7 @@ snapshots: '@vue/compiler-core@3.5.31': dependencies: - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.7 '@vue/shared': 3.5.31 entities: 7.0.1 estree-walker: 2.0.2 @@ -13752,7 +13845,7 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 25.9.3 + '@types/node': 25.9.4 buffer@5.7.1: dependencies: @@ -13841,6 +13934,8 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: {} + chalk@4.1.1: dependencies: ansi-styles: 4.3.0 @@ -13885,7 +13980,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.26.0 + undici: 7.27.2 whatwg-mimetype: 4.0.0 chokidar@5.0.0: @@ -13951,14 +14046,14 @@ snapshots: - '@types/react' - '@types/react-dom' - code-inspector-plugin@1.6.0(supports-color@10.2.2): + code-inspector-plugin@1.6.1(supports-color@10.2.2): dependencies: - '@code-inspector/core': 1.6.0(supports-color@10.2.2) - '@code-inspector/esbuild': 1.6.0 - '@code-inspector/mako': 1.6.0 - '@code-inspector/turbopack': 1.6.0 - '@code-inspector/vite': 1.6.0 - '@code-inspector/webpack': 1.6.0 + '@code-inspector/core': 1.6.1(supports-color@10.2.2) + '@code-inspector/esbuild': 1.6.1 + '@code-inspector/mako': 1.6.1 + '@code-inspector/turbopack': 1.6.1 + '@code-inspector/vite': 1.6.1 + '@code-inspector/webpack': 1.6.1 chalk: 4.1.1 transitivePeerDependencies: - supports-color @@ -14022,7 +14117,7 @@ snapshots: dependencies: layout-base: 2.0.1 - cron-parser@5.5.0: + cron-parser@5.6.0: dependencies: luxon: 3.7.2 @@ -14388,7 +14483,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.10: + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -14708,7 +14803,7 @@ snapshots: dependencies: eslint: 10.5.0(jiti@2.7.0) - eslint-plugin-better-tailwindcss@4.6.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tailwindcss@4.3.1)(typescript@6.0.3): + eslint-plugin-better-tailwindcss@4.6.0(eslint@10.5.0(jiti@2.7.0))(oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(tailwindcss@4.3.1)(typescript@6.0.3): dependencies: '@eslint/css-tree': 4.0.1 '@valibot/to-json-schema': 1.7.0(valibot@1.4.1(typescript@6.0.3)) @@ -14721,23 +14816,23 @@ snapshots: valibot: 1.4.1(typescript@6.0.3) optionalDependencies: eslint: 10.5.0(jiti@2.7.0) - oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + oxlint: 1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3))(@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-command@3.5.2(@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3))(@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)): dependencies: '@es-joy/jsdoccomment': 0.84.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) eslint-plugin-es-x@7.8.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): @@ -15007,8 +15102,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) compare-versions: 6.1.1 eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 @@ -15022,8 +15117,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 eslint: 10.5.0(jiti@2.7.0) typescript: 6.0.3 @@ -15037,8 +15132,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: @@ -15052,8 +15147,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: @@ -15065,8 +15160,8 @@ snapshots: '@eslint-react/core': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -15080,8 +15175,8 @@ snapshots: '@eslint-react/core': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 @@ -15104,8 +15199,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: @@ -15119,8 +15214,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: @@ -15133,8 +15228,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) birecord: 0.1.1 eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) ts-pattern: 5.9.0 @@ -15150,8 +15245,8 @@ snapshots: '@eslint-react/eslint': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) birecord: 0.1.1 eslint: 10.5.0(jiti@2.7.0) ts-pattern: 5.9.0 @@ -15167,11 +15262,11 @@ snapshots: '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) compare-versions: 6.1.1 eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) string-ts: 2.3.1 @@ -15190,11 +15285,11 @@ snapshots: '@eslint-react/jsx': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/shared': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) '@eslint-react/var': 5.9.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 eslint: 10.5.0(jiti@2.7.0) string-ts: 2.3.1 @@ -15226,7 +15321,7 @@ snapshots: regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.3(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-sonarjs@4.1.0(eslint@10.5.0(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 @@ -15241,12 +15336,13 @@ snapshots: semver: 7.8.4 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 + yaml: 2.9.0 - eslint-plugin-storybook@10.4.4(eslint@10.5.0(jiti@2.7.0))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3): + eslint-plugin-storybook@10.4.6(eslint@10.5.0(jiti@2.7.0))(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(typescript@6.0.3): dependencies: '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.5.0(jiti@2.7.0) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - typescript @@ -15313,19 +15409,19 @@ snapshots: semver: 7.8.4 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)): dependencies: eslint: 10.5.0(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)): + eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2) @@ -15337,7 +15433,7 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)))(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.5.0(jiti@2.7.0))): dependencies: @@ -15351,7 +15447,7 @@ snapshots: xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@10.5.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-yml@3.3.2(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: @@ -15538,6 +15634,8 @@ snapshots: expand-template@2.0.3: optional: true + expect-type@1.3.0: {} + exsolve@1.0.8: {} extend@3.0.2: {} @@ -15640,7 +15738,7 @@ snapshots: dependencies: fd-package-json: 2.0.0 - foxact@0.3.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + foxact@0.3.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: client-only: 0.0.1 event-target-bus: 1.0.0 @@ -15773,9 +15871,9 @@ snapshots: hachure-fill@0.5.2: {} - happy-dom@20.10.3: + happy-dom@20.10.6: dependencies: - '@types/node': 25.9.3 + '@types/node': 25.9.4 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -15961,7 +16059,7 @@ snapshots: hex-rgb@4.3.0: {} - hono@4.12.25: {} + hono@4.12.26: {} hosted-git-info@9.0.2: dependencies: @@ -16307,19 +16405,19 @@ snapshots: khroma@2.1.0: {} - knip@6.16.1: + knip@6.17.1: dependencies: fdir: 6.5.0(picomatch@4.0.4) formatly: 0.3.0 get-tsconfig: 4.14.0 jiti: 2.7.0 - oxc-parser: 0.133.0 + oxc-parser: 0.135.0 oxc-resolver: 11.20.0 picomatch: 4.0.4 smol-toml: 1.6.1 strip-json-comments: 5.0.3 - tinyglobby: 0.2.16 - unbash: 3.0.0 + tinyglobby: 0.2.17 + unbash: 4.0.1 yaml: 2.9.0 zod: 4.4.3 @@ -16453,7 +16551,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loro-crdt@1.13.2: {} + loro-crdt@1.13.5: {} loupe@3.2.1: {} @@ -16473,8 +16571,8 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-dir@4.0.0: @@ -16718,7 +16816,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.21 - dompurify: 3.4.10 + dompurify: 3.4.11 es-toolkit: 1.47.1 katex: 0.16.47 khroma: 2.1.0 @@ -16726,7 +16824,7 @@ snapshots: roughjs: 4.6.6 stylis: 4.3.6 ts-dedent: 2.2.0 - uuid: 14.0.0 + uuid: 14.0.1 micromark-core-commonmark@2.0.3: dependencies: @@ -17109,7 +17207,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.2.9 '@swc/helpers': 0.5.15 @@ -17128,7 +17226,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.9 '@next/swc-win32-arm64-msvc': 16.2.9 '@next/swc-win32-x64-msvc': 16.2.9 - '@playwright/test': 1.60.0 + '@playwright/test': 1.61.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -17164,12 +17262,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): + nuqs@2.8.9(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.7 optionalDependencies: - next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) object-assign@4.1.1: {} @@ -17316,30 +17414,30 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.132.0 '@oxc-parser/binding-win32-x64-msvc': 0.132.0 - oxc-parser@0.133.0: + oxc-parser@0.135.0: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.135.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.133.0 - '@oxc-parser/binding-android-arm64': 0.133.0 - '@oxc-parser/binding-darwin-arm64': 0.133.0 - '@oxc-parser/binding-darwin-x64': 0.133.0 - '@oxc-parser/binding-freebsd-x64': 0.133.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.133.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.133.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.133.0 - '@oxc-parser/binding-linux-arm64-musl': 0.133.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.133.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.133.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.133.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.133.0 - '@oxc-parser/binding-linux-x64-gnu': 0.133.0 - '@oxc-parser/binding-linux-x64-musl': 0.133.0 - '@oxc-parser/binding-openharmony-arm64': 0.133.0 - '@oxc-parser/binding-wasm32-wasi': 0.133.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.133.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.133.0 - '@oxc-parser/binding-win32-x64-msvc': 0.133.0 + '@oxc-parser/binding-android-arm-eabi': 0.135.0 + '@oxc-parser/binding-android-arm64': 0.135.0 + '@oxc-parser/binding-darwin-arm64': 0.135.0 + '@oxc-parser/binding-darwin-x64': 0.135.0 + '@oxc-parser/binding-freebsd-x64': 0.135.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.135.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.135.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.135.0 + '@oxc-parser/binding-linux-arm64-musl': 0.135.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.135.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.135.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.135.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.135.0 + '@oxc-parser/binding-linux-x64-gnu': 0.135.0 + '@oxc-parser/binding-linux-x64-musl': 0.135.0 + '@oxc-parser/binding-openharmony-arm64': 0.135.0 + '@oxc-parser/binding-wasm32-wasi': 0.135.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.135.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.135.0 + '@oxc-parser/binding-win32-x64-msvc': 0.135.0 oxc-resolver@11.20.0: optionalDependencies: @@ -17363,55 +17461,30 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 '@oxc-resolver/binding-win32-x64-msvc': 11.20.0 - oxfmt@0.52.0(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): + oxfmt@0.55.0(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.52.0 - '@oxfmt/binding-android-arm64': 0.52.0 - '@oxfmt/binding-darwin-arm64': 0.52.0 - '@oxfmt/binding-darwin-x64': 0.52.0 - '@oxfmt/binding-freebsd-x64': 0.52.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.52.0 - '@oxfmt/binding-linux-arm64-gnu': 0.52.0 - '@oxfmt/binding-linux-arm64-musl': 0.52.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-musl': 0.52.0 - '@oxfmt/binding-linux-s390x-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-musl': 0.52.0 - '@oxfmt/binding-openharmony-arm64': 0.52.0 - '@oxfmt/binding-win32-arm64-msvc': 0.52.0 - '@oxfmt/binding-win32-ia32-msvc': 0.52.0 - '@oxfmt/binding-win32-x64-msvc': 0.52.0 - vite-plus: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - - oxfmt@0.52.0(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): - dependencies: - tinypool: 2.1.0 - optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.52.0 - '@oxfmt/binding-android-arm64': 0.52.0 - '@oxfmt/binding-darwin-arm64': 0.52.0 - '@oxfmt/binding-darwin-x64': 0.52.0 - '@oxfmt/binding-freebsd-x64': 0.52.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.52.0 - '@oxfmt/binding-linux-arm64-gnu': 0.52.0 - '@oxfmt/binding-linux-arm64-musl': 0.52.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-musl': 0.52.0 - '@oxfmt/binding-linux-s390x-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-musl': 0.52.0 - '@oxfmt/binding-openharmony-arm64': 0.52.0 - '@oxfmt/binding-win32-arm64-msvc': 0.52.0 - '@oxfmt/binding-win32-ia32-msvc': 0.52.0 - '@oxfmt/binding-win32-x64-msvc': 0.52.0 - vite-plus: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + '@oxfmt/binding-android-arm-eabi': 0.55.0 + '@oxfmt/binding-android-arm64': 0.55.0 + '@oxfmt/binding-darwin-arm64': 0.55.0 + '@oxfmt/binding-darwin-x64': 0.55.0 + '@oxfmt/binding-freebsd-x64': 0.55.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.55.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.55.0 + '@oxfmt/binding-linux-arm64-gnu': 0.55.0 + '@oxfmt/binding-linux-arm64-musl': 0.55.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.55.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.55.0 + '@oxfmt/binding-linux-riscv64-musl': 0.55.0 + '@oxfmt/binding-linux-s390x-gnu': 0.55.0 + '@oxfmt/binding-linux-x64-gnu': 0.55.0 + '@oxfmt/binding-linux-x64-musl': 0.55.0 + '@oxfmt/binding-openharmony-arm64': 0.55.0 + '@oxfmt/binding-win32-arm64-msvc': 0.55.0 + '@oxfmt/binding-win32-ia32-msvc': 0.55.0 + '@oxfmt/binding-win32-x64-msvc': 0.55.0 + vite-plus: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) oxlint-tsgolint@0.23.0: optionalDependencies: @@ -17422,53 +17495,29 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.23.0 '@oxlint-tsgolint/win32-x64': 0.23.0 - oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): + oxlint@1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.67.0 - '@oxlint/binding-android-arm64': 1.67.0 - '@oxlint/binding-darwin-arm64': 1.67.0 - '@oxlint/binding-darwin-x64': 1.67.0 - '@oxlint/binding-freebsd-x64': 1.67.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.67.0 - '@oxlint/binding-linux-arm-musleabihf': 1.67.0 - '@oxlint/binding-linux-arm64-gnu': 1.67.0 - '@oxlint/binding-linux-arm64-musl': 1.67.0 - '@oxlint/binding-linux-ppc64-gnu': 1.67.0 - '@oxlint/binding-linux-riscv64-gnu': 1.67.0 - '@oxlint/binding-linux-riscv64-musl': 1.67.0 - '@oxlint/binding-linux-s390x-gnu': 1.67.0 - '@oxlint/binding-linux-x64-gnu': 1.67.0 - '@oxlint/binding-linux-x64-musl': 1.67.0 - '@oxlint/binding-openharmony-arm64': 1.67.0 - '@oxlint/binding-win32-arm64-msvc': 1.67.0 - '@oxlint/binding-win32-ia32-msvc': 1.67.0 - '@oxlint/binding-win32-x64-msvc': 1.67.0 + '@oxlint/binding-android-arm-eabi': 1.70.0 + '@oxlint/binding-android-arm64': 1.70.0 + '@oxlint/binding-darwin-arm64': 1.70.0 + '@oxlint/binding-darwin-x64': 1.70.0 + '@oxlint/binding-freebsd-x64': 1.70.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.70.0 + '@oxlint/binding-linux-arm-musleabihf': 1.70.0 + '@oxlint/binding-linux-arm64-gnu': 1.70.0 + '@oxlint/binding-linux-arm64-musl': 1.70.0 + '@oxlint/binding-linux-ppc64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-musl': 1.70.0 + '@oxlint/binding-linux-s390x-gnu': 1.70.0 + '@oxlint/binding-linux-x64-gnu': 1.70.0 + '@oxlint/binding-linux-x64-musl': 1.70.0 + '@oxlint/binding-openharmony-arm64': 1.70.0 + '@oxlint/binding-win32-arm64-msvc': 1.70.0 + '@oxlint/binding-win32-ia32-msvc': 1.70.0 + '@oxlint/binding-win32-x64-msvc': 1.70.0 oxlint-tsgolint: 0.23.0 - vite-plus: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - - oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.67.0 - '@oxlint/binding-android-arm64': 1.67.0 - '@oxlint/binding-darwin-arm64': 1.67.0 - '@oxlint/binding-darwin-x64': 1.67.0 - '@oxlint/binding-freebsd-x64': 1.67.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.67.0 - '@oxlint/binding-linux-arm-musleabihf': 1.67.0 - '@oxlint/binding-linux-arm64-gnu': 1.67.0 - '@oxlint/binding-linux-arm64-musl': 1.67.0 - '@oxlint/binding-linux-ppc64-gnu': 1.67.0 - '@oxlint/binding-linux-riscv64-gnu': 1.67.0 - '@oxlint/binding-linux-riscv64-musl': 1.67.0 - '@oxlint/binding-linux-s390x-gnu': 1.67.0 - '@oxlint/binding-linux-x64-gnu': 1.67.0 - '@oxlint/binding-linux-x64-musl': 1.67.0 - '@oxlint/binding-openharmony-arm64': 1.67.0 - '@oxlint/binding-win32-arm64-msvc': 1.67.0 - '@oxlint/binding-win32-ia32-msvc': 1.67.0 - '@oxlint/binding-win32-x64-msvc': 1.67.0 - oxlint-tsgolint: 0.23.0 - vite-plus: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + vite-plus: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) p-limit@3.1.0: dependencies: @@ -17573,10 +17622,6 @@ snapshots: pinyin-pro@3.28.1: {} - pixelmatch@7.1.0: - dependencies: - pngjs: 7.0.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -17589,11 +17634,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.60.0: {} + playwright-core@1.61.0: {} - playwright@1.60.0: + playwright@1.61.0: dependencies: - playwright-core: 1.60.0 + playwright-core: 1.61.0 optionalDependencies: fsevents: 2.3.2 @@ -17721,8 +17766,8 @@ snapshots: react-docgen@8.0.3: dependencies: '@babel/core': 7.29.7 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 @@ -18245,37 +18290,37 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - sharp@0.35.1: + sharp@0.35.2: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.4 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.1 - '@img/sharp-darwin-x64': 0.35.1 - '@img/sharp-freebsd-wasm32': 0.35.1 - '@img/sharp-libvips-darwin-arm64': 1.3.0 - '@img/sharp-libvips-darwin-x64': 1.3.0 - '@img/sharp-libvips-linux-arm': 1.3.0 - '@img/sharp-libvips-linux-arm64': 1.3.0 - '@img/sharp-libvips-linux-ppc64': 1.3.0 - '@img/sharp-libvips-linux-riscv64': 1.3.0 - '@img/sharp-libvips-linux-s390x': 1.3.0 - '@img/sharp-libvips-linux-x64': 1.3.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 - '@img/sharp-linux-arm': 0.35.1 - '@img/sharp-linux-arm64': 0.35.1 - '@img/sharp-linux-ppc64': 0.35.1 - '@img/sharp-linux-riscv64': 0.35.1 - '@img/sharp-linux-s390x': 0.35.1 - '@img/sharp-linux-x64': 0.35.1 - '@img/sharp-linuxmusl-arm64': 0.35.1 - '@img/sharp-linuxmusl-x64': 0.35.1 - '@img/sharp-webcontainers-wasm32': 0.35.1 - '@img/sharp-win32-arm64': 0.35.1 - '@img/sharp-win32-ia32': 0.35.1 - '@img/sharp-win32-x64': 0.35.1 + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 shebang-command@2.0.0: dependencies: @@ -18296,6 +18341,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -18376,6 +18423,8 @@ snapshots: srvx@0.11.15: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} state-local@1.0.7: {} @@ -18391,7 +18440,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): + storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -18410,7 +18459,7 @@ snapshots: ws: 8.21.0 optionalDependencies: '@types/react': 19.2.17 - vite-plus: 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + vite-plus: 0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -18606,6 +18655,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@2.1.0: {} tinyrainbow@2.0.0: {} @@ -18614,11 +18668,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.4.2: {} + tldts-core@7.4.3: {} - tldts@7.4.2: + tldts@7.4.3: dependencies: - tldts-core: 7.4.2 + tldts-core: 7.4.3 to-regex-range@5.0.1: dependencies: @@ -18740,7 +18794,7 @@ snapshots: uglify-js@3.19.3: {} - unbash@3.0.0: {} + unbash@4.0.1: {} unbox-primitive@1.1.0: dependencies: @@ -18751,10 +18805,10 @@ snapshots: undici-types@7.24.6: {} - undici@7.26.0: {} - undici@7.27.2: {} + undici@7.28.0: {} + unicode-trie@2.0.0: dependencies: pako: 0.2.9 @@ -18890,7 +18944,7 @@ snapshots: util-deprecate@1.0.2: {} - uuid@14.0.0: {} + uuid@14.0.1: {} valibot@1.4.1(typescript@6.0.3): optionalDependencies: @@ -18916,23 +18970,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.1.2(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3): + vinext@0.1.6(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@unpic/react': 1.0.2(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.2(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@vitejs/plugin-react': 6.0.2(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) image-size: 2.0.2 ipaddr.js: 2.4.0 magic-string: 0.30.21 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3) web-vitals: 4.2.4 optionalDependencies: '@mdx-js/rollup': 3.1.1 - '@vitejs/plugin-rsc': 0.5.27(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + '@vitejs/plugin-rsc': 0.5.27(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) react-server-dom-webpack: 19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - next @@ -18952,9 +19006,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.0-beta.3(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3): + vite-plugin-inspect@12.0.0-beta.3(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3): dependencies: - '@vitejs/devtools-kit': 0.3.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) + '@vitejs/devtools-kit': 0.3.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(typescript@6.0.3) ansis: 4.3.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -18963,7 +19017,7 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -18971,39 +19025,49 @@ snapshots: - typescript - utf-8-validate - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(next@16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)))(supports-color@10.2.2)(typescript@6.0.3): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - storybook: 10.4.4(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + next: 16.2.9(@babel/core@7.29.7)(@playwright/test@1.61.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3) + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0): + vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0): dependencies: - '@oxc-project/types': 0.133.0 - '@oxlint/plugins': 1.61.0 - '@voidzero-dev/vite-plus-core': 0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - '@voidzero-dev/vite-plus-test': 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - oxfmt: 0.52.0(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@oxc-project/types': 0.136.0 + '@oxlint/plugins': 1.68.0 + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + '@voidzero-dev/vite-plus-core': 0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + oxfmt: 0.55.0(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + oxlint: 1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) oxlint-tsgolint: 0.23.0 + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.24 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.24 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.24 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.24 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.24 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.24 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.24 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.24 + '@vitest/browser-playwright': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(playwright@1.61.0)(vitest@4.1.9) + '@voidzero-dev/vite-plus-darwin-arm64': 0.2.1 + '@voidzero-dev/vite-plus-darwin-x64': 0.2.1 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.2.1 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.2.1 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.2.1 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.2.1 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.2.1 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.2.1 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -19021,6 +19085,7 @@ snapshots: - jiti - jsdom - less + - msw - publint - sass - sass-embedded @@ -19036,95 +19101,76 @@ snapshots: - vite - yaml - vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0): - dependencies: - '@oxc-project/types': 0.133.0 - '@oxlint/plugins': 1.61.0 - '@voidzero-dev/vite-plus-core': 0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - '@voidzero-dev/vite-plus-test': 0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) - oxfmt: 0.52.0(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) - oxlint-tsgolint: 0.23.0 - optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.24 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.24 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.24 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.24 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.24 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.24 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.24 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.24 - transitivePeerDependencies: - - '@arethetypeswrong/core' - - '@edge-runtime/vm' - - '@opentelemetry/api' - - '@tsdown/css' - - '@tsdown/exe' - - '@types/node' - - '@vitejs/devtools' - - '@vitest/coverage-istanbul' - - '@vitest/coverage-v8' - - '@vitest/ui' - - bufferutil - - esbuild - - happy-dom - - jiti - - jsdom - - less - - publint - - sass - - sass-embedded - - stylus - - sugarss - - svelte - - terser - - tsx - - typescript - - unplugin-unused - - unrun - - utf-8-validate - - vite - - yaml - - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3): dependencies: debug: 4.4.3(supports-color@10.2.2) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.3) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(supports-color@10.2.2)(typescript@6.0.3): dependencies: debug: 4.4.3(supports-color@10.2.2) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.3) - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - supports-color - typescript - vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' - vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-test@0.1.24)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9): dependencies: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) optionalDependencies: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.24): + vitest-canvas-mock@1.1.4(vitest@4.1.9): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.3)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + vitest: 4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6) + + vitest@4.1.9(@types/node@25.9.4)(@vitest/browser-playwright@4.1.9)(@vitest/browser-preview@4.1.9)(@vitest/coverage-v8@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(happy-dom@20.10.6): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.2.3 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0)' + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.4 + '@vitest/browser-playwright': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(playwright@1.61.0)(vitest@4.1.9) + '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@vitest/coverage-v8': 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) + happy-dom: 20.10.6 + transitivePeerDependencies: + - msw void-elements@3.1.0: {} @@ -19219,6 +19265,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@9.0.2: @@ -19332,7 +19383,7 @@ time: '@formatjs/intl-localematcher@0.8.10': '2026-06-04T15:24:22.451Z' '@heroicons/react@2.2.0': '2024-11-18T15:33:27.317Z' '@hey-api/openapi-ts@0.98.2': '2026-06-08T05:37:17.524Z' - '@hono/node-server@2.0.4': '2026-05-24T08:20:41.156Z' + '@hono/node-server@2.0.5': '2026-06-15T12:55:56.300Z' '@iconify-json/heroicons@1.2.3': '2025-09-20T05:33:02.364Z' '@iconify-json/ri@1.2.10': '2026-02-10T08:41:46.666Z' '@lexical/link@0.45.0': '2026-05-28T20:34:31.715Z' @@ -19352,19 +19403,19 @@ time: '@orpc/contract@1.14.6': '2026-06-12T04:16:50.143Z' '@orpc/openapi-client@1.14.6': '2026-06-12T04:17:49.271Z' '@orpc/tanstack-query@1.14.6': '2026-06-12T04:17:13.682Z' - '@playwright/test@1.60.0': '2026-05-11T19:09:45.394Z' + '@playwright/test@1.61.0': '2026-06-15T10:06:35.237Z' '@remixicon/react@4.9.0': '2026-01-29T10:53:18.993Z' '@rgrove/parse-xml@4.2.0': '2024-10-25T03:58:22.145Z' - '@sentry/react@10.57.0': '2026-06-09T09:44:56.173Z' - '@storybook/addon-a11y@10.4.4': '2026-06-11T11:47:24.917Z' - '@storybook/addon-docs@10.4.4': '2026-06-11T11:47:25.111Z' - '@storybook/addon-links@10.4.4': '2026-06-11T11:47:29.814Z' - '@storybook/addon-onboarding@10.4.4': '2026-06-11T11:47:29.674Z' - '@storybook/addon-themes@10.4.4': '2026-06-11T11:47:34.054Z' - '@storybook/addon-vitest@10.4.4': '2026-06-11T11:47:37.457Z' - '@storybook/nextjs-vite@10.4.4': '2026-06-11T11:47:57.869Z' - '@storybook/react-vite@10.4.4': '2026-06-11T11:48:05.555Z' - '@storybook/react@10.4.4': '2026-06-11T11:48:50.264Z' + '@sentry/react@10.59.0': '2026-06-19T12:52:07.460Z' + '@storybook/addon-a11y@10.4.6': '2026-06-16T11:41:54.974Z' + '@storybook/addon-docs@10.4.6': '2026-06-16T11:41:55.049Z' + '@storybook/addon-links@10.4.6': '2026-06-16T11:42:01.532Z' + '@storybook/addon-onboarding@10.4.6': '2026-06-16T11:42:01.175Z' + '@storybook/addon-themes@10.4.6': '2026-06-16T11:42:06.465Z' + '@storybook/addon-vitest@10.4.6': '2026-06-16T11:42:09.951Z' + '@storybook/nextjs-vite@10.4.6': '2026-06-16T11:42:31.117Z' + '@storybook/react-vite@10.4.6': '2026-06-16T11:42:40.527Z' + '@storybook/react@10.4.6': '2026-06-16T11:43:30.413Z' '@streamdown/math@1.0.2': '2026-02-09T17:31:31.085Z' '@svgdotjs/svg.js@3.2.5': '2025-09-15T16:22:12.771Z' '@t3-oss/env-nextjs@0.13.11': '2026-03-22T19:16:09.026Z' @@ -19376,32 +19427,32 @@ time: '@tanstack/react-form@1.33.0': '2026-05-28T17:05:42.660Z' '@tanstack/react-hotkeys@0.10.0': '2026-04-25T12:28:06.989Z' '@tanstack/react-query@5.101.0': '2026-06-02T19:24:39.383Z' - '@tanstack/react-virtual@3.14.2': '2026-06-02T07:27:48.537Z' + '@tanstack/react-virtual@3.14.3': '2026-06-15T19:53:08.471Z' '@testing-library/dom@10.4.1': '2025-07-27T13:23:37.151Z' '@testing-library/jest-dom@6.9.1': '2025-10-01T20:04:22.720Z' '@testing-library/react@16.3.2': '2026-01-19T10:59:08.185Z' '@testing-library/user-event@14.6.1': '2025-01-21T17:35:55.574Z' - '@tsslint/cli@3.1.3': '2026-05-16T14:17:58.782Z' - '@tsslint/compat-eslint@3.1.3': '2026-05-16T14:17:54.194Z' - '@tsslint/config@3.1.3': '2026-05-16T14:17:56.687Z' + '@tsslint/cli@3.1.4': '2026-06-16T18:21:29.075Z' + '@tsslint/compat-eslint@3.1.4': '2026-06-16T18:21:23.786Z' + '@tsslint/config@3.1.4': '2026-06-16T18:21:26.545Z' '@types/js-cookie@3.0.6': '2023-11-07T08:41:16.889Z' '@types/js-yaml@4.0.9': '2023-11-07T20:20:13.264Z' '@types/lockfile@1.0.4': '2023-11-07T20:23:21.070Z' '@types/negotiator@0.6.4': '2025-06-07T02:18:17.532Z' - '@types/node@25.9.3': '2026-06-10T22:15:10.607Z' + '@types/node@25.9.4': '2026-06-19T07:15:05.196Z' '@types/qs@6.15.1': '2026-05-06T23:46:01.024Z' '@types/react-dom@19.2.3': '2025-11-12T04:37:39.524Z' '@types/react@19.2.17': '2026-06-05T20:10:24.692Z' '@types/sortablejs@1.15.9': '2025-10-24T04:31:45.132Z' - '@typescript-eslint/eslint-plugin@8.61.0': '2026-06-08T18:01:21.196Z' - '@typescript-eslint/parser@8.61.0': '2026-06-08T18:00:59.869Z' - '@typescript/native-preview@7.0.0-dev.20260613.1': '2026-06-13T08:01:38.753Z' + '@typescript-eslint/eslint-plugin@8.61.1': '2026-06-15T18:31:50.659Z' + '@typescript-eslint/parser@8.61.1': '2026-06-15T18:31:27.695Z' + '@typescript/native-preview@7.0.0-dev.20260620.1': '2026-06-20T08:01:54.980Z' '@vitejs/plugin-react@6.0.2': '2026-05-14T20:03:24.044Z' '@vitejs/plugin-rsc@0.5.27': '2026-06-01T09:07:55.389Z' - '@vitest/browser@4.1.8': '2026-06-01T08:14:46.126Z' - '@vitest/coverage-v8@4.1.8': '2026-06-01T08:15:09.012Z' - '@voidzero-dev/vite-plus-core@0.1.24': '2026-06-01T13:04:55.392Z' - '@voidzero-dev/vite-plus-test@0.1.24': '2026-06-01T13:05:02.584Z' + '@vitest/browser-playwright@4.1.9': '2026-06-15T07:21:50.860Z' + '@vitest/browser@4.1.9': '2026-06-15T07:21:58.373Z' + '@vitest/coverage-v8@4.1.9': '2026-06-15T07:21:14.145Z' + '@voidzero-dev/vite-plus-core@0.2.1': '2026-06-18T05:33:26.237Z' abcjs@6.6.3: '2026-04-24T17:38:01.079Z' agentation@3.0.2: '2026-03-25T16:24:19.682Z' ahooks@3.9.7: '2026-03-23T15:49:13.605Z' @@ -19411,13 +19462,13 @@ time: cli-table3@0.6.5: '2024-05-12T16:36:50.079Z' clsx@2.1.1: '2024-04-23T05:26:04.645Z' cmdk@1.1.1: '2025-03-14T19:21:16.194Z' - code-inspector-plugin@1.6.0: '2026-06-12T11:45:35.134Z' + code-inspector-plugin@1.6.1: '2026-06-15T01:14:26.288Z' concurrently@10.0.3: '2026-06-02T04:31:54.180Z' copy-to-clipboard@4.0.2: '2026-04-24T22:15:18.933Z' - cron-parser@5.5.0: '2026-01-16T13:14:50.225Z' + cron-parser@5.6.0: '2026-06-20T18:24:08.824Z' dayjs@1.11.21: '2026-05-26T05:46:12.427Z' decimal.js@10.6.0: '2025-07-06T22:50:38.844Z' - dompurify@3.4.10: '2026-06-12T12:49:46.101Z' + dompurify@3.4.11: '2026-06-17T10:33:28.065Z' echarts-for-react@3.0.6: '2026-01-21T04:38:21.243Z' echarts@6.1.0: '2026-05-19T17:52:11.076Z' elkjs@0.11.1: '2026-03-03T12:21:48.463Z' @@ -19433,16 +19484,16 @@ time: eslint-plugin-markdown-preferences@0.41.1: '2026-04-09T23:28:41.552Z' eslint-plugin-no-barrel-files@1.3.1: '2026-04-12T18:28:18.653Z' eslint-plugin-react-refresh@0.5.3: '2026-06-14T12:46:34.395Z' - eslint-plugin-sonarjs@4.0.3: '2026-04-16T08:09:42.856Z' - eslint-plugin-storybook@10.4.4: '2026-06-11T11:48:35.390Z' + eslint-plugin-sonarjs@4.1.0: '2026-06-18T17:14:11.087Z' + eslint-plugin-storybook@10.4.6: '2026-06-16T11:43:14.113Z' eslint@10.5.0: '2026-06-12T17:54:40.577Z' eventsource-parser@3.1.0: '2026-05-27T20:55:51.466Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' - foxact@0.3.5: '2026-06-11T15:55:09.889Z' + foxact@0.3.7: '2026-06-17T18:34:56.297Z' fuse.js@7.4.2: '2026-06-05T22:22:52.388Z' - happy-dom@20.10.3: '2026-06-12T22:45:20.950Z' + happy-dom@20.10.6: '2026-06-17T23:41:40.697Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' - hono@4.12.25: '2026-06-09T03:28:50.819Z' + hono@4.12.26: '2026-06-18T02:18:42.144Z' html-entities@2.6.0: '2025-03-30T15:40:10.885Z' html-to-image@1.11.13: '2025-02-14T01:43:48.709Z' i18next-resources-to-backend@1.2.1: '2024-04-10T19:22:23.117Z' @@ -19457,13 +19508,13 @@ time: js-yaml@4.2.0: '2026-05-31T22:17:13.783Z' jsonschema@1.5.0: '2025-01-07T15:09:11.287Z' katex@0.17.0: '2026-05-22T08:06:26.967Z' - knip@6.16.1: '2026-06-06T17:52:39.499Z' + knip@6.17.1: '2026-06-16T06:18:07.787Z' ky@2.0.2: '2026-04-21T08:58:46.923Z' lamejs@1.2.1: '2021-12-02T15:44:40.036Z' lexical-code-no-prism@0.41.0: '2026-03-08T16:50:40.266Z' lexical@0.45.0: '2026-05-28T20:33:56.686Z' lockfile@1.0.4: '2018-04-17T00:36:12.565Z' - loro-crdt@1.13.2: '2026-06-12T05:06:09.528Z' + loro-crdt@1.13.5: '2026-06-21T00:28:46.635Z' mermaid@11.15.0: '2026-05-11T11:15:09.824Z' mime@4.1.0: '2025-09-12T17:53:01.376Z' mitt@3.0.1: '2023-07-04T17:31:47.638Z' @@ -19476,7 +19527,7 @@ time: ora@9.4.0: '2026-04-22T06:11:17.972Z' picocolors@1.1.1: '2024-10-16T18:20:03.921Z' pinyin-pro@3.28.1: '2026-04-10T09:18:57.903Z' - playwright@1.60.0: '2026-05-11T19:09:33.114Z' + playwright@1.61.0: '2026-06-15T10:06:22.269Z' postcss@8.5.15: '2026-05-19T09:51:29.843Z' qrcode.react@4.2.0: '2024-12-11T17:22:40.569Z' qs@6.15.2: '2026-05-16T23:19:19.539Z' @@ -19495,29 +19546,30 @@ time: remark-directive@4.0.0: '2025-02-27T15:15:20.630Z' scheduler@0.27.0: '2025-10-01T21:39:15.208Z' server-only@0.0.1: '2022-09-03T01:07:26.139Z' - sharp@0.35.1: '2026-06-11T17:10:33.369Z' + sharp@0.35.2: '2026-06-19T13:47:27.073Z' shiki@4.2.0: '2026-06-03T01:35:47.302Z' socket.io-client@4.8.3: '2025-12-23T16:39:16.428Z' sortablejs@1.15.7: '2026-02-11T22:42:31.720Z' std-semver@1.0.8: '2026-03-09T17:23:55.795Z' - storybook@10.4.4: '2026-06-11T11:47:45.294Z' + storybook@10.4.6: '2026-06-16T11:42:18.729Z' streamdown@2.5.0: '2026-03-17T17:35:05.216Z' string-ts@2.3.1: '2025-11-28T17:33:10.099Z' tailwind-merge@3.6.0: '2026-05-10T12:56:43.142Z' tailwindcss@4.3.1: '2026-06-12T17:59:19.225Z' - tldts@7.4.2: '2026-05-30T09:56:28.759Z' + tldts@7.4.3: '2026-06-15T14:51:15.009Z' tsx@4.22.4: '2026-05-31T12:22:19.330Z' typescript@6.0.3: '2026-04-16T23:38:27.905Z' uglify-js@3.19.3: '2024-08-29T13:49:01.316Z' - undici@7.27.2: '2026-06-06T08:21:50.946Z' + undici@7.28.0: '2026-06-15T15:51:12.886Z' unist-util-visit@5.1.0: '2026-01-22T19:02:58.977Z' use-context-selector@2.0.0: '2024-05-06T11:23:59.259Z' - uuid@14.0.0: '2026-04-19T15:15:42.302Z' - vinext@0.1.2: '2026-06-12T10:31:09.245Z' + uuid@14.0.1: '2026-06-20T11:56:02.499Z' + vinext@0.1.6: '2026-06-19T15:54:13.598Z' vite-plugin-inspect@12.0.0-beta.3: '2026-05-29T06:16:55.694Z' - vite-plus@0.1.24: '2026-06-01T13:05:08.610Z' + vite-plus@0.2.1: '2026-06-18T05:33:32.399Z' vitest-browser-react@2.2.0: '2026-04-05T06:56:34.635Z' vitest-canvas-mock@1.1.4: '2026-03-24T14:42:39.285Z' + vitest@4.1.9: '2026-06-15T07:23:00.326Z' zod@4.4.3: '2026-05-04T07:06:40.819Z' zundo@2.3.0: '2024-11-17T16:35:11.372Z' zustand@5.0.14: '2026-05-28T10:17:58.249Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0971ce8a1f5..03b286096c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,14 +39,14 @@ overrides: postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4 postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.4 postcss@<8.5.10: ^8.5.10 - rollup@>=4.0.0 <4.59.0: 4.61.1 + rollup@>=4.0.0 <4.59.0: 4.62.2 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 solid-js: 1.9.13 string-width: ~8.2.1 tar@<=7.5.15: ^7.5.16 - vite: npm:@voidzero-dev/vite-plus-core@0.1.24 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 + vite: npm:@voidzero-dev/vite-plus-core@0.2.1 + vitest: 4.1.9 ws@>=8.0.0 <8.20.1: ^8.21.0 yaml@>=2.0.0 <2.8.3: 2.9.0 yauzl@<3.2.1: 3.2.1 @@ -65,7 +65,7 @@ catalog: '@formatjs/intl-localematcher': 0.8.10 '@heroicons/react': 2.2.0 '@hey-api/openapi-ts': 0.98.2 - '@hono/node-server': 2.0.4 + '@hono/node-server': 2.0.5 '@iconify-json/heroicons': 1.2.3 '@iconify-json/ri': 1.2.10 '@lexical/code': 0.45.0 @@ -86,19 +86,19 @@ catalog: '@orpc/contract': 1.14.6 '@orpc/openapi-client': 1.14.6 '@orpc/tanstack-query': 1.14.6 - '@playwright/test': 1.60.0 + '@playwright/test': 1.61.0 '@remixicon/react': 4.9.0 '@rgrove/parse-xml': 4.2.0 - '@sentry/react': 10.57.0 - '@storybook/addon-a11y': 10.4.4 - '@storybook/addon-docs': 10.4.4 - '@storybook/addon-links': 10.4.4 - '@storybook/addon-onboarding': 10.4.4 - '@storybook/addon-themes': 10.4.4 - '@storybook/addon-vitest': 10.4.4 - '@storybook/nextjs-vite': 10.4.4 - '@storybook/react': 10.4.4 - '@storybook/react-vite': 10.4.4 + '@sentry/react': 10.59.0 + '@storybook/addon-a11y': 10.4.6 + '@storybook/addon-docs': 10.4.6 + '@storybook/addon-links': 10.4.6 + '@storybook/addon-onboarding': 10.4.6 + '@storybook/addon-themes': 10.4.6 + '@storybook/addon-vitest': 10.4.6 + '@storybook/nextjs-vite': 10.4.6 + '@storybook/react': 10.4.6 + '@storybook/react-vite': 10.4.6 '@streamdown/math': 1.0.2 '@svgdotjs/svg.js': 3.2.5 '@t3-oss/env-nextjs': 0.13.11 @@ -110,30 +110,31 @@ catalog: '@tanstack/react-form': 1.33.0 '@tanstack/react-hotkeys': 0.10.0 '@tanstack/react-query': 5.101.0 - '@tanstack/react-virtual': 3.14.2 + '@tanstack/react-virtual': 3.14.3 '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 '@testing-library/react': 16.3.2 '@testing-library/user-event': 14.6.1 - '@tsslint/cli': 3.1.3 - '@tsslint/compat-eslint': 3.1.3 - '@tsslint/config': 3.1.3 + '@tsslint/cli': 3.1.4 + '@tsslint/compat-eslint': 3.1.4 + '@tsslint/config': 3.1.4 '@types/js-cookie': 3.0.6 '@types/js-yaml': 4.0.9 '@types/lockfile': 1.0.4 '@types/negotiator': 0.6.4 - '@types/node': 25.9.3 + '@types/node': 25.9.4 '@types/qs': 6.15.1 '@types/react': 19.2.17 '@types/react-dom': 19.2.3 '@types/sortablejs': 1.15.9 - '@typescript-eslint/eslint-plugin': 8.61.0 - '@typescript-eslint/parser': 8.61.0 - '@typescript/native-preview': 7.0.0-dev.20260613.1 + '@typescript-eslint/eslint-plugin': 8.61.1 + '@typescript-eslint/parser': 8.61.1 + '@typescript/native-preview': 7.0.0-dev.20260620.1 '@vitejs/plugin-react': 6.0.2 '@vitejs/plugin-rsc': 0.5.27 - '@vitest/browser': 4.1.8 - '@vitest/coverage-v8': 4.1.8 + '@vitest/browser': 4.1.9 + '@vitest/browser-playwright': 4.1.9 + '@vitest/coverage-v8': 4.1.9 abcjs: 6.6.3 agentation: 3.0.2 ahooks: 3.9.7 @@ -143,13 +144,13 @@ catalog: cli-table3: 0.6.5 clsx: 2.1.1 cmdk: 1.1.1 - code-inspector-plugin: 1.6.0 + code-inspector-plugin: 1.6.1 concurrently: ^10.0.3 copy-to-clipboard: 4.0.2 - cron-parser: 5.5.0 + cron-parser: 5.6.0 dayjs: 1.11.21 decimal.js: 10.6.0 - dompurify: 3.4.10 + dompurify: 3.4.11 echarts: 6.1.0 echarts-for-react: 3.0.6 elkjs: 0.11.1 @@ -166,15 +167,15 @@ catalog: eslint-plugin-markdown-preferences: 0.41.1 eslint-plugin-no-barrel-files: 1.3.1 eslint-plugin-react-refresh: 0.5.3 - eslint-plugin-sonarjs: 4.0.3 - eslint-plugin-storybook: 10.4.4 + eslint-plugin-sonarjs: 4.1.0 + eslint-plugin-storybook: 10.4.6 eventsource-parser: 3.1.0 fast-deep-equal: 3.1.3 - foxact: 0.3.5 + foxact: 0.3.7 fuse.js: 7.4.2 - happy-dom: 20.10.3 + happy-dom: 20.10.6 hast-util-to-jsx-runtime: 2.3.6 - hono: 4.12.25 + hono: 4.12.26 html-entities: 2.6.0 html-to-image: 1.11.13 i18next: 26.3.1 @@ -189,12 +190,12 @@ catalog: js-yaml: 4.2.0 jsonschema: 1.5.0 katex: 0.17.0 - knip: 6.16.1 + knip: 6.17.1 ky: 2.0.2 lamejs: 1.2.1 lexical: 0.45.0 lockfile: 1.0.4 - loro-crdt: 1.13.2 + loro-crdt: 1.13.5 mermaid: 11.15.0 mime: 4.1.0 mitt: 3.0.1 @@ -207,7 +208,7 @@ catalog: ora: 9.4.0 picocolors: 1.1.1 pinyin-pro: 3.28.1 - playwright: 1.60.0 + playwright: 1.61.0 postcss: 8.5.15 qrcode.react: 4.2.0 qs: 6.15.2 @@ -226,29 +227,29 @@ catalog: remark-directive: 4.0.0 scheduler: 0.27.0 server-only: 0.0.1 - sharp: 0.35.1 + sharp: 0.35.2 shiki: 4.2.0 socket.io-client: 4.8.3 sortablejs: 1.15.7 std-semver: 1.0.8 - storybook: 10.4.4 + storybook: 10.4.6 streamdown: 2.5.0 string-ts: 2.3.1 tailwind-merge: 3.6.0 tailwindcss: 4.3.1 - tldts: 7.4.2 + tldts: 7.4.3 tsx: 4.22.4 typescript: 6.0.3 uglify-js: 3.19.3 - undici: 7.27.2 + undici: 7.28.0 unist-util-visit: 5.1.0 use-context-selector: 2.0.0 - uuid: 14.0.0 - vinext: 0.1.2 - vite: npm:@voidzero-dev/vite-plus-core@0.1.24 + uuid: 14.0.1 + vinext: 0.1.6 + vite: npm:@voidzero-dev/vite-plus-core@0.2.1 vite-plugin-inspect: 12.0.0-beta.3 - vite-plus: 0.1.24 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 + vite-plus: 0.2.1 + vitest: 4.1.9 vitest-browser-react: 2.2.0 vitest-canvas-mock: 1.1.4 zod: 4.4.3 From 7b3508e3768cf884478821071aa31c1cab993178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 22 Jun 2026 13:37:21 +0800 Subject: [PATCH 05/35] fix: add legacy snippet permissions (#37718) --- api/services/enterprise/rbac_service.py | 5 +++ .../services/enterprise/test_rbac_service.py | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index 39a3a61a781..c32a5759105 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -321,6 +321,8 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [ "dataset.external.connect", "tool.manage", "mcp.manage", + "snippets.create_and_modify", + "snippets.management", ] _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ @@ -343,6 +345,8 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ "dataset.external.connect", "tool.manage", "mcp.manage", + "snippets.create_and_modify", + "snippets.management", ] _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ @@ -357,6 +361,7 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ "dataset.tag.manage", "dataset.external.connect", "tool.manage", + "snippets.create_and_modify", ] _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [ diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index aa4780af0b4..dfd5662cf3e 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -621,6 +621,38 @@ class TestMyPermissions: assert out.app.overrides == [] assert out.dataset.overrides == [] + @pytest.mark.parametrize( + ("role", "expected_snippet_keys"), + [ + ("owner", {"snippets.create_and_modify", "snippets.management"}), + ("admin", {"snippets.create_and_modify", "snippets.management"}), + ("editor", {"snippets.create_and_modify"}), + ("normal", set()), + ("dataset_operator", set()), + ], + ) + def test_get_uses_legacy_snippet_permissions_when_rbac_disabled( + self, + mock_send: MagicMock, + role: str, + expected_snippet_keys: set[str], + ): + mock_session = MagicMock() + mock_session.__enter__.return_value = mock_session + mock_session.scalar.return_value = role + with ( + patch(f"{MODULE}.dify_config.RBAC_ENABLED", False), + patch(f"{MODULE}.session_factory.create_session", return_value=mock_session), + ): + out = svc.RBACService.MyPermissions.get("tenant-1", "acct-1") + + actual_snippet_keys = { + permission_key for permission_key in out.workspace.permission_keys if permission_key.startswith("snippets.") + } + + mock_send.assert_not_called() + assert actual_snippet_keys == expected_snippet_keys + def test_get_returns_empty_when_role_missing_and_rbac_disabled(self, mock_send: MagicMock): mock_session = MagicMock() mock_session.__enter__.return_value = mock_session From ea3ef813962c6ed2360cafee90212e7b13b4c5d8 Mon Sep 17 00:00:00 2001 From: Evan <2869018789@qq.com> Date: Mon, 22 Jun 2026 13:53:51 +0800 Subject: [PATCH 06/35] refactor(tests): replace mock_logger with caplog in 5 service tests (#37468) (#37715) Co-authored-by: Asuka Minato Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../services/test_human_input_service.py | 9 +-- .../services/test_summary_index_service.py | 61 +++++++++++-------- .../services/test_trigger_provider_service.py | 4 +- .../services/test_vector_service.py | 18 +++--- .../test_webhook_service_additional.py | 34 +++++------ 5 files changed, 67 insertions(+), 59 deletions(-) diff --git a/api/tests/unit_tests/services/test_human_input_service.py b/api/tests/unit_tests/services/test_human_input_service.py index 4c4abbbb8ec..d9d81d66566 100644 --- a/api/tests/unit_tests/services/test_human_input_service.py +++ b/api/tests/unit_tests/services/test_human_input_service.py @@ -1,4 +1,5 @@ import dataclasses +import logging from datetime import datetime, timedelta from unittest.mock import MagicMock @@ -672,7 +673,7 @@ def test_enqueue_resume_workflow_not_found(mocker: MockerFixture, mock_session_f assert "WorkflowRun not found" in str(excinfo.value) -def test_enqueue_resume_app_not_found(mocker: MockerFixture, mock_session_factory): +def test_enqueue_resume_app_not_found(mocker, mock_session_factory, caplog): session_factory, session = mock_session_factory service = HumanInputService(session_factory) @@ -687,10 +688,10 @@ def test_enqueue_resume_app_not_found(mocker: MockerFixture, mock_session_factor ) session.execute.return_value.scalar_one_or_none.return_value = None - logger_spy = mocker.patch("services.human_input_service.logger") - service.enqueue_resume("workflow-run-id") - logger_spy.error.assert_called_once() + with caplog.at_level(logging.ERROR, logger="services.human_input_service"): + service.enqueue_resume("workflow-run-id") + assert any(r.levelno >= logging.ERROR for r in caplog.records) def test_is_globally_expired_zero_timeout( diff --git a/api/tests/unit_tests/services/test_summary_index_service.py b/api/tests/unit_tests/services/test_summary_index_service.py index e17d4134ace..cef11c0038d 100644 --- a/api/tests/unit_tests/services/test_summary_index_service.py +++ b/api/tests/unit_tests/services/test_summary_index_service.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging import sys from dataclasses import dataclass from datetime import UTC, datetime @@ -532,7 +533,10 @@ def test_vectorize_summary_error_handler_tries_chunk_id_lookup_and_can_warn_not_ error_session.commit.assert_not_called() -def test_update_summary_record_error_warns_when_missing(monkeypatch: pytest.MonkeyPatch) -> None: +def test_update_summary_record_error_warns_when_missing( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: dataset = _dataset() segment = _segment() @@ -544,14 +548,15 @@ def test_update_summary_record_error_warns_when_missing(monkeypatch: pytest.Monk SimpleNamespace(create_session=MagicMock(return_value=_SessionContext(session))), ) - logger_mock = MagicMock() - monkeypatch.setattr(summary_module, "logger", logger_mock) - - SummaryIndexService.update_summary_record_error(segment, dataset, "err") - logger_mock.warning.assert_called_once() + with caplog.at_level(logging.WARNING, logger="services.summary_index_service"): + SummaryIndexService.update_summary_record_error(segment, dataset, "err") + assert any(r.levelno >= logging.WARNING for r in caplog.records) -def test_generate_and_vectorize_summary_creates_missing_record_and_logs_usage(monkeypatch: pytest.MonkeyPatch) -> None: +def test_generate_and_vectorize_summary_creates_missing_record_and_logs_usage( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: dataset = _dataset() segment = _segment() @@ -567,12 +572,10 @@ def test_generate_and_vectorize_summary_creates_missing_record_and_logs_usage(mo monkeypatch.setattr(SummaryIndexService, "generate_summary_for_segment", MagicMock(return_value=("sum", usage))) monkeypatch.setattr(SummaryIndexService, "vectorize_summary", MagicMock(return_value=None)) - logger_mock = MagicMock() - monkeypatch.setattr(summary_module, "logger", logger_mock) - - result = SummaryIndexService.generate_and_vectorize_summary(segment, dataset, {"enable": True}) - assert result.status in {SummaryStatus.GENERATING, SummaryStatus.COMPLETED} - logger_mock.info.assert_called() + with caplog.at_level(logging.INFO, logger="services.summary_index_service"): + result = SummaryIndexService.generate_and_vectorize_summary(segment, dataset, {"enable": True}) + assert result.status in {SummaryStatus.GENERATING, SummaryStatus.COMPLETED} + assert any(r.levelno >= logging.INFO for r in caplog.records) def test_generate_summaries_for_document_skip_conditions(monkeypatch: pytest.MonkeyPatch) -> None: @@ -759,6 +762,7 @@ def test_enable_summaries_for_segments_no_summaries_noop(monkeypatch: pytest.Mon def test_enable_summaries_for_segments_skips_segment_or_content_and_handles_vectorize_error( monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: dataset = _dataset() summary1 = _summary_record(summary_content="sum", node_id="n1") @@ -786,12 +790,11 @@ def test_enable_summaries_for_segments_skips_segment_or_content_and_handles_vect SimpleNamespace(create_session=MagicMock(return_value=_SessionContext(session))), ) - logger_mock = MagicMock() - monkeypatch.setattr(summary_module, "logger", logger_mock) monkeypatch.setattr(SummaryIndexService, "vectorize_summary", MagicMock(side_effect=RuntimeError("boom"))) - SummaryIndexService.enable_summaries_for_segments(dataset) - logger_mock.exception.assert_called_once() + with caplog.at_level(logging.ERROR, logger="services.summary_index_service"): + SummaryIndexService.enable_summaries_for_segments(dataset) + assert any(r.levelno >= logging.ERROR for r in caplog.records) session.commit.assert_called_once() @@ -859,7 +862,10 @@ def test_update_summary_for_segment_empty_content_deletes_existing(monkeypatch: session.commit.assert_called_once() -def test_update_summary_for_segment_empty_content_delete_vector_warns(monkeypatch: pytest.MonkeyPatch) -> None: +def test_update_summary_for_segment_empty_content_delete_vector_warns( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: dataset = _dataset() segment = _segment() record = _summary_record(summary_content="old", node_id="n1") @@ -875,11 +881,10 @@ def test_update_summary_for_segment_empty_content_delete_vector_warns(monkeypatc vector_instance = MagicMock() vector_instance.delete_by_ids.side_effect = RuntimeError("boom") monkeypatch.setattr(summary_module, "Vector", MagicMock(return_value=vector_instance)) - logger_mock = MagicMock() - monkeypatch.setattr(summary_module, "logger", logger_mock) - assert SummaryIndexService.update_summary_for_segment(segment, dataset, "") is None - logger_mock.warning.assert_called() + with caplog.at_level(logging.WARNING, logger="services.summary_index_service"): + assert SummaryIndexService.update_summary_for_segment(segment, dataset, "") is None + assert any(r.levelno >= logging.WARNING for r in caplog.records) def test_update_summary_for_segment_empty_content_no_record_noop(monkeypatch: pytest.MonkeyPatch) -> None: @@ -923,7 +928,10 @@ def test_update_summary_for_segment_updates_existing_and_vectorizes(monkeypatch: session.commit.assert_called() -def test_update_summary_for_segment_existing_vector_delete_warns(monkeypatch: pytest.MonkeyPatch) -> None: +def test_update_summary_for_segment_existing_vector_delete_warns( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: dataset = _dataset() segment = _segment() record = _summary_record(summary_content="old", node_id="n1") @@ -940,11 +948,10 @@ def test_update_summary_for_segment_existing_vector_delete_warns(monkeypatch: py vector_instance.delete_by_ids.side_effect = RuntimeError("boom") monkeypatch.setattr(summary_module, "Vector", MagicMock(return_value=vector_instance)) monkeypatch.setattr(SummaryIndexService, "vectorize_summary", MagicMock(return_value=None)) - logger_mock = MagicMock() - monkeypatch.setattr(summary_module, "logger", logger_mock) - SummaryIndexService.update_summary_for_segment(segment, dataset, "new") - logger_mock.warning.assert_called() + with caplog.at_level(logging.WARNING, logger="services.summary_index_service"): + SummaryIndexService.update_summary_for_segment(segment, dataset, "new") + assert any(r.levelno >= logging.WARNING for r in caplog.records) def test_update_summary_for_segment_existing_vectorize_failure_returns_error_record( diff --git a/api/tests/unit_tests/services/test_trigger_provider_service.py b/api/tests/unit_tests/services/test_trigger_provider_service.py index a47d946bab0..0a4452cf478 100644 --- a/api/tests/unit_tests/services/test_trigger_provider_service.py +++ b/api/tests/unit_tests/services/test_trigger_provider_service.py @@ -253,7 +253,7 @@ def test_add_trigger_subscription_should_raise_error_when_provider_limit_reached mock_session: MagicMock, provider_id: TriggerProviderID, provider_controller: MagicMock, - caplog, + caplog: pytest.LogCaptureFixture, ) -> None: # Arrange _patch_redis_lock(mocker) @@ -274,7 +274,7 @@ def test_add_trigger_subscription_should_raise_error_when_provider_limit_reached properties={}, credentials={}, ) - assert sum(1 for r in caplog.records if r.levelno >= logging.ERROR) == 1 + assert any(r.levelno >= logging.ERROR for r in caplog.records) def test_add_trigger_subscription_should_raise_error_when_name_exists( diff --git a/api/tests/unit_tests/services/test_vector_service.py b/api/tests/unit_tests/services/test_vector_service.py index e6cc59144b3..a5873870103 100644 --- a/api/tests/unit_tests/services/test_vector_service.py +++ b/api/tests/unit_tests/services/test_vector_service.py @@ -269,7 +269,8 @@ def test_create_segments_vector_parent_child_uses_default_embedding_model_when_p def test_create_segments_vector_parent_child_missing_document_logs_warning_and_continues( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: dataset = _make_dataset(doc_form=vector_service_module.IndexStructureType.PARENT_CHILD_INDEX) segment = _make_segment() @@ -290,7 +291,7 @@ def test_create_segments_vector_parent_child_missing_document_logs_warning_and_c VectorService.create_segments_vector( None, [segment], dataset, vector_service_module.IndexStructureType.PARENT_CHILD_INDEX ) - assert "Expected DatasetDocument record to exist, but none was found" in caplog.text + assert any(r.levelno >= logging.WARNING for r in caplog.records) index_processor.load.assert_not_called() @@ -614,7 +615,8 @@ def test_update_multimodel_vector_commits_when_no_upload_files_found(monkeypatch def test_update_multimodel_vector_adds_bindings_and_vectors_and_skips_missing_upload_files( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: dataset = _make_dataset(indexing_technique=IndexTechniqueType.HIGH_QUALITY, is_multimodal=True) segment = _make_segment(segment_id="seg-1", tenant_id="tenant-1", attachments=[{"id": "old-1"}]) @@ -631,8 +633,7 @@ def test_update_multimodel_vector_adds_bindings_and_vectors_and_skips_missing_up with caplog.at_level(logging.WARNING, logger="services.vector_service"): VectorService.update_multimodel_vector(segment=segment, attachment_ids=["file-1", "missing"], dataset=dataset) - - assert "Upload file not found for attachment_id" in caplog.text + assert any(r.levelno >= logging.WARNING for r in caplog.records) db_mock.session.add_all.assert_called_once() bindings = db_mock.session.add_all.call_args.args[0] assert len(bindings) == 1 @@ -671,7 +672,8 @@ def test_update_multimodel_vector_updates_bindings_without_multimodal_vector_ops def test_update_multimodel_vector_rolls_back_and_reraises_on_error( - monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: dataset = _make_dataset(indexing_technique=IndexTechniqueType.HIGH_QUALITY, is_multimodal=True) segment = _make_segment(segment_id="seg-1", tenant_id="tenant-1", attachments=[{"id": "old-1"}]) @@ -691,7 +693,5 @@ def test_update_multimodel_vector_rolls_back_and_reraises_on_error( with pytest.raises(RuntimeError, match="boom"): VectorService.update_multimodel_vector(segment=segment, attachment_ids=["file-1"], dataset=dataset) - exception_records = [r for r in caplog.records if r.levelname == "ERROR"] - assert len(exception_records) == 1 - assert "Failed to update multimodal vector for segment" in exception_records[0].getMessage() + assert any(r.levelno >= logging.ERROR for r in caplog.records) db_mock.session.rollback.assert_called_once() diff --git a/api/tests/unit_tests/services/test_webhook_service_additional.py b/api/tests/unit_tests/services/test_webhook_service_additional.py index 491dd948427..e3a8e282e9c 100644 --- a/api/tests/unit_tests/services/test_webhook_service_additional.py +++ b/api/tests/unit_tests/services/test_webhook_service_additional.py @@ -1,3 +1,4 @@ +import logging from types import SimpleNamespace from typing import Any from unittest.mock import MagicMock @@ -31,21 +32,21 @@ class TestWebhookServiceExtractionFallbacks: self, flask_app: Flask, monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: - warning_mock = MagicMock() - monkeypatch.setattr(service_module.logger, "warning", warning_mock) webhook_trigger = MagicMock() - with flask_app.test_request_context( - "/webhook", - method="POST", - headers={"Content-Type": "application/vnd.custom"}, - data="plain content", - ): - result = WebhookService.extract_webhook_data(webhook_trigger) + with caplog.at_level(logging.WARNING, logger="services.trigger.webhook_service"): + with flask_app.test_request_context( + "/webhook", + method="POST", + headers={"Content-Type": "application/vnd.custom"}, + data="plain content", + ): + result = WebhookService.extract_webhook_data(webhook_trigger) - assert result["body"] == {"raw": "plain content"} - warning_mock.assert_called_once() + assert result["body"] == {"raw": "plain content"} + assert any(r.levelno >= logging.WARNING for r in caplog.records) def test_extract_webhook_data_should_raise_for_request_too_large( self, @@ -171,14 +172,13 @@ class TestWebhookServiceValidationAndConversion: def test_validate_json_value_should_return_original_for_unmapped_supported_segment_type( self, monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: - warning_mock = MagicMock() - monkeypatch.setattr(service_module.logger, "warning", warning_mock) + with caplog.at_level(logging.WARNING, logger="services.trigger.webhook_service"): + result = WebhookService._validate_json_value("param", {"x": 1}, "unsupported-type") - result = WebhookService._validate_json_value("param", {"x": 1}, "unsupported-type") - - assert result == {"x": 1} - warning_mock.assert_called_once() + assert result == {"x": 1} + assert any(r.levelno >= logging.WARNING for r in caplog.records) def test_validate_and_convert_value_should_wrap_conversion_errors(self) -> None: with pytest.raises(ValueError, match="validation failed"): From 1acf6e7eb68ae1943e9484562b26bda21a3a850c Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:56:31 -0700 Subject: [PATCH 07/35] fix(cli): document HITL pause exit code as 0, not 2 (#37737) --- cli/src/commands/resume/app/guide.ts | 7 ++++--- cli/src/commands/run/app/guide.ts | 2 +- cli/src/help/contract.ts | 6 +++--- cli/src/help/skill-template.ts | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cli/src/commands/resume/app/guide.ts b/cli/src/commands/resume/app/guide.ts index 03b904f3d5b..1483d40b9d4 100644 --- a/cli/src/commands/resume/app/guide.ts +++ b/cli/src/commands/resume/app/guide.ts @@ -1,14 +1,15 @@ export const agentGuide = ` WHEN TO USE Continue a workflow that paused for human input. run app (or a prior - resume app) exits 2 and prints a JSON object with status "paused", + resume app) exits 0 and prints a JSON object with status "paused", form_token, workflow_run_id and resolved_default_values. Resume with: difyctl resume app --workflow-run-id \\ --inputs '{"name":"Alice"}' -o json LOOP - A resume can pause again (exit 2 with a new form_token). Repeat until - exit 0. Pass --stream to print events live. + A resume can pause again (exit 0 with a new form_token and status + "paused"). Repeat until the output is no longer a pause. Pass --stream + to print events live. ERROR RECOVERY not logged in (exit 4) difyctl auth login diff --git a/cli/src/commands/run/app/guide.ts b/cli/src/commands/run/app/guide.ts index 70433fb4632..2c58e01edd9 100644 --- a/cli/src/commands/run/app/guide.ts +++ b/cli/src/commands/run/app/guide.ts @@ -16,7 +16,7 @@ APP MODES JSON object via --inputs. agent-chat Conversational with autonomous tool use. -HITL PAUSE (exit code 2) +HITL PAUSE (exit code 0 — success-with-pending) When a workflow pauses for human input, stdout receives a JSON object with status "paused", form_token, workflow_run_id, and resolved_default_values. Resume with: diff --git a/cli/src/help/contract.ts b/cli/src/help/contract.ts index d4be7209280..d3a7549ecc9 100644 --- a/cli/src/help/contract.ts +++ b/cli/src/help/contract.ts @@ -14,9 +14,9 @@ export type Contract = { } const EXIT_CODE_DESCRIPTIONS: Readonly> = { - [ExitCode.Success]: 'success', + [ExitCode.Success]: 'success (also a workflow paused for human input — check stdout for status "paused")', [ExitCode.Generic]: 'generic error', - [ExitCode.Usage]: 'usage error (bad flag / missing arg), or a workflow paused for human input', + [ExitCode.Usage]: 'usage error (bad flag / missing arg)', [ExitCode.Auth]: 'auth error (not logged in / token expired)', [ExitCode.VersionCompat]: 'version / compatibility error', } @@ -42,7 +42,7 @@ export const CONTRACT: Contract = { }, hitl: { description: - 'When a workflow pauses for human input, `run app` exits 2 and writes a JSON object to stdout with status "paused", form_token, workflow_run_id and resolved_default_values.', + 'When a workflow pauses for human input, `run app` exits 0 (success-with-pending) and writes a JSON object to stdout with status "paused", form_token, workflow_run_id and resolved_default_values.', resume: 'difyctl resume app --workflow-run-id [--inputs \'{"key":"value"}\']', }, diff --git a/cli/src/help/skill-template.ts b/cli/src/help/skill-template.ts index 1038a8f99ef..581d6392e13 100644 --- a/cli/src/help/skill-template.ts +++ b/cli/src/help/skill-template.ts @@ -26,7 +26,7 @@ output formats, error envelope, HITL protocol). Treat that JSON as the source of truth; this file only bootstraps you into it. ## The one non-obvious thing: HITL pauses are not failures -A run can pause for human input. It exits with **code 2** and emits a +A run can pause for human input. It exits with **code 0** and emits a \`paused\` JSON payload — this is success-with-pending, NOT a crash. Resume as the payload instructs (see \`difyctl resume app --help\`). From 908c148667039fedba3eddf3ccb5428f74a94e97 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:11:27 -0700 Subject: [PATCH 08/35] fix(hitl): stop confusing 404 when resuming forms via CLI (#37556) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/openapi/_errors.py | 15 ++ api/controllers/openapi/human_input_form.py | 20 ++- .../common/workflow_response_converter.py | 18 +- api/core/app/entities/task_entities.py | 3 + api/core/workflow/human_input_forms.py | 95 +++++------ api/core/workflow/human_input_policy.py | 43 ++++- api/models/human_input.py | 32 +++- .../workflow_event_snapshot_service.py | 19 ++- .../openapi/test_error_contract.py | 4 + .../openapi/test_human_input_form.py | 46 ++++- .../service_api/app/test_hitl_service_api.py | 14 +- .../test_generate_task_pipeline_core.py | 1 + .../app/apps/test_workflow_pause_events.py | 157 +++++++++++++----- .../test_generate_task_pipeline_core.py | 1 + .../test_human_input_form_repository_impl.py | 4 + .../workflow/test_enrich_pause_reasons.py | 63 +++++++ .../core/workflow/test_human_input_forms.py | 152 +++++++++-------- .../core/workflow/test_human_input_policy.py | 65 ++++---- .../test_human_input_policy_openapi.py | 34 ---- .../models/test_recipient_type_label.py | 21 +++ .../test_workflow_event_snapshot_service.py | 103 +++++++++++- cli/src/commands/run/app/hitl-render.test.ts | 68 ++++++++ cli/src/commands/run/app/hitl-render.ts | 28 +++- cli/src/commands/run/app/sse-collector.ts | 2 + cli/test/e2e/suites/run/run-app-hitl.e2e.ts | 24 +-- .../generated/api/openapi/types.gen.ts | 2 + .../generated/api/openapi/zod.gen.ts | 2 + 27 files changed, 750 insertions(+), 286 deletions(-) create mode 100644 api/tests/unit_tests/core/workflow/test_enrich_pause_reasons.py delete mode 100644 api/tests/unit_tests/core/workflow/test_human_input_policy_openapi.py create mode 100644 api/tests/unit_tests/models/test_recipient_type_label.py create mode 100644 cli/src/commands/run/app/hitl-render.test.ts diff --git a/api/controllers/openapi/_errors.py b/api/controllers/openapi/_errors.py index 38c068bd354..5e82c2614de 100644 --- a/api/controllers/openapi/_errors.py +++ b/api/controllers/openapi/_errors.py @@ -63,6 +63,8 @@ class OpenApiErrorCode(StrEnum): FILE_EXTENSION_BLOCKED = "file_extension_blocked" MEMBER_LIMIT_EXCEEDED = "member_limit_exceeded" MEMBER_LICENSE_EXCEEDED = "member_license_exceeded" + HUMAN_INPUT_FORM_NOT_FOUND = "form_not_found" + RECIPIENT_SURFACE_MISMATCH = "recipient_surface_mismatch" class ErrorDetail(BaseModel): @@ -239,3 +241,16 @@ class MemberLicenseExceeded(OpenApiError): # noqa: N818 error_code = OpenApiErrorCode.MEMBER_LICENSE_EXCEEDED description = "Workspace member license capacity reached." hint = "Contact your workspace administrator to expand the license seat count." + + +class HumanInputFormNotFound(OpenApiError): # noqa: N818 + code = 404 + error_code = OpenApiErrorCode.HUMAN_INPUT_FORM_NOT_FOUND + description = "No human-input form matches this token. It may be wrong, expired, or already submitted." + + +class RecipientSurfaceMismatch(OpenApiError): # noqa: N818 + code = 403 + error_code = OpenApiErrorCode.RECIPIENT_SURFACE_MISMATCH + description = "This form's recipient can't be submitted via the OpenAPI surface." + hint = "Action it through its channel (web app or console)." diff --git a/api/controllers/openapi/human_input_form.py b/api/controllers/openapi/human_input_form.py index 995315150cc..e5930ea8fa2 100644 --- a/api/controllers/openapi/human_input_form.py +++ b/api/controllers/openapi/human_input_form.py @@ -12,16 +12,20 @@ import logging from flask import Response from flask_restx import Resource -from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.exceptions import BadRequest from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values from controllers.common.schema import register_schema_models from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns +from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch from controllers.openapi._models import FormSubmitResponse, HumanInputFormDefinitionResponse from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData -from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface +from core.workflow.human_input_policy import ( + HumanInputSurface, + is_recipient_type_allowed_for_surface, +) from extensions.ext_database import db from libs.helper import to_timestamp from libs.oauth_bearer import Scope @@ -47,12 +51,12 @@ def _jsonify_form_definition(form) -> Response: def _ensure_form_belongs_to_app(form, app_model: App) -> None: if form.app_id != app_model.id or form.tenant_id != app_model.tenant_id: - raise NotFound("Form not found") + raise HumanInputFormNotFound() def _ensure_form_is_allowed_for_openapi(form) -> None: if not is_recipient_type_allowed_for_surface(form.recipient_type, HumanInputSurface.OPENAPI): - raise NotFound("Form not found") + raise RecipientSurfaceMismatch() @openapi_ns.route("/apps//form/human_input/") @@ -60,11 +64,11 @@ class OpenApiWorkflowHumanInputFormApi(Resource): @openapi_ns.response(200, "Form definition", openapi_ns.models[HumanInputFormDefinitionResponse.__name__]) @auth_router.guard(scope=Scope.APPS_RUN) def get(self, app_id: str, form_token: str, *, auth_data: AuthData): - app_model, caller, caller_kind = auth_data.require_app_context() + app_model, _caller, _caller_kind = auth_data.require_app_context() service = HumanInputService(db.engine) form = service.get_form_by_token(form_token) if form is None: - raise NotFound("Form not found") + raise HumanInputFormNotFound() _ensure_form_belongs_to_app(form, app_model) _ensure_form_is_allowed_for_openapi(form) @@ -80,7 +84,7 @@ class OpenApiWorkflowHumanInputFormApi(Resource): service = HumanInputService(db.engine) form = service.get_form_by_token(form_token) if form is None: - raise NotFound("Form not found") + raise HumanInputFormNotFound() _ensure_form_belongs_to_app(form, app_model) _ensure_form_is_allowed_for_openapi(form) @@ -106,6 +110,6 @@ class OpenApiWorkflowHumanInputFormApi(Resource): submission_end_user_id=submission_end_user_id, ) except FormNotFoundError: - raise NotFound("Form not found") + raise HumanInputFormNotFound() return FormSubmitResponse() diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index c9486b5821f..67f37e78ab9 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -51,8 +51,11 @@ from core.tools.entities.tool_entities import ToolProviderType from core.tools.tool_manager import ToolManager from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE from core.trigger.trigger_manager import TriggerManager -from core.workflow.human_input_forms import load_form_tokens_by_form_id +from core.workflow.human_input_forms import ( + load_form_dispositions_by_form_id, +) from core.workflow.human_input_policy import ( + FormDisposition, HumanInputSurface, enrich_human_input_pause_reasons, resolve_human_input_pause_reason_inputs, @@ -340,13 +343,14 @@ class WorkflowResponseConverter: human_input_form_ids = [reason.form_id for reason in resolved_reasons if isinstance(reason, HumanInputRequired)] expiration_times_by_form_id: dict[str, datetime] = {} display_in_ui_by_form_id: dict[str, bool] = {} - form_token_by_form_id: dict[str, str] = {} + dispositions_by_form_id: dict[str, FormDisposition] = {} if human_input_form_ids: stmt = select( HumanInputForm.id, HumanInputForm.expiration_time, HumanInputForm.form_definition, ).where(HumanInputForm.id.in_(human_input_form_ids)) + hitl_surface = _INVOKE_FROM_TO_HITL_SURFACE.get(self._application_generate_entity.invoke_from) with Session(bind=db.engine) as session: for form_id, expiration_time, form_definition in session.execute(stmt): expiration_times_by_form_id[str(form_id)] = expiration_time @@ -355,17 +359,17 @@ class WorkflowResponseConverter: except (TypeError, json.JSONDecodeError): definition_payload = {} display_in_ui_by_form_id[str(form_id)] = bool(definition_payload.get("display_in_ui")) - form_token_by_form_id = load_form_tokens_by_form_id( + dispositions_by_form_id = load_form_dispositions_by_form_id( human_input_form_ids, session=session, - surface=_INVOKE_FROM_TO_HITL_SURFACE.get(self._application_generate_entity.invoke_from), + surface=hitl_surface, ) # Reconnect paths must preserve the same pause-reason contract as live streams; # otherwise clients see schema drift after resume. pause_reasons = enrich_human_input_pause_reasons( pause_reasons, - form_tokens_by_form_id=form_token_by_form_id, + dispositions_by_form_id=dispositions_by_form_id, expiration_times_by_form_id={ form_id: int(expiration_time.timestamp()) for form_id, expiration_time in expiration_times_by_form_id.items() @@ -379,6 +383,7 @@ class WorkflowResponseConverter: expiration_time = expiration_times_by_form_id.get(reason.form_id) if expiration_time is None: raise ValueError(f"HumanInputForm not found for pause reason, form_id={reason.form_id}") + disposition = dispositions_by_form_id.get(reason.form_id) responses.append( HumanInputRequiredResponse( task_id=task_id, @@ -391,7 +396,8 @@ class WorkflowResponseConverter: inputs=reason.inputs, actions=reason.actions, display_in_ui=display_in_ui_by_form_id.get(reason.form_id, False), - form_token=form_token_by_form_id.get(reason.form_id), + form_token=disposition.form_token if disposition else None, + approval_channels=list(disposition.approval_channels) if disposition else [], resolved_default_values=reason.resolved_default_values, expiration_time=int(expiration_time.timestamp()), ), diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index 803fdacf78d..3a8107e0461 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -288,6 +288,7 @@ class HumanInputRequiredResponse(StreamResponse): actions: Sequence[UserActionConfig] = Field(default_factory=list) display_in_ui: bool = False form_token: str | None = None + approval_channels: list[str] = Field(default_factory=list) resolved_default_values: Mapping[str, Any] = Field(default_factory=dict) expiration_time: int = Field(..., description="Unix timestamp in seconds") @@ -311,6 +312,7 @@ class HumanInputRequiredPauseReasonPayload(BaseModel): actions: Sequence[UserActionConfig] = Field(default_factory=list) display_in_ui: bool = False form_token: str | None = None + approval_channels: list[str] = Field(default_factory=list) resolved_default_values: Mapping[str, Any] = Field(default_factory=dict) expiration_time: int @@ -325,6 +327,7 @@ class HumanInputRequiredPauseReasonPayload(BaseModel): actions=data.actions, display_in_ui=data.display_in_ui, form_token=data.form_token, + approval_channels=data.approval_channels, resolved_default_values=data.resolved_default_values, expiration_time=data.expiration_time, ) diff --git a/api/core/workflow/human_input_forms.py b/api/core/workflow/human_input_forms.py index fe3c161a326..b850cd23914 100644 --- a/api/core/workflow/human_input_forms.py +++ b/api/core/workflow/human_input_forms.py @@ -12,60 +12,61 @@ from collections.abc import Sequence from sqlalchemy import select from sqlalchemy.orm import Session -from core.workflow.human_input_policy import HumanInputSurface, get_preferred_form_token +from core.workflow.human_input_policy import ( + FormDisposition, + HumanInputSurface, + disposition_for_surface, +) from extensions.ext_database import db from models.human_input import HumanInputFormRecipient, RecipientType +def load_form_dispositions_by_form_id( + form_ids: Sequence[str], + *, + session: Session | None = None, + surface: HumanInputSurface | None = None, +) -> dict[str, FormDisposition]: + """Resolve each paused form's resume token and approval channels for `surface`.""" + unique_form_ids = list(dict.fromkeys(form_ids)) + if not unique_form_ids: + return {} + + if session is not None: + return _load_form_dispositions_by_form_id(session, unique_form_ids, surface=surface) + + with Session(bind=db.engine, expire_on_commit=False) as new_session: + return _load_form_dispositions_by_form_id(new_session, unique_form_ids, surface=surface) + + +def _load_form_dispositions_by_form_id( + session: Session, + form_ids: Sequence[str], + *, + surface: HumanInputSurface | None, +) -> dict[str, FormDisposition]: + recipients_by_form_id: dict[str, list[tuple[RecipientType, str]]] = {} + stmt = select(HumanInputFormRecipient).where(HumanInputFormRecipient.form_id.in_(form_ids)) + for recipient in session.scalars(stmt): + recipients_by_form_id.setdefault(recipient.form_id, []).append( + (recipient.recipient_type, recipient.access_token or "") + ) + return { + form_id: disposition_for_surface(recipients, surface=surface) + for form_id, recipients in recipients_by_form_id.items() + } + + def load_form_tokens_by_form_id( form_ids: Sequence[str], *, session: Session | None = None, surface: HumanInputSurface | None = None, ) -> dict[str, str]: - """Load the preferred access token for each human input form.""" - unique_form_ids = list(dict.fromkeys(form_ids)) - if not unique_form_ids: - return {} - - if session is not None: - return _load_form_tokens_by_form_id(session, unique_form_ids, surface=surface) - - with Session(bind=db.engine, expire_on_commit=False) as new_session: - return _load_form_tokens_by_form_id(new_session, unique_form_ids, surface=surface) - - -def _load_form_tokens_by_form_id( - session: Session, - form_ids: Sequence[str], - *, - surface: HumanInputSurface | None = None, -) -> dict[str, str]: - recipients_by_form_id: dict[str, list[tuple[RecipientType, str]]] = {} - stmt = select(HumanInputFormRecipient).where(HumanInputFormRecipient.form_id.in_(form_ids)) - for recipient in session.scalars(stmt): - if not recipient.access_token: - continue - recipients_by_form_id.setdefault(recipient.form_id, []).append( - (recipient.recipient_type, recipient.access_token) - ) - - tokens_by_form_id: dict[str, str] = {} - for form_id, recipients in recipients_by_form_id.items(): - token = _get_surface_form_token(recipients, surface=surface) - if token is not None: - tokens_by_form_id[form_id] = token - return tokens_by_form_id - - -def _get_surface_form_token( - recipients: Sequence[tuple[RecipientType, str]], - *, - surface: HumanInputSurface | None, -) -> str | None: - if surface in {HumanInputSurface.SERVICE_API, HumanInputSurface.OPENAPI}: - for recipient_type, token in recipients: - if recipient_type == RecipientType.STANDALONE_WEB_APP and token: - return token - - return get_preferred_form_token(recipients) + """Resume tokens only, for callers that don't surface approval channels.""" + dispositions = load_form_dispositions_by_form_id(form_ids, session=session, surface=surface) + return { + form_id: disposition.form_token + for form_id, disposition in dispositions.items() + if disposition.form_token is not None + } diff --git a/api/core/workflow/human_input_policy.py b/api/core/workflow/human_input_policy.py index e95d753ae96..d6f7df52354 100644 --- a/api/core/workflow/human_input_policy.py +++ b/api/core/workflow/human_input_policy.py @@ -2,14 +2,14 @@ from __future__ import annotations from collections.abc import Mapping, Sequence from enum import StrEnum -from typing import Any +from typing import Any, NamedTuple from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType from graphon.nodes.human_input.entities import FormInputConfig, SelectInputConfig from graphon.nodes.human_input.enums import ValueSourceType from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool from graphon.variables import ArrayStringSegment -from models.human_input import RecipientType +from models.human_input import ApprovalChannel, RecipientType class HumanInputSurface(StrEnum): @@ -20,7 +20,7 @@ class HumanInputSurface(StrEnum): # SERVICE_API and OPENAPI are intentionally narrower than CONSOLE: token callers # should only be able to act on end-user web forms, not internal console flows. -_ALLOWED_RECIPIENT_TYPES_BY_SURFACE: dict[HumanInputSurface, frozenset[RecipientType]] = { +ALLOWED_RECIPIENT_TYPES_BY_SURFACE: dict[HumanInputSurface, frozenset[RecipientType]] = { HumanInputSurface.SERVICE_API: frozenset({RecipientType.STANDALONE_WEB_APP}), HumanInputSurface.CONSOLE: frozenset({RecipientType.CONSOLE, RecipientType.BACKSTAGE}), HumanInputSurface.OPENAPI: frozenset({RecipientType.STANDALONE_WEB_APP}), @@ -41,7 +41,7 @@ def is_recipient_type_allowed_for_surface( ) -> bool: if recipient_type is None: return False - return recipient_type in _ALLOWED_RECIPIENT_TYPES_BY_SURFACE[surface] + return recipient_type in ALLOWED_RECIPIENT_TYPES_BY_SURFACE[surface] def get_preferred_form_token( @@ -59,10 +59,39 @@ def get_preferred_form_token( return chosen_token +class FormDisposition(NamedTuple): + """How a paused form resolves for one API surface. + + A form's recipients split into those the surface may act on (yielding a resume + `form_token`) and those it may not (their channels named in `approval_channels` + so the caller is told where approval actually happens instead). + """ + + form_token: str | None + approval_channels: list[ApprovalChannel] + + +def disposition_for_surface( + recipients: Sequence[tuple[RecipientType, str]], + *, + surface: HumanInputSurface | None, +) -> FormDisposition: + if surface is None: + return FormDisposition(form_token=get_preferred_form_token(recipients), approval_channels=[]) + allowed = ALLOWED_RECIPIENT_TYPES_BY_SURFACE[surface] + actionable = [(recipient_type, token) for recipient_type, token in recipients if recipient_type in allowed] + return FormDisposition( + form_token=get_preferred_form_token(actionable), + approval_channels=sorted( + {recipient_type.approval_channel for recipient_type, _ in recipients if recipient_type not in allowed} + ), + ) + + def enrich_human_input_pause_reasons( reasons: Sequence[Mapping[str, Any]], *, - form_tokens_by_form_id: Mapping[str, str], + dispositions_by_form_id: Mapping[str, FormDisposition], expiration_times_by_form_id: Mapping[str, int], ) -> list[dict[str, Any]]: enriched: list[dict[str, Any]] = [] @@ -71,7 +100,9 @@ def enrich_human_input_pause_reasons( if updated.get("TYPE") == PauseReasonType.HUMAN_INPUT_REQUIRED: form_id = updated.get("form_id") if isinstance(form_id, str): - updated["form_token"] = form_tokens_by_form_id.get(form_id) + disposition = dispositions_by_form_id.get(form_id) + updated["form_token"] = disposition.form_token if disposition else None + updated["approval_channels"] = list(disposition.approval_channels) if disposition else [] expiration_time = expiration_times_by_form_id.get(form_id) if expiration_time is not None: updated["expiration_time"] = expiration_time diff --git a/api/models/human_input.py b/api/models/human_input.py index d11274bc921..b84579a4e09 100644 --- a/api/models/human_input.py +++ b/api/models/human_input.py @@ -134,20 +134,40 @@ class HumanInputDelivery(DefaultFieldsMixin, Base): ) +class ApprovalChannel(StrEnum): + """Where a paused human input form can be approved, surfaced to API callers.""" + + EMAIL = "email" + WEB_APP = "web_app" + CONSOLE = "console" + + class RecipientType(StrEnum): - # EMAIL_MEMBER member means that the - EMAIL_MEMBER = "email_member" - EMAIL_EXTERNAL = "email_external" + # Second value = the approval channel this recipient maps to (surfaced in `approval_channels`). + EMAIL_MEMBER = "email_member", ApprovalChannel.EMAIL + EMAIL_EXTERNAL = "email_external", ApprovalChannel.EMAIL # STANDALONE_WEB_APP is used by the standalone web app. # # It's not used while running workflows / chatflows containing HumanInput # node inside console. - STANDALONE_WEB_APP = "standalone_web_app" + STANDALONE_WEB_APP = "standalone_web_app", ApprovalChannel.WEB_APP # CONSOLE is used while running workflows / chatflows containing HumanInput # node inside console. (E.G. running installed apps or debugging workflows / chatflows) - CONSOLE = "console" + CONSOLE = "console", ApprovalChannel.CONSOLE # BACKSTAGE is used for backstage input inside console. - BACKSTAGE = "backstage" + BACKSTAGE = "backstage", ApprovalChannel.CONSOLE + + _approval_channel: ApprovalChannel + + def __new__(cls, value: str, approval_channel: ApprovalChannel) -> "RecipientType": + member = str.__new__(cls, value) + member._value_ = value + member._approval_channel = approval_channel + return member + + @property + def approval_channel(self) -> ApprovalChannel: + return self._approval_channel @final diff --git a/api/services/workflow_event_snapshot_service.py b/api/services/workflow_event_snapshot_service.py index dad7dff292e..c693f6318bd 100644 --- a/api/services/workflow_event_snapshot_service.py +++ b/api/services/workflow_event_snapshot_service.py @@ -23,8 +23,11 @@ from core.app.entities.task_entities import ( WorkflowStartStreamResponse, ) from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext -from core.workflow.human_input_forms import load_form_tokens_by_form_id +from core.workflow.human_input_forms import ( + load_form_dispositions_by_form_id, +) from core.workflow.human_input_policy import ( + FormDisposition, HumanInputSurface, enrich_human_input_pause_reasons, resolve_human_input_pause_reason_inputs, @@ -359,7 +362,7 @@ def _build_human_input_required_events( expiration_times_by_form_id: dict[str, int] = {} display_in_ui_by_form_id: dict[str, bool] = {} - form_tokens_by_form_id: dict[str, str] = {} + dispositions_by_form_id: dict[str, FormDisposition] = {} if human_input_form_ids and session_maker is not None: stmt = select(HumanInputForm.id, HumanInputForm.expiration_time, HumanInputForm.form_definition).where( HumanInputForm.id.in_(human_input_form_ids) @@ -372,7 +375,7 @@ def _build_human_input_required_events( except (TypeError, json.JSONDecodeError): definition_payload = {} display_in_ui_by_form_id[str(form_id)] = bool(definition_payload.get("display_in_ui")) - form_tokens_by_form_id = load_form_tokens_by_form_id( + dispositions_by_form_id = load_form_dispositions_by_form_id( human_input_form_ids, session=session, surface=human_input_surface, @@ -393,6 +396,7 @@ def _build_human_input_required_events( reason.inputs, variable_pool=variable_pool, ) + disposition = dispositions_by_form_id.get(form_id) response = HumanInputRequiredResponse( task_id=task_id, @@ -405,7 +409,8 @@ def _build_human_input_required_events( inputs=resolved_inputs, actions=reason.actions, display_in_ui=display_in_ui_by_form_id.get(form_id, False), - form_token=form_tokens_by_form_id.get(form_id), + form_token=disposition.form_token if disposition else None, + approval_channels=list(disposition.approval_channels) if disposition else [], resolved_default_values=reason.resolved_default_values, expiration_time=expiration_time, ), @@ -493,11 +498,11 @@ def _build_pause_event( for form_id in [reason.get("form_id")] if isinstance(form_id, str) ] - form_tokens_by_form_id: dict[str, str] = {} + dispositions_by_form_id: dict[str, FormDisposition] = {} expiration_times_by_form_id: dict[str, int] = {} if human_input_form_ids and session_maker is not None: with session_maker() as session: - form_tokens_by_form_id = load_form_tokens_by_form_id( + dispositions_by_form_id = load_form_dispositions_by_form_id( human_input_form_ids, session=session, surface=human_input_surface, @@ -512,7 +517,7 @@ def _build_pause_event( # otherwise clients see schema drift after resume. reasons = enrich_human_input_pause_reasons( reasons, - form_tokens_by_form_id=form_tokens_by_form_id, + dispositions_by_form_id=dispositions_by_form_id, expiration_times_by_form_id=expiration_times_by_form_id, ) diff --git a/api/tests/unit_tests/controllers/openapi/test_error_contract.py b/api/tests/unit_tests/controllers/openapi/test_error_contract.py index 45a577443b7..788a7215ed2 100644 --- a/api/tests/unit_tests/controllers/openapi/test_error_contract.py +++ b/api/tests/unit_tests/controllers/openapi/test_error_contract.py @@ -26,11 +26,13 @@ from controllers.openapi._errors import ( ErrorBody, ErrorDetail, FilenameNotExists, + HumanInputFormNotFound, MemberLicenseExceeded, MemberLimitExceeded, OpenApiError, OpenApiErrorCode, OpenApiErrorFormatter, + RecipientSurfaceMismatch, ) from controllers.service_api.app.error import ( AppUnavailableError, @@ -319,6 +321,8 @@ ERROR_MATRIX = [ (BlockedFileExtensionError(), 400, "file_extension_blocked"), (MemberLimitExceeded(), 403, "member_limit_exceeded"), (MemberLicenseExceeded(), 403, "member_license_exceeded"), + (HumanInputFormNotFound(), 404, "form_not_found"), + (RecipientSurfaceMismatch(), 403, "recipient_surface_mismatch"), ] diff --git a/api/tests/unit_tests/controllers/openapi/test_human_input_form.py b/api/tests/unit_tests/controllers/openapi/test_human_input_form.py index f8d296deb3e..5659cd6eeff 100644 --- a/api/tests/unit_tests/controllers/openapi/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/openapi/test_human_input_form.py @@ -11,8 +11,9 @@ from unittest.mock import Mock import pytest from flask import Flask -from werkzeug.exceptions import NotFound, UnprocessableEntity +from werkzeug.exceptions import UnprocessableEntity +from controllers.openapi._errors import HumanInputFormNotFound, RecipientSurfaceMismatch from controllers.openapi.auth.data import AuthData from libs.oauth_bearer import Scope, TokenType from models.human_input import RecipientType @@ -89,7 +90,7 @@ class TestOpenApiHumanInputFormGet: caller = SimpleNamespace(id="acct-1") with app.test_request_context("/openapi/v1/apps/app-1/form/human_input/bad"): - with pytest.raises(NotFound): + with pytest.raises(HumanInputFormNotFound): api.get.__wrapped__( api, app_id="app-1", @@ -101,7 +102,10 @@ class TestOpenApiHumanInputFormGet: from controllers.openapi.human_input_form import OpenApiWorkflowHumanInputFormApi form = SimpleNamespace( - app_id="other-app", tenant_id="tenant-1", expiration_time=datetime(2099, 1, 1, tzinfo=UTC) + app_id="other-app", + tenant_id="tenant-1", + recipient_type=RecipientType.STANDALONE_WEB_APP, + expiration_time=datetime(2099, 1, 1, tzinfo=UTC), ) service_mock = Mock() service_mock.get_form_by_token.return_value = form @@ -114,7 +118,7 @@ class TestOpenApiHumanInputFormGet: caller = SimpleNamespace(id="acct-1") with app.test_request_context("/openapi/v1/apps/app-1/form/human_input/tok-1"): - with pytest.raises(NotFound): + with pytest.raises(HumanInputFormNotFound): api.get.__wrapped__( api, app_id="app-1", @@ -142,7 +146,7 @@ class TestOpenApiHumanInputFormGet: caller = SimpleNamespace(id="acct-1") with app.test_request_context("/openapi/v1/apps/app-1/form/human_input/tok-1"): - with pytest.raises(NotFound): + with pytest.raises(RecipientSurfaceMismatch): api.get.__wrapped__( api, app_id="app-1", @@ -234,6 +238,38 @@ class TestOpenApiHumanInputFormPost: ) assert result == ({}, 200) + def test_post_standalone_web_app_recipient_submits( + self, app: Flask, bypass_pipeline, monkeypatch: pytest.MonkeyPatch + ): + from controllers.openapi.human_input_form import OpenApiWorkflowHumanInputFormApi + + form = self._make_form(recipient_type=RecipientType.STANDALONE_WEB_APP) + service_mock = Mock() + service_mock.get_form_by_token.return_value = form + + module = sys.modules["controllers.openapi.human_input_form"] + monkeypatch.setattr(module, "HumanInputService", lambda _engine: service_mock) + monkeypatch.setattr(module, "db", SimpleNamespace(engine=object())) + + api = OpenApiWorkflowHumanInputFormApi() + app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1") + caller = SimpleNamespace(id="anyone") + + with app.test_request_context( + "/openapi/v1/apps/app-1/form/human_input/tok-1", + method="POST", + json={"action": "approve", "inputs": {}}, + ): + result = api.post.__wrapped__( + api, + app_id="app-1", + form_token="tok-1", + auth_data=_make_auth_data(app_model, caller, "end_user"), + ) + + service_mock.submit_form_by_token.assert_called_once() + assert result == ({}, 200) + def test_post_rejects_invalid_body_with_422(self, app: Flask, bypass_pipeline): """Malformed body → 422 via @accepts (was an unmapped pydantic error → 500).""" from controllers.openapi.human_input_form import OpenApiWorkflowHumanInputFormApi diff --git a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py index 8686f49a4a8..576b95110a0 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_hitl_service_api.py @@ -29,7 +29,7 @@ from core.app.entities.task_entities import ( WorkflowPauseStreamResponse, ) from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper -from core.workflow.human_input_policy import HumanInputSurface +from core.workflow.human_input_policy import FormDisposition, HumanInputSurface from core.workflow.system_variables import build_system_variables from graphon.entities import WorkflowStartReason from graphon.entities.pause_reason import HumanInputRequired, PauseReasonType @@ -592,8 +592,10 @@ class TestHitlServiceApi: monkeypatch.setattr(workflow_response_converter, "db", SimpleNamespace(engine=object())) monkeypatch.setattr( workflow_response_converter, - "load_form_tokens_by_form_id", - lambda form_ids, session=None, surface=None: {"form-1": "token"}, + "load_form_dispositions_by_form_id", + lambda form_ids, session=None, surface=None: { + "form-1": FormDisposition(form_token="token", approval_channels=[]) + }, ) reason = HumanInputRequired( @@ -652,8 +654,10 @@ class TestHitlServiceApi: snapshot = _build_snapshot(WorkflowNodeExecutionStatus.PAUSED) resumption_context = _build_resumption_context("task-ctx") monkeypatch.setattr( - "services.workflow_event_snapshot_service.load_form_tokens_by_form_id", - lambda form_ids, session=None, surface=None: {"form-1": "wtok"}, + "services.workflow_event_snapshot_service.load_form_dispositions_by_form_id", + lambda form_ids, session=None, surface=None: { + "form-1": FormDisposition(form_token="wtok", approval_channels=[]) + }, ) class _SessionContext: diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py index b75f6d44943..28f416ac27f 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py @@ -175,6 +175,7 @@ class TestAdvancedChatGenerateTaskPipeline: "actions": [{"id": "approve", "title": "Approve", "button_style": "default"}], "display_in_ui": True, "form_token": "token-1", + "approval_channels": [], "resolved_default_values": {}, "expiration_time": 123, } diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py index 319e603b351..098a63a8f91 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py @@ -26,6 +26,26 @@ from models.account import Account from models.human_input import RecipientType +class _FakeSession: + """Stub session: `execute` feeds the form-expiration query, `scalars` the recipients.""" + + def __init__(self, *, execute_rows=(), scalars_rows=()): + self._execute_rows = execute_rows + self._scalars_rows = scalars_rows + + def execute(self, _stmt): + return list(self._execute_rows) + + def scalars(self, _stmt): + return list(self._scalars_rows) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + class _RecordingWorkflowAppRunner(WorkflowAppRunner): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -97,11 +117,11 @@ def test_graph_run_paused_event_emits_queue_pause_event(): assert queue_event.paused_nodes == ["node-pause-1"] -def _build_converter(): +def _build_converter(*, invoke_from: InvokeFrom = InvokeFrom.SERVICE_API): application_generate_entity = SimpleNamespace( inputs={}, files=[], - invoke_from=InvokeFrom.SERVICE_API, + invoke_from=invoke_from, app_config=SimpleNamespace(app_id="app-id", tenant_id="tenant-id"), ) system_variables = build_system_variables( @@ -131,32 +151,15 @@ def test_queue_workflow_paused_event_to_stream_responses(monkeypatch: pytest.Mon ) expiration_time = datetime(2024, 1, 1, tzinfo=UTC) + session = _FakeSession( + execute_rows=[("form-1", expiration_time, '{"display_in_ui": true}')], + scalars_rows=[ + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.CONSOLE, access_token="console-token"), + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.BACKSTAGE, access_token="backstage-token"), + ], + ) - class _FakeSession: - def execute(self, _stmt): - return [("form-1", expiration_time, '{"display_in_ui": true}')] - - def scalars(self, _stmt): - return [ - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.CONSOLE, - access_token="console-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.BACKSTAGE, - access_token="backstage-token", - ), - ] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - monkeypatch.setattr(workflow_response_converter, "Session", lambda **_: _FakeSession()) + monkeypatch.setattr(workflow_response_converter, "Session", lambda **_: session) monkeypatch.setattr(workflow_response_converter, "db", SimpleNamespace(engine=object())) reason = HumanInputRequired( @@ -195,10 +198,92 @@ def test_queue_workflow_paused_event_to_stream_responses(monkeypatch: pytest.Mon assert hi_resp.data.inputs[0].output_variable_name == "field" assert hi_resp.data.actions[0].id == "approve" assert hi_resp.data.display_in_ui is True - assert hi_resp.data.form_token == "backstage-token" + assert hi_resp.data.form_token is None + assert hi_resp.data.approval_channels == ["console"] assert hi_resp.data.expiration_time == int(expiration_time.timestamp()) +def _build_paused_human_input_response(monkeypatch, recipients): + """Drive the live OPENAPI pause path with the given recipients via a fake session.""" + converter = _build_converter(invoke_from=InvokeFrom.OPENAPI) + converter.workflow_start_to_stream_response( + task_id="task", + workflow_run_id="run-id", + workflow_id="workflow-id", + reason=WorkflowStartReason.INITIAL, + ) + + expiration_time = datetime(2024, 1, 1, tzinfo=UTC) + session = _FakeSession( + execute_rows=[("form-1", expiration_time, '{"display_in_ui": true}')], + scalars_rows=list(recipients), + ) + + monkeypatch.setattr(workflow_response_converter, "Session", lambda **_: session) + monkeypatch.setattr(workflow_response_converter, "db", SimpleNamespace(engine=object())) + + reason = HumanInputRequired( + form_id="form-1", + form_content="Rendered", + inputs=[ParagraphInputConfig(output_variable_name="field")], + actions=[UserActionConfig(id="approve", title="Approve")], + node_id="node-id", + node_title="Human Step", + ) + queue_event = QueueWorkflowPausedEvent( + reasons=[reason], + outputs={}, + paused_nodes=["node-id"], + ) + + runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0) + responses = converter.workflow_pause_to_stream_response( + event=queue_event, + task_id="task", + graph_runtime_state=runtime_state, + ) + assert isinstance(responses[0], HumanInputRequiredResponse) + return responses + + +def test_openapi_pause_without_web_app_recipient_emits_approval_channels(monkeypatch: pytest.MonkeyPatch): + responses = _build_paused_human_input_response( + monkeypatch, + recipients=[ + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.EMAIL_MEMBER, access_token="email-token"), + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.BACKSTAGE, access_token="backstage-token"), + ], + ) + + hi_resp = responses[0] + assert hi_resp.data.form_token is None + assert hi_resp.data.approval_channels == ["console", "email"] + + pause_resp = responses[-1] + assert pause_resp.data.reasons[0]["approval_channels"] == ["console", "email"] + + +def test_openapi_pause_with_web_app_recipient_sets_token_and_channels(monkeypatch: pytest.MonkeyPatch): + responses = _build_paused_human_input_response( + monkeypatch, + recipients=[ + SimpleNamespace( + form_id="form-1", + recipient_type=RecipientType.STANDALONE_WEB_APP, + access_token="web-app-token", + ), + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.BACKSTAGE, access_token="backstage-token"), + ], + ) + + hi_resp = responses[0] + assert hi_resp.data.form_token == "web-app-token" + assert hi_resp.data.approval_channels == ["console"] + + pause_resp = responses[-1] + assert pause_resp.data.reasons[0]["approval_channels"] == ["console"] + + def test_queue_workflow_paused_event_resolves_variable_select_options(monkeypatch: pytest.MonkeyPatch): converter = _build_converter() converter.workflow_start_to_stream_response( @@ -209,21 +294,9 @@ def test_queue_workflow_paused_event_resolves_variable_select_options(monkeypatc ) expiration_time = datetime(2024, 1, 1, tzinfo=UTC) + session = _FakeSession(execute_rows=[("form-1", expiration_time, '{"display_in_ui": true}')]) - class _FakeSession: - def execute(self, _stmt): - return [("form-1", expiration_time, '{"display_in_ui": true}')] - - def scalars(self, _stmt): - return [] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - monkeypatch.setattr(workflow_response_converter, "Session", lambda **_: _FakeSession()) + monkeypatch.setattr(workflow_response_converter, "Session", lambda **_: session) monkeypatch.setattr(workflow_response_converter, "db", SimpleNamespace(engine=object())) reason = HumanInputRequired( diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py index ea21a1cc1a6..0aaee900e37 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py @@ -134,6 +134,7 @@ class TestWorkflowGenerateTaskPipeline: "actions": [], "display_in_ui": False, "form_token": None, + "approval_channels": [], "resolved_default_values": {}, "expiration_time": 1, } diff --git a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py index 5bd35e6d3c2..27c32d47ee2 100644 --- a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py @@ -32,6 +32,7 @@ from models.human_input import ( EmailMemberRecipientPayload, HumanInputFormRecipient, RecipientType, + StandaloneWebAppRecipientPayload, ) @@ -307,6 +308,9 @@ class _DummyRecipient: recipient_type: RecipientType access_token: str form: _DummyForm | None = None + recipient_payload: str = dataclasses.field( + default_factory=lambda: StandaloneWebAppRecipientPayload().model_dump_json() + ) class _FakeScalarResult: diff --git a/api/tests/unit_tests/core/workflow/test_enrich_pause_reasons.py b/api/tests/unit_tests/core/workflow/test_enrich_pause_reasons.py new file mode 100644 index 00000000000..f53a8eba734 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/test_enrich_pause_reasons.py @@ -0,0 +1,63 @@ +import pytest + +from core.workflow.human_input_policy import FormDisposition, enrich_human_input_pause_reasons +from graphon.entities.pause_reason import PauseReasonType + +_HUMAN_INPUT_REASON = {"TYPE": PauseReasonType.HUMAN_INPUT_REQUIRED, "form_id": "f1"} + + +@pytest.mark.parametrize( + ("dispositions", "expected_token", "expected_channels"), + [ + ({"f1": FormDisposition(form_token=None, approval_channels=["console", "email"])}, None, ["console", "email"]), + ({"f1": FormDisposition(form_token="tok", approval_channels=[])}, "tok", []), + # form_id absent from the map (no recipient rows) falls back to no token, no channels. + ({}, None, []), + ], +) +def test_enrich_projects_disposition_onto_reason(dispositions, expected_token, expected_channels): + out = enrich_human_input_pause_reasons( + [dict(_HUMAN_INPUT_REASON)], + dispositions_by_form_id=dispositions, + expiration_times_by_form_id={}, + ) + + assert out[0]["form_token"] == expected_token + assert out[0]["approval_channels"] == expected_channels + + +def test_enrich_leaves_non_human_input_reasons_untouched(): + reason = {"TYPE": "something_else", "form_id": "f1"} + + out = enrich_human_input_pause_reasons( + [reason], + dispositions_by_form_id={"f1": FormDisposition(form_token="tok", approval_channels=["email"])}, + expiration_times_by_form_id={}, + ) + + assert out[0] == reason + assert "form_token" not in out[0] + assert "approval_channels" not in out[0] + + +def test_pause_reason_payload_carries_approval_channels_through_factory(): + # from_response_data maps fields by hand; this guards approval_channels/form_token + # (the fields this feature added) against being dropped in that mapping. + from core.app.entities.task_entities import ( + HumanInputRequiredPauseReasonPayload, + HumanInputRequiredResponse, + ) + + data = HumanInputRequiredResponse.Data( + form_id="f", + node_id="n", + node_title="t", + form_content="c", + expiration_time=123, + form_token=None, + approval_channels=["console"], + ) + payload = HumanInputRequiredPauseReasonPayload.from_response_data(data) + + assert payload.approval_channels == ["console"] + assert payload.form_token is None diff --git a/api/tests/unit_tests/core/workflow/test_human_input_forms.py b/api/tests/unit_tests/core/workflow/test_human_input_forms.py index e508815b35e..c84c7d578be 100644 --- a/api/tests/unit_tests/core/workflow/test_human_input_forms.py +++ b/api/tests/unit_tests/core/workflow/test_human_input_forms.py @@ -1,7 +1,16 @@ from types import SimpleNamespace -from core.workflow.human_input_forms import _load_form_tokens_by_form_id, load_form_tokens_by_form_id -from core.workflow.human_input_policy import HumanInputSurface +import pytest + +from core.workflow.human_input_forms import ( + load_form_dispositions_by_form_id, + load_form_tokens_by_form_id, +) +from core.workflow.human_input_policy import ( + FormDisposition, + HumanInputSurface, + disposition_for_surface, +) from models.human_input import RecipientType @@ -13,91 +22,100 @@ class _FakeSession: return self._recipients -def test_load_form_tokens_by_form_id_prefers_backstage_token() -> None: +def _recipient(form_id: str, recipient_type: RecipientType, access_token: str | None) -> SimpleNamespace: + return SimpleNamespace(form_id=form_id, recipient_type=recipient_type, access_token=access_token) + + +@pytest.mark.parametrize( + ("surface", "expected_token"), + [ + # Unfiltered (no surface) picks the highest-priority recipient: backstage. + (None, "backstage-token"), + # SERVICE_API may only act on the web-app recipient. + (HumanInputSurface.SERVICE_API, "web-token"), + ], +) +def test_load_form_tokens_picks_token_for_surface(surface, expected_token) -> None: session = _FakeSession( - recipients=[ - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.STANDALONE_WEB_APP, - access_token="web-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.CONSOLE, - access_token="console-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.BACKSTAGE, - access_token="backstage-token", - ), + [ + _recipient("form-1", RecipientType.STANDALONE_WEB_APP, "web-token"), + _recipient("form-1", RecipientType.CONSOLE, "console-token"), + _recipient("form-1", RecipientType.BACKSTAGE, "backstage-token"), ] ) - assert load_form_tokens_by_form_id(["form-1"], session=session) == {"form-1": "backstage-token"} + assert load_form_tokens_by_form_id(["form-1"], session=session, surface=surface) == {"form-1": expected_token} -def test_load_form_tokens_by_form_id_ignores_unsupported_recipients() -> None: +def test_load_form_tokens_drops_forms_without_actionable_token() -> None: session = _FakeSession( - recipients=[ - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.EMAIL_MEMBER, - access_token="email-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.CONSOLE, - access_token=None, - ), + [ + _recipient("form-1", RecipientType.EMAIL_MEMBER, "email-token"), + _recipient("form-1", RecipientType.CONSOLE, None), ] ) assert load_form_tokens_by_form_id(["form-1"], session=session) == {} -def test_load_form_tokens_by_form_id_uses_shared_priority() -> None: +def test_load_form_tokens_service_api_surface_uses_web_token() -> None: session = _FakeSession( - recipients=[ - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.STANDALONE_WEB_APP, - access_token="web-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.CONSOLE, - access_token="console-token", - ), + [ + _recipient("form-1", RecipientType.STANDALONE_WEB_APP, "web-token"), + _recipient("form-1", RecipientType.CONSOLE, "console-token"), + _recipient("form-1", RecipientType.BACKSTAGE, "backstage-token"), ] ) - assert _load_form_tokens_by_form_id(session, ["form-1"]) == {"form-1": "console-token"} + assert load_form_tokens_by_form_id(["form-1"], session=session, surface=HumanInputSurface.SERVICE_API) == { + "form-1": "web-token" + } -def test_load_form_tokens_by_form_id_uses_web_token_for_service_api_surface() -> None: +def test_load_dispositions_openapi_webapp_form_is_resumable() -> None: session = _FakeSession( - recipients=[ - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.STANDALONE_WEB_APP, - access_token="web-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.CONSOLE, - access_token="console-token", - ), - SimpleNamespace( - form_id="form-1", - recipient_type=RecipientType.BACKSTAGE, - access_token="backstage-token", - ), + [ + _recipient("form-1", RecipientType.STANDALONE_WEB_APP, "web-token"), + _recipient("form-1", RecipientType.BACKSTAGE, "backstage-token"), ] ) - assert load_form_tokens_by_form_id( - ["form-1"], - session=session, - surface=HumanInputSurface.SERVICE_API, - ) == {"form-1": "web-token"} + assert load_form_dispositions_by_form_id(["form-1"], session=session, surface=HumanInputSurface.OPENAPI) == { + "form-1": FormDisposition(form_token="web-token", approval_channels=["console"]) + } + + +def test_load_dispositions_openapi_backstage_only_form_yields_channels_not_token() -> None: + session = _FakeSession([_recipient("form-1", RecipientType.BACKSTAGE, "backstage-token")]) + + assert load_form_dispositions_by_form_id(["form-1"], session=session, surface=HumanInputSurface.OPENAPI) == { + "form-1": FormDisposition(form_token=None, approval_channels=["console"]) + } + + +# disposition_for_surface partitions recipients into a surface-actionable resume +# token plus the approval channels of the recipients the surface may NOT act on. +_WEB = (RecipientType.STANDALONE_WEB_APP, "tok_web") +_BACKSTAGE = (RecipientType.BACKSTAGE, "tok_b") +_CONSOLE = (RecipientType.CONSOLE, "tok_c") +_EMAIL_MEMBER = (RecipientType.EMAIL_MEMBER, "t1") +_EMAIL_EXTERNAL = (RecipientType.EMAIL_EXTERNAL, "t2") + + +@pytest.mark.parametrize( + ("recipients", "surface", "expected"), + [ + # Token surface acts on the web-app recipient; blocked recipients become channels. + ([_BACKSTAGE, _WEB], HumanInputSurface.OPENAPI, FormDisposition("tok_web", ["console"])), + ([_EMAIL_MEMBER, _EMAIL_EXTERNAL], HumanInputSurface.OPENAPI, FormDisposition(None, ["email"])), + ([_EMAIL_MEMBER, _BACKSTAGE], HumanInputSurface.OPENAPI, FormDisposition(None, ["console", "email"])), + # CONSOLE acts on console/backstage; a web-app recipient is blocked → web_app channel. + ([_CONSOLE, _WEB], HumanInputSurface.CONSOLE, FormDisposition("tok_c", ["web_app"])), + ([_WEB], HumanInputSurface.CONSOLE, FormDisposition(None, ["web_app"])), + # No surface: unfiltered priority token, channels never populated. + ([_BACKSTAGE], None, FormDisposition("tok_b", [])), + ([_WEB, _EMAIL_MEMBER], None, FormDisposition("tok_web", [])), + ], +) +def test_disposition_for_surface_partitions_token_and_channels(recipients, surface, expected) -> None: + assert disposition_for_surface(recipients, surface=surface) == expected diff --git a/api/tests/unit_tests/core/workflow/test_human_input_policy.py b/api/tests/unit_tests/core/workflow/test_human_input_policy.py index 651b69216ae..a1c6fee98cf 100644 --- a/api/tests/unit_tests/core/workflow/test_human_input_policy.py +++ b/api/tests/unit_tests/core/workflow/test_human_input_policy.py @@ -12,38 +12,28 @@ from graphon.runtime import VariablePool from models.human_input import RecipientType -def test_service_api_only_allows_public_webapp_forms() -> None: - assert is_recipient_type_allowed_for_surface( - RecipientType.STANDALONE_WEB_APP, - HumanInputSurface.SERVICE_API, - ) - assert not is_recipient_type_allowed_for_surface( - RecipientType.CONSOLE, - HumanInputSurface.SERVICE_API, - ) - assert not is_recipient_type_allowed_for_surface( - RecipientType.BACKSTAGE, - HumanInputSurface.SERVICE_API, - ) - assert not is_recipient_type_allowed_for_surface( - RecipientType.EMAIL_MEMBER, - HumanInputSurface.SERVICE_API, - ) - - -def test_console_only_allows_internal_console_surfaces() -> None: - assert is_recipient_type_allowed_for_surface( - RecipientType.CONSOLE, - HumanInputSurface.CONSOLE, - ) - assert is_recipient_type_allowed_for_surface( - RecipientType.BACKSTAGE, - HumanInputSurface.CONSOLE, - ) - assert not is_recipient_type_allowed_for_surface( - RecipientType.STANDALONE_WEB_APP, - HumanInputSurface.CONSOLE, - ) +# Token surfaces (SERVICE_API, OPENAPI) may act only on public web-app forms; +# CONSOLE may act on internal console/backstage forms. OPENAPI mirrors SERVICE_API +# today but is pinned independently because the two are expected to diverge. +@pytest.mark.parametrize( + ("recipient_type", "surface", "allowed"), + [ + (RecipientType.STANDALONE_WEB_APP, HumanInputSurface.SERVICE_API, True), + (RecipientType.CONSOLE, HumanInputSurface.SERVICE_API, False), + (RecipientType.BACKSTAGE, HumanInputSurface.SERVICE_API, False), + (RecipientType.EMAIL_MEMBER, HumanInputSurface.SERVICE_API, False), + (RecipientType.STANDALONE_WEB_APP, HumanInputSurface.OPENAPI, True), + (RecipientType.CONSOLE, HumanInputSurface.OPENAPI, False), + (RecipientType.BACKSTAGE, HumanInputSurface.OPENAPI, False), + (RecipientType.CONSOLE, HumanInputSurface.CONSOLE, True), + (RecipientType.BACKSTAGE, HumanInputSurface.CONSOLE, True), + (RecipientType.STANDALONE_WEB_APP, HumanInputSurface.CONSOLE, False), + ], +) +def test_recipient_type_allowed_per_surface( + recipient_type: RecipientType, surface: HumanInputSurface, allowed: bool +) -> None: + assert is_recipient_type_allowed_for_surface(recipient_type, surface) is allowed def test_preferred_form_token_uses_shared_priority_order() -> None: @@ -56,6 +46,17 @@ def test_preferred_form_token_uses_shared_priority_order() -> None: assert get_preferred_form_token(recipients) == "backstage-token" +def test_preferred_form_token_skips_prioritized_type_with_empty_token() -> None: + # An empty token is not actionable: the highest-priority recipient that + # actually carries a token wins, not the highest-priority type. + recipients = [ + (RecipientType.BACKSTAGE, ""), + (RecipientType.CONSOLE, "console-token"), + ] + + assert get_preferred_form_token(recipients) == "console-token" + + def test_resolve_variable_select_input_options_uses_runtime_values() -> None: variable_pool = VariablePool() variable_pool.add(("start", "options"), ["approve", "reject"]) diff --git a/api/tests/unit_tests/core/workflow/test_human_input_policy_openapi.py b/api/tests/unit_tests/core/workflow/test_human_input_policy_openapi.py deleted file mode 100644 index b78e821237d..00000000000 --- a/api/tests/unit_tests/core/workflow/test_human_input_policy_openapi.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Tests for OPENAPI surface in HumanInputPolicy and human_input_forms.""" - -from __future__ import annotations - -from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface -from models.human_input import RecipientType - - -def test_openapi_surface_exists(): - assert HumanInputSurface.OPENAPI == "openapi" - - -def test_openapi_allows_standalone_web_app(): - assert is_recipient_type_allowed_for_surface(RecipientType.STANDALONE_WEB_APP, HumanInputSurface.OPENAPI) - - -def test_openapi_rejects_console_recipient(): - assert not is_recipient_type_allowed_for_surface(RecipientType.CONSOLE, HumanInputSurface.OPENAPI) - - -def test_openapi_rejects_backstage_recipient(): - assert not is_recipient_type_allowed_for_surface(RecipientType.BACKSTAGE, HumanInputSurface.OPENAPI) - - -def test_get_surface_form_token_openapi_picks_standalone_web_app(): - """OPENAPI surface should pick STANDALONE_WEB_APP token, same as SERVICE_API.""" - from core.workflow.human_input_forms import _get_surface_form_token - - recipients = [ - (RecipientType.BACKSTAGE, "backstage-token"), - (RecipientType.STANDALONE_WEB_APP, "web-token"), - ] - token = _get_surface_form_token(recipients, surface=HumanInputSurface.OPENAPI) - assert token == "web-token" diff --git a/api/tests/unit_tests/models/test_recipient_type_label.py b/api/tests/unit_tests/models/test_recipient_type_label.py new file mode 100644 index 00000000000..3e98c17ca99 --- /dev/null +++ b/api/tests/unit_tests/models/test_recipient_type_label.py @@ -0,0 +1,21 @@ +import pytest + +from models.human_input import ApprovalChannel, RecipientType + + +@pytest.mark.parametrize( + ("recipient_type", "expected_channel"), + [ + (RecipientType.EMAIL_MEMBER, ApprovalChannel.EMAIL), + (RecipientType.EMAIL_EXTERNAL, ApprovalChannel.EMAIL), + (RecipientType.CONSOLE, ApprovalChannel.CONSOLE), + (RecipientType.BACKSTAGE, ApprovalChannel.CONSOLE), + (RecipientType.STANDALONE_WEB_APP, ApprovalChannel.WEB_APP), + ], +) +def test_approval_channel_collapses_delivery_types( + recipient_type: RecipientType, expected_channel: ApprovalChannel +) -> None: + # Both email types collapse to EMAIL and console/backstage to CONSOLE: + # the user-facing approval channel, not the internal recipient type. + assert recipient_type.approval_channel == expected_channel diff --git a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py index eafbabe1f9b..dc3a6a31205 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py @@ -16,12 +16,14 @@ from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.task_entities import StreamEvent from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper +from core.workflow.human_input_policy import FormDisposition, HumanInputSurface from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus from graphon.nodes.human_input.entities import SelectInputConfig, StringListSource from graphon.nodes.human_input.enums import ValueSourceType from graphon.runtime import GraphRuntimeState, VariablePool from models.enums import CreatorUserRole +from models.human_input import RecipientType from models.model import AppMode from models.workflow import WorkflowRun from repositories.api_workflow_node_execution_repository import WorkflowNodeExecutionSnapshot @@ -763,7 +765,11 @@ def test_build_snapshot_events_preserves_public_form_token(monkeypatch: pytest.M snapshot = _build_snapshot(WorkflowNodeExecutionStatus.PAUSED) resumption_context = _build_resumption_context("task-ctx") monkeypatch.setattr( - service_module, "load_form_tokens_by_form_id", lambda form_ids, session=None, surface=None: {"form-1": "wtok"} + service_module, + "load_form_dispositions_by_form_id", + lambda form_ids, session=None, surface=None: { + "form-1": FormDisposition(form_token="wtok", approval_channels=[]) + }, ) session_maker = _SessionMaker( SimpleNamespace( @@ -803,12 +809,99 @@ def test_build_snapshot_events_preserves_public_form_token(monkeypatch: pytest.M assert pause_data["reasons"][0]["expiration_time"] == int(datetime(2024, 1, 1, tzinfo=UTC).timestamp()) +def _build_recipient_snapshot_events(recipients: Sequence[Any]) -> list[Mapping[str, Any]]: + """Drive the reconnect snapshot pause path for the OPENAPI surface. + + Lets the real disposition loader run against a fake session whose ``scalars`` + yields the given recipients, so the reconnect path derives the same token and + approval channels as the live path for the same recipient set. + """ + workflow_run = _build_workflow_run(WorkflowExecutionStatus.PAUSED) + snapshot = _build_snapshot(WorkflowNodeExecutionStatus.PAUSED) + resumption_context = _build_resumption_context("task-ctx") + expiration_time = datetime(2024, 1, 1, tzinfo=UTC) + session_maker = _SessionMaker( + SimpleNamespace( + execute=lambda _stmt: [("form-1", expiration_time, '{"display_in_ui": true}')], + scalars=lambda _stmt: list(recipients), + ) + ) + pause_entity = _FakePauseEntity( + pause_id="pause-1", + workflow_run_id="run-1", + paused_at_value=expiration_time, + pause_reasons=[ + HumanInputRequired( + form_id="form-1", + form_content="content", + node_id="node-1", + node_title="Human Input", + ) + ], + ) + + return _build_snapshot_events( + workflow_run=workflow_run, + node_snapshots=[snapshot], + task_id="task-ctx", + message_context=None, + pause_entity=pause_entity, + resumption_context=resumption_context, + session_maker=cast(sessionmaker[Session], session_maker), + human_input_surface=HumanInputSurface.OPENAPI, + ) + + +def test_reconnect_pause_without_web_app_recipient_emits_approval_channels() -> None: + events = _build_recipient_snapshot_events( + recipients=[ + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.EMAIL_MEMBER, access_token="email-token"), + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.BACKSTAGE, access_token="backstage-token"), + ], + ) + + human_input_event = events[-2] + assert human_input_event["event"] == StreamEvent.HUMAN_INPUT_REQUIRED + assert human_input_event["data"]["form_token"] is None + assert human_input_event["data"]["approval_channels"] == ["console", "email"] + + pause_data = events[-1]["data"] + assert pause_data["reasons"][0]["form_token"] is None + assert pause_data["reasons"][0]["approval_channels"] == ["console", "email"] + + +def test_reconnect_pause_with_web_app_recipient_sets_token_and_channels() -> None: + events = _build_recipient_snapshot_events( + recipients=[ + SimpleNamespace( + form_id="form-1", + recipient_type=RecipientType.STANDALONE_WEB_APP, + access_token="web-app-token", + ), + SimpleNamespace(form_id="form-1", recipient_type=RecipientType.BACKSTAGE, access_token="backstage-token"), + ], + ) + + human_input_event = events[-2] + assert human_input_event["event"] == StreamEvent.HUMAN_INPUT_REQUIRED + assert human_input_event["data"]["form_token"] == "web-app-token" + assert human_input_event["data"]["approval_channels"] == ["console"] + + pause_data = events[-1]["data"] + assert pause_data["reasons"][0]["form_token"] == "web-app-token" + assert pause_data["reasons"][0]["approval_channels"] == ["console"] + + def test_build_snapshot_events_resolves_pause_reason_select_options(monkeypatch: pytest.MonkeyPatch) -> None: workflow_run = _build_workflow_run(WorkflowExecutionStatus.PAUSED) snapshot = _build_snapshot(WorkflowNodeExecutionStatus.PAUSED) resumption_context = _build_resumption_context("task-ctx", select_options=["approve", "reject"]) monkeypatch.setattr( - service_module, "load_form_tokens_by_form_id", lambda form_ids, session=None, surface=None: {"form-1": "wtok"} + service_module, + "load_form_dispositions_by_form_id", + lambda form_ids, session=None, surface=None: { + "form-1": FormDisposition(form_token="wtok", approval_channels=[]) + }, ) session_maker = _SessionMaker( SimpleNamespace( @@ -886,7 +979,11 @@ def test_build_workflow_event_stream_loads_pause_tokens_without_flask_app_contex service_module, "_load_resumption_context", MagicMock(return_value=_build_resumption_context("task-1")) ) monkeypatch.setattr( - service_module, "load_form_tokens_by_form_id", lambda form_ids, session=None, surface=None: {"form-1": "wtok"} + service_module, + "load_form_dispositions_by_form_id", + lambda form_ids, session=None, surface=None: { + "form-1": FormDisposition(form_token="wtok", approval_channels=[]) + }, ) session = SimpleNamespace( diff --git a/cli/src/commands/run/app/hitl-render.test.ts b/cli/src/commands/run/app/hitl-render.test.ts new file mode 100644 index 00000000000..92d91575735 --- /dev/null +++ b/cli/src/commands/run/app/hitl-render.test.ts @@ -0,0 +1,68 @@ +import type { HitlPauseData, HitlPausePayload } from './sse-collector' +import { describe, expect, it } from 'vitest' +import { buildHitlExitObject, renderHitlHint } from './hitl-render' + +function payload(overrides: Partial = {}): HitlPausePayload { + return { + event: 'human_input_required', + task_id: 'task-1', + workflow_run_id: 'run-1', + data: { + form_id: 'form-1', + node_id: 'node-1', + node_title: 'Approve', + form_content: 'Please approve', + inputs: [], + actions: [], + display_in_ui: false, + form_token: null, + approval_channels: ['email'], + resolved_default_values: {}, + expiration_time: 0, + ...overrides, + }, + } +} + +describe('renderHitlHint — non-resumable form (form_token null)', () => { + it.each<[string[], string]>([ + [['email'], 'form delivered via email — resume only from that channel'], + [['console'], 'form delivered via the console — resume only from that channel'], + [['web_app'], 'form delivered via the web app — resume only from that channel'], + [['console', 'email'], 'form delivered via the console or email — resume only from those channels'], + [[], 'form delivered via another channel — resume only from that channel'], + ])('renders %j as the channel note', (channels, expected) => { + const out = renderHitlHint('app-1', payload({ approval_channels: channels }), false) + expect(out).toBe(`hint: workflow paused — ${expected}\n`) + expect(out).not.toContain('difyctl resume') + }) + + it('falls back to a generic note when approval_channels is absent (older server)', () => { + const p = payload() + delete p.data.approval_channels + const out = renderHitlHint('app-1', p, false) + expect(out).toContain('another channel') + }) +}) + +describe('renderHitlHint — resumable form (form_token present)', () => { + it('renders the resume command and ignores approval_channels', () => { + const out = renderHitlHint('app-1', payload({ form_token: 'tok-123', approval_channels: [] }), false) + expect(out).toContain('difyctl resume app app-1 tok-123 --workflow-run-id run-1') + expect(out).not.toContain('delivered via') + }) +}) + +describe('buildHitlExitObject', () => { + it('carries approval_channels into the JSON exit object', () => { + const obj = buildHitlExitObject('app-1', payload({ approval_channels: ['email'] })) + expect(obj.approval_channels).toEqual(['email']) + expect(obj.form_token).toBeNull() + }) + + it('defaults approval_channels to [] when absent', () => { + const p = payload({ form_token: 'tok' }) + delete p.data.approval_channels + expect(buildHitlExitObject('app-1', p).approval_channels).toEqual([]) + }) +}) diff --git a/cli/src/commands/run/app/hitl-render.ts b/cli/src/commands/run/app/hitl-render.ts index 991c63e1112..6eb251f4b38 100644 --- a/cli/src/commands/run/app/hitl-render.ts +++ b/cli/src/commands/run/app/hitl-render.ts @@ -10,6 +10,7 @@ export type HitlExitObject = { node_id: string node_title: string form_token: string | null + approval_channels: string[] form_content: string inputs: unknown[] actions: unknown[] @@ -29,6 +30,7 @@ export function buildHitlExitObject(appId: string, payload: HitlPausePayload): H node_id: d.node_id, node_title: d.node_title, form_token: d.form_token, + approval_channels: d.approval_channels ?? [], form_content: d.form_content, inputs: d.inputs, actions: d.actions, @@ -92,15 +94,35 @@ export function renderHitlOutput(appId: string, payload: HitlPausePayload, isTex return `${renderHitlExit(obj)}\n` } -const EXTERNAL_CHANNEL_NOTE = 'form delivered via email/external channel — resume only from that channel' +// Server approval-channel labels → human wording for the pause hint. +const APPROVAL_CHANNEL_LABELS: Record = { + email: 'email', + console: 'the console', + web_app: 'the web app', +} + +function describeApprovalChannels(channels: string[]): string { + const labels = channels.map(c => APPROVAL_CHANNEL_LABELS[c] ?? c) + if (labels.length <= 1) + return labels[0] ?? 'another channel' + if (labels.length === 2) + return `${labels[0]} or ${labels[1]}` + return `${labels.slice(0, -1).join(', ')}, or ${labels[labels.length - 1]}` +} + +function externalChannelNote(channels: string[]): string { + const where = channels.length > 1 ? 'those channels' : 'that channel' + return `form delivered via ${describeApprovalChannels(channels)} — resume only from ${where}` +} export function renderHitlHint(appId: string, payload: HitlPausePayload, isErrTTY: boolean): string { const d = payload.data const cs = colorScheme(colorEnabled(isErrTTY)) if (d.form_token === null) { + const note = externalChannelNote(d.approval_channels ?? []) if (!isErrTTY) - return `hint: workflow paused — ${EXTERNAL_CHANNEL_NOTE}\n` - return `${cs.warningIcon()} ${cs.bold('workflow paused')} — ${cs.dim(EXTERNAL_CHANNEL_NOTE)}\n` + return `hint: workflow paused — ${note}\n` + return `${cs.warningIcon()} ${cs.bold('workflow paused')} — ${cs.dim(note)}\n` } const actions = (d.actions ?? []) as { id: string }[] let cmd = `difyctl resume app ${appId} ${d.form_token} --workflow-run-id ${payload.workflow_run_id}` diff --git a/cli/src/commands/run/app/sse-collector.ts b/cli/src/commands/run/app/sse-collector.ts index ba329746e73..043a9690d5f 100644 --- a/cli/src/commands/run/app/sse-collector.ts +++ b/cli/src/commands/run/app/sse-collector.ts @@ -13,6 +13,8 @@ export type HitlPauseData = { actions: unknown[] display_in_ui: boolean form_token: string | null + // Channels where the form can be approved when it is not CLI-resumable, e.g. ['email']. + approval_channels?: string[] resolved_default_values: Record expiration_time: number } diff --git a/cli/test/e2e/suites/run/run-app-hitl.e2e.ts b/cli/test/e2e/suites/run/run-app-hitl.e2e.ts index a582bc5282c..58290331c90 100644 --- a/cli/test/e2e/suites/run/run-app-hitl.e2e.ts +++ b/cli/test/e2e/suites/run/run-app-hitl.e2e.ts @@ -386,15 +386,7 @@ describeExternal('E2E / difyctl run app — HITL display_in_ui=false (4.5.8)', ( await fx.cleanup() }) - it('[P1] 4.5.8 HITL pause with display_in_ui=false: JSON contains display_in_ui=false and exit is 0', async () => { - // Spec 4.5.8: when the Human Input node has display_in_ui=false the CLI - // should indicate the form is delivered via an external channel. - // - // Current CLI behaviour (v1.0): the JSON field display_in_ui is correctly - // set to false. The stderr hint still includes the resume command (the - // "form delivered via external channel" hint is not yet implemented in CLI). - // This test verifies the current actual behaviour and will need updating - // once the CLI implements the display_in_ui=false hint distinction. + it('[P1] 4.5.8 HITL pause with display_in_ui=false: external-channel form is not CLI-resumable', async () => { const result = await fx.r([ 'run', 'app', @@ -407,7 +399,8 @@ describeExternal('E2E / difyctl run app — HITL display_in_ui=false (4.5.8)', ( const parsed = assertJson<{ status: string display_in_ui: boolean - form_token: string + form_token: string | null + approval_channels: string[] workflow_run_id: string }>(result) @@ -417,12 +410,13 @@ describeExternal('E2E / difyctl run app — HITL display_in_ui=false (4.5.8)', ( // status must be paused expect(parsed.status).toBe('paused') - // form_token must be present (resume is still possible even for external delivery) - expect(parsed.form_token, 'form_token must be non-empty').toBeTruthy() + // external delivery is not CLI-resumable: no token, channels name the real route + expect(parsed.form_token, 'form_token must be null for external delivery').toBeNull() + expect(parsed.approval_channels, 'approval_channels must name the delivery channel').toContain('email') - // stderr must contain a hint (current behaviour: hint includes resume command) - expect(result.stderr.trim().length, 'stderr must contain a hint').toBeGreaterThan(0) - expect(result.stderr).toMatch(/hint|resume|paused/i) + // stderr hint must describe the channel, not offer a resume command + expect(result.stderr).toMatch(/delivered via|resume only from/i) + expect(result.stderr).not.toMatch(/difyctl resume/i) }) }) diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index e1217f3f6d7..14f8ef0a818 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -332,6 +332,7 @@ export type OpenApiErrorCode | 'file_too_large' | 'filename_not_exists' | 'forbidden' + | 'form_not_found' | 'internal_server_error' | 'invalid_param' | 'member_license_exceeded' @@ -344,6 +345,7 @@ export type OpenApiErrorCode | 'provider_not_initialize' | 'provider_quota_exceeded' | 'rate_limit_error' + | 'recipient_surface_mismatch' | 'request_entity_too_large' | 'too_many_files' | 'too_many_requests' diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index 51a3cb8f480..57d65e62c0e 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -366,6 +366,7 @@ export const zOpenApiErrorCode = z.enum([ 'file_too_large', 'filename_not_exists', 'forbidden', + 'form_not_found', 'internal_server_error', 'invalid_param', 'member_license_exceeded', @@ -378,6 +379,7 @@ export const zOpenApiErrorCode = z.enum([ 'provider_not_initialize', 'provider_quota_exceeded', 'rate_limit_error', + 'recipient_surface_mismatch', 'request_entity_too_large', 'too_many_files', 'too_many_requests', From c1ab6226a26c103402c5876ca8eea9318ee61a7e Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 22 Jun 2026 14:31:50 +0800 Subject: [PATCH 09/35] fix(agent): support restoring roster versions (#37734) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 23 +++++ api/fields/agent_fields.py | 5 ++ api/models/agent.py | 2 + api/openapi/markdown/console-openapi.md | 21 +++++ api/services/agent/roster_service.py | 57 ++++++++++++- .../console/agent/test_agent_controllers.py | 21 +++++ api/tests/unit_tests/models/test_agent.py | 1 + .../services/agent/test_agent_services.py | 83 ++++++++++++++++++- .../generated/api/console/agent/orpc.gen.ts | 22 ++++- .../generated/api/console/agent/types.gen.ts | 23 +++++ .../generated/api/console/agent/zod.gen.ts | 20 +++++ 11 files changed, 274 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index d4546ac88bf..eff4f910dae 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -36,6 +36,7 @@ from extensions.ext_database import db from fields.agent_fields import ( AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, + AgentConfigSnapshotRestoreResponse, AgentInviteOptionsResponse, AgentLogListResponse, AgentLogMessageListResponse, @@ -223,6 +224,7 @@ register_response_schema_models( AgentAppPartial, AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, + AgentConfigSnapshotRestoreResponse, AgentInviteOptionsResponse, AgentLogListResponse, AgentLogMessageListResponse, @@ -649,3 +651,24 @@ class AgentRosterVersionDetailApi(Resource): version_id=str(version_id), ), ) + + +@console_ns.route("/agent//versions//restore") +class AgentRosterVersionRestoreApi(Resource): + @console_ns.response(200, "Agent version restored", console_ns.models[AgentConfigSnapshotRestoreResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @edit_permission_required + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, current_user: Account, agent_id: UUID, version_id: UUID): + return dump_response( + AgentConfigSnapshotRestoreResponse, + _agent_roster_service().restore_agent_version( + tenant_id=tenant_id, + agent_id=str(agent_id), + version_id=str(version_id), + account_id=current_user.id, + ), + ) diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index ec64395d6fd..07bcbad26e3 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -291,6 +291,11 @@ class AgentConfigSnapshotListResponse(ResponseModel): data: list[AgentConfigSnapshotSummaryResponse] +class AgentConfigSnapshotRestoreResponse(ResponseModel): + result: Literal["success"] + active_config_snapshot_id: str + + class AgentComposerAgentResponse(ResponseModel): id: str name: str diff --git a/api/models/agent.py b/api/models/agent.py index 80abf810922..1905377359f 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -83,6 +83,8 @@ class AgentConfigRevisionOperation(StrEnum): SAVE_NEW_AGENT = "save_new_agent" # Promotes a workflow-only Agent into the reusable Agent Roster. SAVE_TO_ROSTER = "save_to_roster" + # Switches the Agent's current published config back to an existing version. + RESTORE_VERSION = "restore_version" class WorkflowAgentBindingType(StrEnum): diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 85141d63618..5d3407f4159 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -936,6 +936,20 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill | ---- | ----------- | ------ | | 200 | Agent version detail | **application/json**: [AgentConfigSnapshotDetailResponse](#agentconfigsnapshotdetailresponse)
| +### [POST] /agent/{agent_id}/versions/{version_id}/restore +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | +| version_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent version restored | **application/json**: [AgentConfigSnapshotRestoreResponse](#agentconfigsnapshotrestoreresponse)
| + ### [GET] /all-workspaces #### Parameters @@ -12405,6 +12419,13 @@ Audit operation recorded for Agent Soul version/revision changes. | ---- | ---- | ----------- | -------- | | data | [ [AgentConfigSnapshotSummaryResponse](#agentconfigsnapshotsummaryresponse) ] | | Yes | +#### AgentConfigSnapshotRestoreResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| active_config_snapshot_id | string | | Yes | +| result | string | | Yes | + #### AgentConfigSnapshotSummaryResponse | Name | Type | Description | Required | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index ca8428b4f7c..00ea86859d8 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -666,12 +666,16 @@ class AgentRosterService: @staticmethod def _visible_version_operations(agent: Agent) -> set[AgentConfigRevisionOperation]: if agent.source == AgentSource.AGENT_APP: - return {AgentConfigRevisionOperation.SAVE_NEW_VERSION} + return { + AgentConfigRevisionOperation.SAVE_NEW_VERSION, + AgentConfigRevisionOperation.RESTORE_VERSION, + } return { AgentConfigRevisionOperation.CREATE_VERSION, AgentConfigRevisionOperation.SAVE_NEW_VERSION, AgentConfigRevisionOperation.SAVE_NEW_AGENT, AgentConfigRevisionOperation.SAVE_TO_ROSTER, + AgentConfigRevisionOperation.RESTORE_VERSION, } def active_config_is_published(self, *, tenant_id: str, agent: Agent) -> bool: @@ -764,6 +768,46 @@ class AgentRosterService: ] return result + def restore_agent_version( + self, *, tenant_id: str, agent_id: str, version_id: str, account_id: str + ) -> dict[str, Any]: + agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True) + visible_version_ids = self._visible_version_ids_stmt(tenant_id=tenant_id, agent_id=agent_id, agent=agent) + visible_version_id = self._session.scalar( + select(AgentConfigSnapshot.id) + .where( + AgentConfigSnapshot.tenant_id == tenant_id, + AgentConfigSnapshot.agent_id == agent_id, + AgentConfigSnapshot.id == version_id, + AgentConfigSnapshot.id.in_(select(visible_version_ids.c.current_snapshot_id)), + ) + .limit(1) + ) + if not visible_version_id: + raise AgentVersionNotFoundError() + + version = self._get_version(tenant_id=tenant_id, agent_id=agent_id, version_id=version_id) + if agent.active_config_snapshot_id == version.id: + return {"result": "success", "active_config_snapshot_id": version.id} + + previous_snapshot_id = agent.active_config_snapshot_id + agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(version.config_snapshot) + agent.updated_by = account_id + self._session.add( + AgentConfigRevision( + tenant_id=tenant_id, + agent_id=agent_id, + previous_snapshot_id=previous_snapshot_id, + current_snapshot_id=version.id, + revision=self._next_revision(tenant_id=tenant_id, agent_id=agent_id), + operation=AgentConfigRevisionOperation.RESTORE_VERSION, + created_by=account_id, + ) + ) + self._session.commit() + return {"result": "success", "active_config_snapshot_id": version.id} + def _get_agent(self, *, tenant_id: str, agent_id: str, roster_only: bool = False) -> Agent: stmt = select(Agent).where(Agent.tenant_id == tenant_id, Agent.id == agent_id) if roster_only: @@ -789,6 +833,17 @@ class AgentRosterService: raise AgentVersionNotFoundError() return version + def _next_revision(self, *, tenant_id: str, agent_id: str) -> int: + return ( + self._session.scalar( + select(func.max(AgentConfigRevision.revision)).where( + AgentConfigRevision.tenant_id == tenant_id, + AgentConfigRevision.agent_id == agent_id, + ) + ) + or 0 + ) + 1 + def _load_published_active_snapshot_agent_ids(self, *, tenant_id: str, agents: list[Agent]) -> set[str]: predicates = [ and_( diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 8b77772a36d..36f98047738 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -28,6 +28,7 @@ from controllers.console.agent.roster import ( AgentLogsApi, AgentLogSourcesApi, AgentRosterVersionDetailApi, + AgentRosterVersionRestoreApi, AgentRosterVersionsApi, AgentStatisticsSummaryApi, ) @@ -158,6 +159,9 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None: "/agent//logs//messages", "/agent//log-sources", "/agent//statistics/summary", + "/agent//versions", + "/agent//versions/", + "/agent//versions//restore", "/agent/invite-options", ): assert route in paths @@ -513,6 +517,13 @@ def test_agent_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatc ], }, ) + captured_restore: dict[str, object] = {} + + def restore_agent_version(_self, **kwargs): + captured_restore.update(kwargs) + return {"result": "success", "active_config_snapshot_id": kwargs["version_id"]} + + monkeypatch.setattr(roster_controller.AgentRosterService, "restore_agent_version", restore_agent_version) assert ( unwrap(AgentRosterVersionsApi.get)(AgentRosterVersionsApi(), "tenant-1", agent_id)["data"][0]["id"] @@ -523,6 +534,16 @@ def test_agent_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatc ) assert version_detail["id"] == version_id assert version_detail["agent_id"] == agent_id + restored = unwrap(AgentRosterVersionRestoreApi.post)( + AgentRosterVersionRestoreApi(), "tenant-1", SimpleNamespace(id="account-1"), agent_id, version_id + ) + assert restored == {"result": "success", "active_config_snapshot_id": version_id} + assert captured_restore == { + "tenant_id": "tenant-1", + "agent_id": agent_id, + "version_id": version_id, + "account_id": "account-1", + } def test_agent_observability_routes_resolve_app_from_agent_id( diff --git a/api/tests/unit_tests/models/test_agent.py b/api/tests/unit_tests/models/test_agent.py index aabbd4df300..422a3218eaa 100644 --- a/api/tests/unit_tests/models/test_agent.py +++ b/api/tests/unit_tests/models/test_agent.py @@ -33,6 +33,7 @@ def test_agent_enums_match_prd_boundaries(): assert AgentStatus.ACTIVE.value == "active" assert AgentStatus.ARCHIVED.value == "archived" assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION.value == "save_current_version" + assert AgentConfigRevisionOperation.RESTORE_VERSION.value == "restore_version" assert WorkflowAgentBindingType.ROSTER_AGENT.value == "roster_agent" assert WorkflowAgentBindingType.INLINE_AGENT.value == "inline_agent" diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index 2c077e20b46..e5acc43c52b 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -1046,12 +1046,93 @@ def test_agent_app_visible_versions_exclude_draft_saves(): agent_app_operations = AgentRosterService._visible_version_operations(agent_app) roster_operations = AgentRosterService._visible_version_operations(roster_agent) - assert agent_app_operations == {AgentConfigRevisionOperation.SAVE_NEW_VERSION} + assert agent_app_operations == { + AgentConfigRevisionOperation.SAVE_NEW_VERSION, + AgentConfigRevisionOperation.RESTORE_VERSION, + } assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations assert AgentConfigRevisionOperation.CREATE_VERSION in roster_operations + assert AgentConfigRevisionOperation.RESTORE_VERSION in roster_operations assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in roster_operations +def test_restore_roster_agent_version_switches_active_snapshot(monkeypatch: pytest.MonkeyPatch): + fake_session = FakeSession(scalar=["version-2", 6]) + service = AgentRosterService(fake_session) + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + name="Analyst", + description="old", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id="version-4", + ) + version = AgentConfigSnapshot( + id="version-2", + tenant_id="tenant-1", + agent_id="agent-1", + version=2, + config_snapshot=_agent_soul_with_model(), + ) + + monkeypatch.setattr(service, "_get_agent", lambda **kwargs: agent) + monkeypatch.setattr(service, "_get_version", lambda **kwargs: version) + + restored = service.restore_agent_version( + tenant_id="tenant-1", + agent_id="agent-1", + version_id="version-2", + account_id="account-1", + ) + + assert restored == {"result": "success", "active_config_snapshot_id": "version-2"} + assert agent.active_config_snapshot_id == "version-2" + assert agent.active_config_has_model is True + assert agent.updated_by == "account-1" + assert fake_session.commits == 1 + revision = fake_session.added[0] + assert revision.tenant_id == "tenant-1" + assert revision.agent_id == "agent-1" + assert revision.previous_snapshot_id == "version-4" + assert revision.current_snapshot_id == "version-2" + assert revision.revision == 7 + assert revision.operation == AgentConfigRevisionOperation.RESTORE_VERSION + assert revision.created_by == "account-1" + + +def test_restore_roster_agent_version_rejects_invisible_versions(monkeypatch: pytest.MonkeyPatch): + fake_session = FakeSession(scalar=[None]) + service = AgentRosterService(fake_session) + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + name="Analyst", + description="old", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id="version-4", + ) + + monkeypatch.setattr(service, "_get_agent", lambda **kwargs: agent) + + with pytest.raises(roster_service.AgentVersionNotFoundError): + service.restore_agent_version( + tenant_id="tenant-1", + agent_id="agent-1", + version_id="version-2", + account_id="account-1", + ) + + assert agent.active_config_snapshot_id == "version-4" + assert fake_session.added == [] + assert fake_session.commits == 0 + + def test_app_list_all_excludes_agent_apps_by_default(): filters = AppService._build_app_list_filters( "account-1", "tenant-1", AppListParams(mode="all"), FakeSession(scalar=None, scalars=None) diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index 597649d21c2..3c9e9187cb2 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -90,6 +90,8 @@ import { zPostAgentByAgentIdSkillsUploadBody, zPostAgentByAgentIdSkillsUploadPath, zPostAgentByAgentIdSkillsUploadResponse, + zPostAgentByAgentIdVersionsByVersionIdRestorePath, + zPostAgentByAgentIdVersionsByVersionIdRestoreResponse, zPostAgentResponse, zPutAgentByAgentIdBody, zPutAgentByAgentIdComposerBody, @@ -738,6 +740,21 @@ export const statistics = { summary, } +export const post10 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postAgentByAgentIdVersionsByVersionIdRestore', + path: '/agent/{agent_id}/versions/{version_id}/restore', + tags: ['console'], + }) + .input(z.object({ params: zPostAgentByAgentIdVersionsByVersionIdRestorePath })) + .output(zPostAgentByAgentIdVersionsByVersionIdRestoreResponse) + +export const restore = { + post: post10, +} + export const get19 = oc .route({ inputStructure: 'detailed', @@ -751,6 +768,7 @@ export const get19 = oc export const byVersionId = { get: get19, + restore, } export const get20 = oc @@ -835,7 +853,7 @@ export const get22 = oc .input(z.object({ query: zGetAgentQuery.optional() })) .output(zGetAgentResponse) -export const post10 = oc +export const post11 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -849,7 +867,7 @@ export const post10 = oc export const agent = { get: get22, - post: post10, + post: post11, inviteOptions, byAgentId, } diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 4192dfbaf9e..7b82989af89 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -315,6 +315,11 @@ export type AgentConfigSnapshotDetailResponse = { version_note?: string | null } +export type AgentConfigSnapshotRestoreResponse = { + active_config_snapshot_id: string + result: 'success' +} + export type AgentAppPartial = { access_mode?: string | null active_config_is_published?: boolean @@ -1176,6 +1181,7 @@ export type AgentUserSatisfactionRateStatisticResponse = { export type AgentConfigRevisionOperation = | 'create_version' + | 'restore_version' | 'save_current_version' | 'save_new_agent' | 'save_new_version' @@ -2274,3 +2280,20 @@ export type GetAgentByAgentIdVersionsByVersionIdResponses = { export type GetAgentByAgentIdVersionsByVersionIdResponse = GetAgentByAgentIdVersionsByVersionIdResponses[keyof GetAgentByAgentIdVersionsByVersionIdResponses] + +export type PostAgentByAgentIdVersionsByVersionIdRestoreData = { + body?: never + path: { + agent_id: string + version_id: string + } + query?: never + url: '/agent/{agent_id}/versions/{version_id}/restore' +} + +export type PostAgentByAgentIdVersionsByVersionIdRestoreResponses = { + 200: AgentConfigSnapshotRestoreResponse +} + +export type PostAgentByAgentIdVersionsByVersionIdRestoreResponse + = PostAgentByAgentIdVersionsByVersionIdRestoreResponses[keyof PostAgentByAgentIdVersionsByVersionIdRestoreResponses] diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 5dca172d9ea..ec9f5b0107b 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -78,6 +78,14 @@ export const zAgentSandboxUploadPayload = z.object({ path: z.string().min(1), }) +/** + * AgentConfigSnapshotRestoreResponse + */ +export const zAgentConfigSnapshotRestoreResponse = z.object({ + active_config_snapshot_id: z.string(), + result: z.literal('success'), +}) + /** * IconType */ @@ -1183,6 +1191,7 @@ export const zAgentStatisticSummaryEnvelopeResponse = z.object({ */ export const zAgentConfigRevisionOperation = z.enum([ 'create_version', + 'restore_version', 'save_current_version', 'save_new_agent', 'save_new_version', @@ -2616,3 +2625,14 @@ export const zGetAgentByAgentIdVersionsByVersionIdPath = z.object({ * Agent version detail */ export const zGetAgentByAgentIdVersionsByVersionIdResponse = zAgentConfigSnapshotDetailResponse + +export const zPostAgentByAgentIdVersionsByVersionIdRestorePath = z.object({ + agent_id: z.uuid(), + version_id: z.uuid(), +}) + +/** + * Agent version restored + */ +export const zPostAgentByAgentIdVersionsByVersionIdRestoreResponse + = zAgentConfigSnapshotRestoreResponse From 762e7f7e8aa374849b64c9dd131082ee5063adb3 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:42:51 -0700 Subject: [PATCH 10/35] fix(cli): align run app --conversation mode list with runtime gate (#37733) --- .../commands/run/app/_strategies/streaming-structured.ts | 4 +--- cli/src/commands/run/app/guide.ts | 6 +++--- cli/src/commands/run/app/handlers.ts | 6 ++++++ cli/src/commands/run/app/index.ts | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cli/src/commands/run/app/_strategies/streaming-structured.ts b/cli/src/commands/run/app/_strategies/streaming-structured.ts index c6f02292528..b6fedae2c41 100644 --- a/cli/src/commands/run/app/_strategies/streaming-structured.ts +++ b/cli/src/commands/run/app/_strategies/streaming-structured.ts @@ -1,7 +1,7 @@ import type { RunContext, RunStrategy } from './index' import type { SseEvent } from '@/http/sse' import { buildRunBody } from '@/api/app-run' -import { chatConversationHint, newAppRunObject, RUN_MODES } from '@/commands/run/app/handlers' +import { CHAT_MODES, chatConversationHint, newAppRunObject } from '@/commands/run/app/handlers' import { renderHitlHint, renderHitlOutput } from '@/commands/run/app/hitl-render' import { collect, HitlPauseError } from '@/commands/run/app/sse-collector' import { formatted, stringifyOutput } from '@/framework/output' @@ -10,8 +10,6 @@ import { colorEnabled, colorScheme } from '@/sys/io/color' import { startSpinner } from '@/sys/io/spinner' import { extractThinkBlocks, stripThinkBlocks } from '@/sys/io/think-filter' -const CHAT_MODES: ReadonlySet = new Set([RUN_MODES.Chat, RUN_MODES.AgentChat, RUN_MODES.AdvancedChat]) - async function* captureTaskId( iter: AsyncIterable, onCapture: (id: string) => void, diff --git a/cli/src/commands/run/app/guide.ts b/cli/src/commands/run/app/guide.ts index 2c58e01edd9..66403c0cbf1 100644 --- a/cli/src/commands/run/app/guide.ts +++ b/cli/src/commands/run/app/guide.ts @@ -9,12 +9,12 @@ WORKFLOW difyctl run app --inputs '{"key":"value"}' -o json APP MODES - chat / advanced-chat Conversational. Accepts --conversation to - resume an existing thread. + chat / agent-chat / Conversational. Accept --conversation to + advanced-chat resume an existing thread. agent-chat adds + autonomous tool use. completion Single-turn. Ignores --conversation. workflow Multi-step graph. Pass all input variables as a JSON object via --inputs. - agent-chat Conversational with autonomous tool use. HITL PAUSE (exit code 0 — success-with-pending) When a workflow pauses for human input, stdout receives a JSON object diff --git a/cli/src/commands/run/app/handlers.ts b/cli/src/commands/run/app/handlers.ts index 3d3d75ec082..9536c4fe2a9 100644 --- a/cli/src/commands/run/app/handlers.ts +++ b/cli/src/commands/run/app/handlers.ts @@ -11,6 +11,12 @@ export const RUN_MODES = { export type RunMode = typeof RUN_MODES[keyof typeof RUN_MODES] +export const CHAT_MODES: ReadonlySet = new Set([ + RUN_MODES.Chat, + RUN_MODES.AgentChat, + RUN_MODES.AdvancedChat, +]) + export type AppRunObject = FormattedPrintable export function newAppRunObject(mode: string, resp: Record): AppRunObject { diff --git a/cli/src/commands/run/app/index.ts b/cli/src/commands/run/app/index.ts index 44ea93c542b..815d708d9d8 100644 --- a/cli/src/commands/run/app/index.ts +++ b/cli/src/commands/run/app/index.ts @@ -4,6 +4,7 @@ import { httpRetryFlag } from '@/commands/_shared/global-flags' import { Args, Flags } from '@/framework/flags' import { OutputFormat } from '@/framework/output' import { agentGuide } from './guide' +import { CHAT_MODES } from './handlers' import { runApp } from './run' export default class RunApp extends DifyCommand { @@ -30,7 +31,7 @@ export default class RunApp extends DifyCommand { 'inputs': Flags.string({ description: 'Input variables as a JSON object, e.g. --inputs \'{"key":"value"}\'. Mutually exclusive with --inputs-file.' }), 'inputs-file': Flags.string({ description: 'Path to a JSON file containing the inputs object. Mutually exclusive with --inputs.' }), 'file': Flags.stringArray({ description: 'Named file input: --file key=@path for a local file or --file key=https://url for a remote URL. Repeatable.', default: [] }), - 'conversation': Flags.string({ description: 'Resume a chat conversation by id (chat/advanced-chat only)' }), + 'conversation': Flags.string({ description: `Resume a chat conversation by id (${[...CHAT_MODES].join('/')} only)` }), 'workflow-id': Flags.string({ description: 'Pin to a specific published workflow version' }), 'workspace': Flags.string({ description: 'Workspace id (overrides DIFY_WORKSPACE_ID and stored default)' }), 'stream': Flags.boolean({ description: 'Print output live as tokens/events arrive (default: collect and print at end)', default: false }), From 47ee9f7435355b7138430ca7105738457954e93d Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:45:03 +0800 Subject: [PATCH 11/35] fix: bound OperationService billing requests (#37425) Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Co-authored-by: QuantumGhost --- api/services/operation_service.py | 6 +++++- api/tests/unit_tests/services/test_operation_service.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/services/operation_service.py b/api/services/operation_service.py index 903efd26ae7..6869cf23ea2 100644 --- a/api/services/operation_service.py +++ b/api/services/operation_service.py @@ -3,6 +3,8 @@ from typing import TypedDict import httpx +OPERATION_REQUEST_TIMEOUT = httpx.Timeout(10.0, connect=3.0) + class UtmInfo(TypedDict, total=False): """Expected shape of the utm_info dict passed to record_utm. @@ -26,7 +28,9 @@ class OperationService: headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key} url = f"{cls.base_url}{endpoint}" - response = httpx.request(method, url, json=json, params=params, headers=headers) + response = httpx.request( + method, url, json=json, params=params, headers=headers, timeout=OPERATION_REQUEST_TIMEOUT + ) return response.json() diff --git a/api/tests/unit_tests/services/test_operation_service.py b/api/tests/unit_tests/services/test_operation_service.py index e43a7fa649e..dffded658eb 100644 --- a/api/tests/unit_tests/services/test_operation_service.py +++ b/api/tests/unit_tests/services/test_operation_service.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import httpx import pytest -from services.operation_service import OperationService +from services.operation_service import OPERATION_REQUEST_TIMEOUT, OperationService class TestOperationService: @@ -44,6 +44,7 @@ class TestOperationService: assert kwargs["json"] == json_data assert kwargs["headers"]["Billing-Api-Secret-Key"] == "s3cr3t" assert kwargs["headers"]["Content-Type"] == "application/json" + assert kwargs["timeout"] == OPERATION_REQUEST_TIMEOUT @patch("httpx.request") def test_should_propagate_httpx_error_when__send_request_raises( From c83dcce1f7fd12883270a69b55d9afd01628dbbc Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Mon, 22 Jun 2026 14:57:56 +0800 Subject: [PATCH 12/35] chore: remove duplicate code (#37724) --- api/controllers/console/workspace/rbac.py | 36 +------- api/services/enterprise/rbac_service.py | 82 ++++++++++++++++--- .../console/workspace/test_rbac.py | 21 +---- .../services/enterprise/test_rbac_service.py | 37 ++++++++- 4 files changed, 109 insertions(+), 67 deletions(-) diff --git a/api/controllers/console/workspace/rbac.py b/api/controllers/console/workspace/rbac.py index 1b213a4f741..f672833061a 100644 --- a/api/controllers/console/workspace/rbac.py +++ b/api/controllers/console/workspace/rbac.py @@ -211,7 +211,7 @@ def _legacy_workspace_roles( name=role_name, description="", is_builtin=True, - permission_keys=list(_LEGACY_ROLE_PERMISSION_KEYS[role_name]), + permission_keys=list(dict.fromkeys(_LEGACY_ROLE_PERMISSION_KEYS[role_name])), role_tag="owner" if role_name == "owner" else "", ) for role_name in ("owner", "admin", "editor", "normal", "dataset_operator") @@ -244,11 +244,6 @@ def _legacy_workspace_roles( ) -# --------------------------------------------------------------------------- -# Permission catalogs. -# --------------------------------------------------------------------------- - - @console_ns.route("/workspaces/current/rbac/role-permissions/catalog") class RBACWorkspaceCatalogApi(Resource): @login_required @@ -375,30 +370,6 @@ class RBACRoleCopyApi(Resource): return _dump(role), 201 -@console_ns.route("/workspaces/current/rbac/roles//members") -class RBACRoleMembersApi(Resource): - @login_required - @rbac_permission_required( - RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False - ) - @console_ns.response(200, "Success", console_ns.models[_RBACRoleAccountList.__name__]) - def get(self, role_id): - tenant_id, account_id = _current_ids() - return _dump( - svc.RBACService.Roles.members( - tenant_id, - account_id, - str(role_id), - options=_pagination_options(), - ) - ) - - -# --------------------------------------------------------------------------- -# Access policies (tenant-level permission sets). -# --------------------------------------------------------------------------- - - class _AccessPolicyCreateRequest(BaseModel): name: str resource_type: svc.RBACResourceType @@ -788,11 +759,6 @@ class RBACDatasetMemberBindingsApi(Resource): return {"result": "success"} -# --------------------------------------------------------------------------- -# Workspace-level access (Settings > Access Rules). -# --------------------------------------------------------------------------- - - @console_ns.route("/workspaces/current/rbac/workspace/apps/access-policy") class RBACWorkspaceAppMatrixApi(Resource): @login_required diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index c32a5759105..32cf69ce99e 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -313,12 +313,20 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [ "plugin.debug", "credential.use", "credential.manage", + "billing.view", + "billing.subscription.manage", + "billing.manage", + "app.acl.preview", "app_library.access", "app.create_and_management", "app.tag.manage", + "dataset.acl.preview", "dataset.create_and_management", "dataset.tag.manage", "dataset.external.connect", + "dataset.api_key.manage", + "snippets.create_and_modify", + "snippets.management", "tool.manage", "mcp.manage", "snippets.create_and_modify", @@ -337,12 +345,18 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ "plugin.debug", "credential.use", "credential.manage", + "billing.view", + "billing.subscription.manage", + "billing.manage", "app_library.access", "app.create_and_management", "app.tag.manage", "dataset.create_and_management", "dataset.tag.manage", "dataset.external.connect", + "dataset.api_key.manage", + "snippets.create_and_modify", + "snippets.management", "tool.manage", "mcp.manage", "snippets.create_and_modify", @@ -360,6 +374,7 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ "dataset.create_and_management", "dataset.tag.manage", "dataset.external.connect", + "snippets.create_and_modify", "tool.manage", "snippets.create_and_modify", ] @@ -378,6 +393,7 @@ _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [ ] _LEGACY_APP_OWNER_KEYS: list[str] = [ + "app.acl.preview", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.edit", @@ -389,6 +405,7 @@ _LEGACY_APP_OWNER_KEYS: list[str] = [ ] _LEGACY_APP_ADMIN_KEYS: list[str] = [ + "app.acl.preview", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.edit", @@ -400,6 +417,7 @@ _LEGACY_APP_ADMIN_KEYS: list[str] = [ ] _LEGACY_APP_EDITOR_KEYS: list[str] = [ + "app.acl.preview", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.edit", @@ -411,12 +429,14 @@ _LEGACY_APP_EDITOR_KEYS: list[str] = [ ] _LEGACY_APP_NORMAL_KEYS: list[str] = [ + "app.acl.preview", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.monitor", ] _LEGACY_DATASET_OWNER_KEYS: list[str] = [ + "dataset.acl.preview", "dataset.acl.readonly", "dataset.acl.edit", "dataset.acl.import_export_dsl", @@ -432,6 +452,7 @@ _LEGACY_DATASET_OWNER_KEYS: list[str] = [ ] _LEGACY_DATASET_ADMIN_KEYS: list[str] = [ + "dataset.acl.preview", "dataset.acl.readonly", "dataset.acl.edit", "dataset.acl.import_export_dsl", @@ -447,6 +468,7 @@ _LEGACY_DATASET_ADMIN_KEYS: list[str] = [ ] _LEGACY_DATASET_EDITOR_KEYS: list[str] = [ + "dataset.acl.preview", "dataset.acl.readonly", "dataset.acl.edit", "dataset.acl.import_export_dsl", @@ -497,6 +519,19 @@ _LEGACY_MY_PERMISSIONS: dict[TenantAccountRole, dict[str, list[str]]] = { } +def _legacy_role_permission_keys(role: TenantAccountRole) -> list[str]: + permissions = _LEGACY_MY_PERMISSIONS.get(role, {}) + return list( + dict.fromkeys( + [ + *permissions.get("workspace", []), + *permissions.get("app", []), + *permissions.get("dataset", []), + ] + ) + ) + + def _legacy_my_permissions(tenant_id: str, account_id: str | None) -> MyPermissionsResponse: if not account_id: return MyPermissionsResponse() @@ -1523,21 +1558,44 @@ class RBACService: ) return AccessMatrixItem.model_validate(data or {}) - # ------------------------------------------------------------------ - # Member ↔ role bindings (screenshot 3: Settings > Members > Assign roles). - # ------------------------------------------------------------------ class MemberRoles: @staticmethod def get(tenant_id: str, account_id: str | None, member_account_id: str) -> MemberRolesResponse: - data = _inner_call( - "GET", - f"{_INNER_PREFIX}/members/rbac-roles", - tenant_id=tenant_id, - account_id=account_id, - params={"account_id": member_account_id}, - ) - rst = MemberRolesResponse.model_validate(data or {}) - return rst + if dify_config.RBAC_ENABLED: + data = _inner_call( + "GET", + f"{_INNER_PREFIX}/members/rbac-roles", + tenant_id=tenant_id, + account_id=account_id, + params={"account_id": member_account_id}, + ) + rst = MemberRolesResponse.model_validate(data or {}) + return rst + else: + with session_factory.create_session() as session: + role = session.scalar( + select(TenantAccountJoin.role).where( + TenantAccountJoin.tenant_id == tenant_id, + TenantAccountJoin.account_id == member_account_id, + ) + ) + return MemberRolesResponse( + account_id=member_account_id, + roles=[ + RBACRole( + id="", + name=role, + description="", + is_builtin=True, + type="", + permission_keys=_legacy_role_permission_keys(role), + role_tag="owner" if role == "owner" else role, + tenant_id=tenant_id, + ) + ] + if role + else [], + ) @staticmethod def batch_get( diff --git a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py index 1ad9637b7bb..d78bc1fc6dd 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_rbac.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_rbac.py @@ -185,7 +185,7 @@ class TestPaginationMapping: "name": "owner", "description": "", "is_builtin": True, - "permission_keys": list(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"]), + "permission_keys": list(dict.fromkeys(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"])), "role_tag": "owner", }, { @@ -196,7 +196,7 @@ class TestPaginationMapping: "name": "admin", "description": "", "is_builtin": True, - "permission_keys": list(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["admin"]), + "permission_keys": list(dict.fromkeys(rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["admin"])), "role_tag": "", }, ] @@ -336,23 +336,6 @@ class TestResourceAccessScopeBindings: class TestPaginationForwarding: - def test_role_members_get_forwards_outer_pagination_params(self, app): - with ( - app.test_request_context("/workspaces/current/rbac/roles/role-1/members?page=2&limit=50&reverse=true"), - patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")), - patch("controllers.console.workspace.rbac.svc.RBACService.Roles.members") as mock_members, - patch("controllers.console.workspace.rbac._dump", return_value={}), - ): - inspect.unwrap(rbac_mod.RBACRoleMembersApi.get)(rbac_mod.RBACRoleMembersApi(), "role-1") - - _, _, role_id = mock_members.call_args.args - _, kwargs = mock_members.call_args - assert role_id == "role-1" - options = kwargs["options"] - assert options.page_number == 2 - assert options.results_per_page == 50 - assert options.reverse is True - def test_access_policies_get_forwards_outer_pagination_params(self, app): with ( app.test_request_context( diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index dfd5662cf3e..35f0d3ac674 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -620,6 +620,13 @@ class TestMyPermissions: assert out.dataset.default_permission_keys == dataset_keys assert out.app.overrides == [] assert out.dataset.overrides == [] + if role == "owner": + assert "billing.view" in out.workspace.permission_keys + assert "snippets.management" in out.workspace.permission_keys + assert "app.acl.preview" in out.workspace.permission_keys + assert "dataset.acl.preview" in out.workspace.permission_keys + assert "app.acl.preview" in out.app.default_permission_keys + assert "dataset.acl.preview" in out.dataset.default_permission_keys @pytest.mark.parametrize( ("role", "expected_snippet_keys"), @@ -700,7 +707,8 @@ class TestMemberRoles: } ], } - out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2") + with patch(f"{MODULE}.dify_config.RBAC_ENABLED", True): + out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2") call = _call_args(mock_send) assert call.method == "GET" assert call.endpoint == "/rbac/members/rbac-roles" @@ -708,6 +716,33 @@ class TestMemberRoles: assert out.account_id == "acct-2" assert out.roles[0].name == "Member" + def test_get_legacy_role_includes_permission_keys(self, mock_send: MagicMock): + session = MagicMock() + session.scalar.return_value = svc.TenantAccountRole.EDITOR + + with ( + patch(f"{MODULE}.dify_config.RBAC_ENABLED", False), + patch(f"{MODULE}.session_factory.create_session") as create_session, + ): + create_session.return_value.__enter__.return_value = session + out = svc.RBACService.MemberRoles.get("tenant-1", "acct-1", "acct-2") + + mock_send.assert_not_called() + assert out.account_id == "acct-2" + assert out.roles[0].name == "editor" + assert out.roles[0].permission_keys == list( + dict.fromkeys( + [ + *svc._LEGACY_WORKSPACE_EDITOR_KEYS, + *svc._LEGACY_APP_EDITOR_KEYS, + *svc._LEGACY_DATASET_EDITOR_KEYS, + ] + ) + ) + assert "snippets.create_and_modify" in out.roles[0].permission_keys + assert "app.acl.preview" in out.roles[0].permission_keys + assert "dataset.acl.preview" in out.roles[0].permission_keys + def test_replace(self, mock_send: MagicMock): mock_send.return_value = {"account_id": "acct-2", "roles": []} svc.RBACService.MemberRoles.replace( From 8f6b57fe24149749d4b856a5a6d2949993717dd7 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:30:28 +0800 Subject: [PATCH 13/35] fix: add RBAC feature across various components (#37732) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 16 +- .../app-sidebar/dataset-info-flow.test.tsx | 3 +- .../[appId]/__tests__/layout-main.spec.tsx | 29 +- .../(appDetailLayout)/[appId]/layout-main.tsx | 8 +- .../__tests__/layout-main.spec.tsx | 42 +- .../[datasetId]/layout-main.tsx | 7 +- .../__tests__/app-detail-section.spec.tsx | 23 +- .../__tests__/dataset-detail-section.spec.tsx | 22 +- .../app-sidebar/app-detail-section.tsx | 7 +- .../__tests__/use-app-info-actions.spec.ts | 4 + .../app-info/use-app-info-actions.ts | 9 +- .../app-sidebar/dataset-detail-section.tsx | 7 +- .../__tests__/dropdown-callbacks.spec.tsx | 18 +- .../dataset-info/__tests__/index.spec.tsx | 37 +- .../app-sidebar/dataset-info/dropdown.tsx | 7 +- .../access-config/__tests__/index.spec.tsx | 22 +- .../components/app/access-config/index.tsx | 7 +- .../app-list/__tests__/index.spec.tsx | 5 +- .../app/create-app-dialog/app-list/index.tsx | 6 +- .../create-app-modal/__tests__/index.spec.tsx | 4 +- .../components/app/create-app-modal/index.tsx | 7 +- .../__tests__/index.spec.tsx | 3 +- .../app/create-from-dsl-modal/index.tsx | 8 +- .../app/log/__tests__/empty-element.spec.tsx | 3 +- web/app/components/app/log/empty-element.tsx | 5 + .../switch-app-modal/__tests__/index.spec.tsx | 3 +- .../components/app/switch-app-modal/index.tsx | 5 + .../app/workflow-log/__tests__/index.spec.tsx | 19 +- .../apps/__tests__/app-card.spec.tsx | 103 +++- web/app/components/apps/app-card.tsx | 554 ++++++++++-------- web/app/components/apps/starred-app-card.tsx | 94 ++- .../access-config/__tests__/index.spec.tsx | 22 +- .../datasets/access-config/index.tsx | 7 +- .../dataset-card/__tests__/index.spec.tsx | 82 +++ .../__tests__/operations-dropdown.spec.tsx | 36 +- .../components/operations-dropdown.tsx | 7 +- .../datasets/list/dataset-card/index.tsx | 62 +- .../components/explore/continue-work/item.tsx | 5 + .../account-setting/__tests__/index.spec.tsx | 23 + .../header/account-setting/index.tsx | 16 +- .../workflow/workflow-generator/index.tsx | 8 +- web/hooks/use-import-dsl.ts | 12 +- web/i18n/ar-TN/app.json | 1 + web/i18n/de-DE/app.json | 1 + web/i18n/en-US/app.json | 1 + web/i18n/es-ES/app.json | 1 + web/i18n/fa-IR/app.json | 1 + web/i18n/fr-FR/app.json | 1 + web/i18n/hi-IN/app.json | 1 + web/i18n/id-ID/app.json | 1 + web/i18n/it-IT/app.json | 1 + web/i18n/ja-JP/app.json | 1 + web/i18n/ko-KR/app.json | 1 + web/i18n/nl-NL/app.json | 1 + web/i18n/pl-PL/app.json | 1 + web/i18n/pt-BR/app.json | 1 + web/i18n/ro-RO/app.json | 1 + web/i18n/ru-RU/app.json | 1 + web/i18n/sl-SI/app.json | 1 + web/i18n/th-TH/app.json | 1 + web/i18n/tr-TR/app.json | 1 + web/i18n/uk-UA/app.json | 1 + web/i18n/vi-VN/app.json | 1 + web/i18n/zh-Hans/app.json | 1 + web/i18n/zh-Hant/app.json | 1 + web/utils/app-redirection.spec.ts | 8 +- web/utils/permission.spec.ts | 46 ++ web/utils/permission.ts | 15 +- 68 files changed, 1099 insertions(+), 360 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 7e4521f51cb..60ed887058a 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -986,6 +986,9 @@ "jsx-a11y/click-events-have-key-events": { "count": 1 }, + "jsx-a11y/no-noninteractive-element-to-interactive-role": { + "count": 1 + }, "jsx-a11y/no-static-element-interactions": { "count": 1 } @@ -1003,6 +1006,11 @@ "count": 3 } }, + "web/app/components/apps/starred-app-card.tsx": { + "jsx-a11y/no-noninteractive-element-to-interactive-role": { + "count": 1 + } + }, "web/app/components/base/action-button/index.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -3364,14 +3372,6 @@ "count": 1 } }, - "web/app/components/datasets/list/dataset-card/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/datasets/list/dataset-card/operation-item.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 diff --git a/web/__tests__/app-sidebar/dataset-info-flow.test.tsx b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx index bff6f5a29c5..62761743a10 100644 --- a/web/__tests__/app-sidebar/dataset-info-flow.test.tsx +++ b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx @@ -1,6 +1,7 @@ import type { DataSet } from '@/models/datasets' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import DatasetInfo from '@/app/components/app-sidebar/dataset-info' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { RETRIEVE_METHOD } from '@/types/app' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx index f9742f35e13..ec7aad05b74 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx @@ -1,5 +1,6 @@ import type { App } from '@/types/app' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { useStore } from '@/app/components/app/store' import { usePathname, useRouter } from '@/next/navigation' import { fetchAppDetailDirect } from '@/service/apps' @@ -10,6 +11,13 @@ import AppDetailLayout from '../layout-main' const mockReplace = vi.fn() let mockPathname = '/app/app-1/workflow' let mockIsLoadingWorkspacePermissionKeys = false +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) vi.mock('@/next/navigation', () => ({ usePathname: vi.fn(), @@ -57,6 +65,7 @@ describe('AppDetailLayout', () => { vi.clearAllMocks() mockPathname = '/app/app-1/workflow' mockIsLoadingWorkspacePermissionKeys = false + mockIsRbacEnabled = true mockUsePathname.mockImplementation(() => mockPathname) mockUseRouter.mockReturnValue({ back: vi.fn(), @@ -262,6 +271,24 @@ describe('AppDetailLayout', () => { expect(useStore.getState().appDetail?.id).toBe('app-1') }) + it('should redirect access config pages when RBAC is disabled', async () => { + mockIsRbacEnabled = false + mockPathname = '/app/app-1/access-config' + mockFetchAppDetailDirect.mockResolvedValue(createAppDetail({ permission_keys: [AppACLPermission.AccessConfig] })) + + render( + +
App page content
+
, + ) + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/app/app-1/develop') + }) + expect(screen.queryByText('App page content')).not.toBeInTheDocument() + expect(useStore.getState().appDetail).toBeUndefined() + }) + it('should redirect annotation pages when edit access is missing', async () => { mockPathname = '/app/app-1/annotations' mockFetchAppDetailDirect.mockResolvedValue(createAppDetail({ diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index dcbcba4116d..517c8819e91 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { App } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' +import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -9,6 +10,7 @@ import { useShallow } from 'zustand/react/shallow' import { useStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' import { useAppContext } from '@/context/app-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' import { usePathname, useRouter } from '@/next/navigation' import { fetchAppDetailDirect } from '@/service/apps' @@ -36,7 +38,9 @@ const AppDetailLayout: FC = (props) => { const { t } = useTranslation() const router = useRouter() const pathname = usePathname() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { isLoadingCurrentWorkspace, isLoadingWorkspacePermissionKeys, currentWorkspace, userProfile, workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const { appDetail, setAppDetail } = useStore(useShallow(state => ({ appDetail: state.appDetail, setAppDetail: state.setAppDetail, @@ -95,6 +99,7 @@ const AppDetailLayout: FC = (props) => { currentUserId: userProfile?.id, resourceMaintainer: routeAppDetail.maintainer, workspacePermissionKeys, + isRbacEnabled, }) const isLayoutPath = pathname.endsWith('configuration') || pathname.endsWith('workflow') const isLogsPath = pathname.endsWith('logs') @@ -112,6 +117,7 @@ const AppDetailLayout: FC = (props) => { currentUserId: userProfile?.id, resourceMaintainer: routeAppDetail.maintainer, workspacePermissionKeys, + isRbacEnabled, })) return } @@ -125,7 +131,7 @@ const AppDetailLayout: FC = (props) => { if (appDetailRes && appDetail?.id !== appDetailRes.id) setAppDetail({ ...appDetailRes, enable_sso: false }) - }, [appDetail?.id, appDetailRes, appId, currentWorkspace.id, isLoadingAppDetail, isLoadingCurrentWorkspace, isLoadingWorkspacePermissionKeys, pathname, routeAppDetail, router, setAppDetail, userProfile?.id, workspacePermissionKeys]) + }, [appDetail?.id, appDetailRes, appId, currentWorkspace.id, isLoadingAppDetail, isLoadingCurrentWorkspace, isLoadingWorkspacePermissionKeys, isRbacEnabled, pathname, routeAppDetail, router, setAppDetail, userProfile?.id, workspacePermissionKeys]) if (!appDetail) { return ( diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx index 23a9672a220..d669a675dac 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx @@ -1,10 +1,18 @@ -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { usePathname, useRouter } from '@/next/navigation' import { useDatasetDetail } from '@/service/knowledge/use-dataset' import { DatasetACLPermission } from '@/utils/permission' import DatasetDetailLayout from '../layout-main' const mockReplace = vi.fn() +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) vi.mock('@/next/navigation', () => ({ usePathname: vi.fn(), @@ -42,6 +50,7 @@ const mockUseDatasetDetail = vi.mocked(useDatasetDetail) describe('DatasetDetailLayout', () => { beforeEach(() => { vi.clearAllMocks() + mockIsRbacEnabled = true mockUsePathname.mockReturnValue('/datasets/dataset-1/documents') mockUseRouter.mockReturnValue({ back: vi.fn(), @@ -292,5 +301,36 @@ describe('DatasetDetailLayout', () => { expect(screen.getByText('Access config content')).toBeInTheDocument() expect(mockReplace).not.toHaveBeenCalled() }) + + it('should redirect from access config when RBAC is disabled', async () => { + // Arrange + mockIsRbacEnabled = false + mockUsePathname.mockReturnValue('/datasets/dataset-1/access-config') + mockUseDatasetDetail.mockReturnValue({ + data: { + id: 'dataset-1', + name: 'Dataset 1', + provider: 'vendor', + runtime_mode: 'general', + is_published: true, + permission_keys: [DatasetACLPermission.AccessConfig], + }, + error: null, + refetch: vi.fn(), + } as unknown as ReturnType) + + // Act + render( + +
Access config content
+
, + ) + + // Assert + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/datasets/dataset-1/documents') + }) + expect(screen.queryByText('Access config content')).not.toBeInTheDocument() + }) }) }) diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 3791009061d..a7df61c6c57 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -2,12 +2,14 @@ import type { FC } from 'react' import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' +import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { useAppContext } from '@/context/app-context' import DatasetDetailContext from '@/context/dataset-detail' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' import { usePathname, useRouter } from '@/next/navigation' import { useDatasetDetail } from '@/service/knowledge/use-dataset' @@ -56,12 +58,14 @@ const DatasetDetailLayout: FC = (props) => { const { t } = useTranslation() const router = useRouter() const pathname = usePathname() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { isLoadingCurrentWorkspace, isLoadingWorkspacePermissionKeys, userProfile, workspacePermissionKeys, } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const { data: datasetRes, error, refetch: mutateDatasetRes } = useDatasetDetail(datasetId) const shouldRedirect = shouldRedirectToDatasetList(error) @@ -69,7 +73,8 @@ const DatasetDetailLayout: FC = (props) => { currentUserId: userProfile?.id, resourceMaintainer: datasetRes?.maintainer, workspacePermissionKeys, - }), [datasetRes?.maintainer, datasetRes?.permission_keys, userProfile?.id, workspacePermissionKeys]) + isRbacEnabled, + }), [datasetRes?.maintainer, datasetRes?.permission_keys, isRbacEnabled, userProfile?.id, workspacePermissionKeys]) const isAccessConfigPath = pathname.endsWith('/access-config') const isHitTestingPath = pathname.endsWith('/hitTesting') const isPermissionControlledPath = isAccessConfigPath || isHitTestingPath diff --git a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx index 6dc9f5dfb19..8d83f6c7711 100644 --- a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { AppACLPermission } from '@/utils/permission' import AppDetailSection from '../app-detail-section' import { useAppInfoActions } from '../app-info/use-app-info-actions' @@ -6,6 +7,13 @@ import { useAppInfoActions } from '../app-info/use-app-info-actions' let mockAppMode = 'chat' let mockPathname = '/app/app-1/logs' let mockAppPermissionKeys: string[] = [] +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) vi.mock('@/app/components/app/store', () => ({ useStore: (selector: (state: Record) => unknown) => selector({ @@ -56,6 +64,7 @@ describe('AppDetailSection', () => { mockAppMode = 'chat' mockPathname = '/app/app-1/logs' mockAppPermissionKeys = [AppACLPermission.Monitor] + mockIsRbacEnabled = true }) // Rendering behavior for app detail navigation entries. @@ -203,6 +212,18 @@ describe('AppDetailSection', () => { expect(screen.queryByRole('link', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() }) + it('should hide resource access navigation when RBAC is disabled', () => { + // Arrange + mockIsRbacEnabled = false + mockAppPermissionKeys = [AppACLPermission.AccessConfig] + + // Act + render() + + // Assert + expect(screen.queryByRole('link', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() + }) + it('should pass collapsed mode to app info and navigation links when collapsed', () => { // Act render() diff --git a/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx b/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx index 25763fb11ae..2b19ee5043e 100644 --- a/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx @@ -1,11 +1,19 @@ import type { DataSet, RelatedAppResponse } from '@/models/datasets' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { DatasetACLPermission } from '@/utils/permission' import DatasetDetailSection from '../dataset-detail-section' let mockPathname = '/datasets/dataset-1/documents' let mockDataset: DataSet | undefined let mockRelatedApps: RelatedAppResponse | undefined +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) vi.mock('@/next/navigation', () => ({ usePathname: () => mockPathname, @@ -77,6 +85,7 @@ describe('DatasetDetailSection', () => { beforeEach(() => { vi.clearAllMocks() mockPathname = '/datasets/dataset-1/documents' + mockIsRbacEnabled = true mockDataset = createDataset() mockRelatedApps = { data: [], @@ -120,6 +129,17 @@ describe('DatasetDetailSection', () => { expect(screen.queryByRole('link', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() }) + it('should hide resource access navigation when RBAC is disabled', () => { + mockIsRbacEnabled = false + mockDataset = createDataset({ + permission_keys: [DatasetACLPermission.AccessConfig], + }) + + render() + + expect(screen.queryByRole('link', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() + }) + it('should render hit testing navigation as a link when retrieval recall permission is granted', () => { mockDataset = createDataset({ permission_keys: [DatasetACLPermission.RetrievalRecall], diff --git a/web/app/components/app-sidebar/app-detail-section.tsx b/web/app/components/app-sidebar/app-detail-section.tsx index 5df0a4ef573..15d963244cb 100644 --- a/web/app/components/app-sidebar/app-detail-section.tsx +++ b/web/app/components/app-sidebar/app-detail-section.tsx @@ -15,12 +15,14 @@ import { RiTerminalWindowFill, RiTerminalWindowLine, } from '@remixicon/react' +import { useSuspenseQuery } from '@tanstack/react-query' import { Fragment, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useStore } from '@/app/components/app/store' import Divider from '@/app/components/base/divider' import Annotations from '@/app/components/base/icons/src/vender/Annotations' import { useAppContext } from '@/context/app-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { usePathname } from '@/next/navigation' import { AppModeEnum } from '@/types/app' import { getAppACLCapabilities } from '@/utils/permission' @@ -71,7 +73,9 @@ const AppDetailSection = ({ }: AppDetailSectionProps) => { const { t } = useTranslation() const pathname = usePathname() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { userProfile, workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const appDetail = useStore(state => state.appDetail) const appInfoActions = useAppInfoActions({ resetKey: appDetail?.id, @@ -88,6 +92,7 @@ const AppDetailSection = ({ currentUserId: userProfile?.id, resourceMaintainer: appDetail.maintainer, workspacePermissionKeys, + isRbacEnabled, }) return [ @@ -143,7 +148,7 @@ const AppDetailSection = ({ : [] ), ] - }, [appDetail, t, userProfile?.id, workspacePermissionKeys]) + }, [appDetail, t, userProfile?.id, workspacePermissionKeys, isRbacEnabled]) if (!appDetail) return null diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index 898b8c55a74..8f417ee7ce4 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -69,6 +69,10 @@ vi.mock('@/service/use-apps', () => ({ })) vi.mock('@tanstack/react-query', () => ({ + queryOptions: (options: TOptions) => options, + useSuspenseQuery: () => ({ + data: { rbac_enabled: true }, + }), useQueryClient: () => ({ setQueryData: mockSetQueryData, }), diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 89688a4882f..24931418733 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -3,12 +3,13 @@ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-moda import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { toast } from '@langgenius/dify-ui/toast' -import { useQueryClient } from '@tanstack/react-query' +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { useSetNeedRefreshAppList } from '@/app/components/apps/storage' import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps' import { appDetailQueryKeyPrefix, useInvalidateAppList } from '@/service/use-apps' @@ -58,6 +59,8 @@ export function useAppInfoActions({ onDetailExpand, resetKey }: UseAppInfoAction const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) const invalidateAppList = useInvalidateAppList() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const [uiState, setUiState] = useState(() => createInitialUiState(resetKey)) const uiStateMatchesResetKey = uiState.resetKey === resetKey @@ -216,12 +219,12 @@ export function useAppInfoActions({ onDetailExpand, resetKey }: UseAppInfoAction toast(t('newApp.appCreated', { ns: 'app' }), { type: 'success' }) setNeedRefresh('1') onPlanInfoChanged() - getRedirection(newApp, replace) + getRedirection(newApp, replace, { isRbacEnabled }) } catch { toast(t('newApp.appCreateFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, onPlanInfoChanged, replace, setNeedRefresh, t]) + }, [appDetail, closeModal, isRbacEnabled, onPlanInfoChanged, replace, setNeedRefresh, t]) const onExport = useCallback(async (include = false) => { if (!appDetail) diff --git a/web/app/components/app-sidebar/dataset-detail-section.tsx b/web/app/components/app-sidebar/dataset-detail-section.tsx index f337c6b15aa..4c57f9eb51a 100644 --- a/web/app/components/app-sidebar/dataset-detail-section.tsx +++ b/web/app/components/app-sidebar/dataset-detail-section.tsx @@ -12,6 +12,7 @@ import { RiLock2Fill, RiLock2Line, } from '@remixicon/react' +import { useSuspenseQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' @@ -19,6 +20,7 @@ import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vend import ExtraInfo from '@/app/components/datasets/extra-info' import { useAppContext } from '@/context/app-context' import DatasetDetailContext from '@/context/dataset-detail' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { usePathname } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import { getDatasetACLCapabilities } from '@/utils/permission' @@ -40,14 +42,17 @@ const DatasetDetailSection = ({ const { t } = useTranslation() const pathname = usePathname() const datasetId = getDatasetIdFromPathname(pathname) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { userProfile, workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const { data: datasetRes, refetch: mutateDatasetRes } = useDatasetDetail(datasetId ?? '') const { data: relatedApps } = useDatasetRelatedApps(datasetId ?? '', { enabled: !!datasetId && !!datasetRes }) const datasetACLCapabilities = useMemo(() => getDatasetACLCapabilities(datasetRes?.permission_keys, { currentUserId: userProfile?.id, resourceMaintainer: datasetRes?.maintainer, workspacePermissionKeys, - }), [datasetRes?.maintainer, datasetRes?.permission_keys, userProfile?.id, workspacePermissionKeys]) + isRbacEnabled, + }), [datasetRes?.maintainer, datasetRes?.permission_keys, isRbacEnabled, userProfile?.id, workspacePermissionKeys]) const isButtonDisabledWithPipeline = useMemo(() => { if (!datasetRes) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx index e1bfb008a71..6155445befb 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx @@ -1,7 +1,8 @@ import type { DataSet } from '@/models/datasets' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { ChunkingMode, DatasetPermission, @@ -19,6 +20,13 @@ const mockExportPipeline = vi.fn() const mockCheckIsUsedInApp = vi.fn() const mockDeleteDataset = vi.fn() const mockToast = vi.fn() +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) const createDataset = (overrides: Partial = {}): DataSet => ({ id: 'dataset-1', @@ -94,6 +102,13 @@ vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }), })) +vi.mock('@/context/app-context', () => ({ + useSelector: (selector: (state: { userProfile: { id: string }, workspacePermissionKeys: string[] }) => unknown) => selector({ + userProfile: { id: 'user-1' }, + workspacePermissionKeys: [], + }), +})) + vi.mock('@/service/knowledge/use-dataset', () => ({ datasetDetailQueryKeyPrefix: ['dataset', 'detail'], useInvalidDatasetList: () => mockInvalidDatasetList, @@ -142,6 +157,7 @@ vi.mock('@/app/components/datasets/rename-modal', () => ({ describe('Dropdown callback coverage', () => { beforeEach(() => { vi.clearAllMocks() + mockIsRbacEnabled = true mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' }) mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' }) mockCheckIsUsedInApp.mockResolvedValue({ is_using: false }) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx index 63e05435c99..a12e7348675 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx @@ -1,7 +1,8 @@ import type { DataSet } from '@/models/datasets' -import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { createEvent, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { ChunkingMode, DatasetPermission, @@ -23,6 +24,13 @@ const mockExportPipeline = vi.fn() const mockCheckIsUsedInApp = vi.fn() const mockDeleteDataset = vi.fn() const TestEditIcon = () => +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) const createDataset = (overrides: Partial = {}): DataSet => ({ id: 'dataset-1', @@ -107,6 +115,13 @@ vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }), })) +vi.mock('@/context/app-context', () => ({ + useSelector: (selector: (state: { userProfile: { id: string }, workspacePermissionKeys: string[] }) => unknown) => selector({ + userProfile: { id: 'user-1' }, + workspacePermissionKeys: [], + }), +})) + vi.mock('@/service/knowledge/use-dataset', () => ({ datasetDetailQueryKeyPrefix: ['dataset', 'detail'], useInvalidDatasetList: () => mockInvalidDatasetList, @@ -162,6 +177,7 @@ const openMenu = async (user: ReturnType) => { describe('DatasetInfo', () => { beforeEach(() => { vi.clearAllMocks() + mockIsRbacEnabled = true mockDataset = createDataset() }) @@ -374,6 +390,7 @@ describe('Dropdown', () => { beforeEach(() => { vi.clearAllMocks() mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' }) + mockIsRbacEnabled = true mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' }) mockCheckIsUsedInApp.mockResolvedValue({ is_using: false }) mockDeleteDataset.mockResolvedValue({}) @@ -430,6 +447,24 @@ describe('Dropdown', () => { expect(screen.queryByText('common.operation.edit')).not.toBeInTheDocument() expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() }) + + it('should hide resource access option when RBAC is disabled', async () => { + const user = userEvent.setup() + // Arrange + mockIsRbacEnabled = false + mockDataset = createDataset({ + runtime_mode: 'general', + permission_keys: [DatasetACLPermission.AccessConfig, DatasetACLPermission.Delete], + }) + render() + + // Act + await openMenu(user) + + // Assert + expect(screen.getByText('common.operation.delete')).toBeInTheDocument() + expect(screen.queryByText('common.settings.resourceAccess')).not.toBeInTheDocument() + }) }) // User interactions that trigger modals and exports. diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 6539e6e1715..51c433d5f0a 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -15,11 +15,13 @@ import { DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { toast } from '@langgenius/dify-ui/toast' +import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { checkIsUsedInApp, deleteDataset } from '@/service/datasets' import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/knowledge/use-dataset' @@ -69,11 +71,14 @@ const DropDown = ({ const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet const currentUserId = useAppContextWithSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys, { currentUserId, resourceMaintainer: dataset?.maintainer, workspacePermissionKeys, - }), [dataset?.maintainer, dataset?.permission_keys, currentUserId, workspacePermissionKeys]) + isRbacEnabled, + }), [dataset?.maintainer, dataset?.permission_keys, currentUserId, isRbacEnabled, workspacePermissionKeys]) const canShowOperations = datasetACLCapabilities.canEdit || datasetACLCapabilities.canImportExportDSL || datasetACLCapabilities.canAccessConfig diff --git a/web/app/components/app/access-config/__tests__/index.spec.tsx b/web/app/components/app/access-config/__tests__/index.spec.tsx index 49e8c86825d..63909979344 100644 --- a/web/app/components/app/access-config/__tests__/index.spec.tsx +++ b/web/app/components/app/access-config/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import type { AccessRulesEditorProps } from '@/app/components/access-rules-editor' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { useStore } from '@/app/components/app/store' import { useAppAccessRules, @@ -13,6 +14,14 @@ const mockAppContext = vi.hoisted(() => ({ workspacePermissionKeys: [] as string[], })) +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) + const mockAppAccessRules = vi.hoisted(() => ({ items: [] as AccessRulesEditorProps['rules'], isLoading: false, @@ -75,6 +84,7 @@ describe('AppAccessConfigPage', () => { vi.clearAllMocks() mockAppContext.userProfile = { id: 'user-1' } mockAppContext.workspacePermissionKeys = [] + mockIsRbacEnabled = true mockAppAccessRules.items = [] mockAppAccessRules.isLoading = false mockAppUserAccessSettings.data = [] @@ -195,6 +205,16 @@ describe('AppAccessConfigPage', () => { expect(useAppUserAccessSettings).not.toHaveBeenCalled() }) + it('should not mount access config data hooks when RBAC is disabled', () => { + mockIsRbacEnabled = false + + render() + + expect(screen.queryByTestId('access-rules-editor')).not.toBeInTheDocument() + expect(useAppAccessRules).not.toHaveBeenCalled() + expect(useAppUserAccessSettings).not.toHaveBeenCalled() + }) + it('should allow the app maintainer with app management workspace permission', () => { mockAppContext.userProfile = { id: 'account-1' } mockAppContext.workspacePermissionKeys = ['app.create_and_management'] diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx index 9704540bbd7..6efc718012c 100644 --- a/web/app/components/app/access-config/index.tsx +++ b/web/app/components/app/access-config/index.tsx @@ -2,12 +2,14 @@ import type { ResourceOpenScope } from '@/models/access-control' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AccessRulesEditor from '@/app/components/access-rules-editor' import { useStore } from '@/app/components/app/store' import { useAppContext } from '@/context/app-context' import { useLocale } from '@/context/i18n' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getAccessControlTemplateLanguage } from '@/i18n-config/language' import { useAppAccessRules, @@ -103,13 +105,16 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP } const AppAccessConfigPage = ({ appId }: AppAccessConfigPageProps) => { + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { userProfile, workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const appDetail = useStore(state => state.appDetail) const appACLCapabilities = useMemo(() => getAppACLCapabilities(appDetail?.permission_keys, { currentUserId: userProfile?.id, resourceMaintainer: appDetail?.maintainer, workspacePermissionKeys, - }), [appDetail?.maintainer, appDetail?.permission_keys, userProfile?.id, workspacePermissionKeys]) + isRbacEnabled, + }), [appDetail?.maintainer, appDetail?.permission_keys, isRbacEnabled, userProfile?.id, workspacePermissionKeys]) if (!appDetail || appDetail.id !== appId || !appACLCapabilities.canAccessConfig) return null diff --git a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx index 72e3ff2c19a..ada0afe17fa 100644 --- a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage' import { AppModeEnum } from '@/types/app' import Apps from '../index' @@ -295,7 +296,7 @@ describe('Apps', () => { id: 'created-app-id', mode: AppModeEnum.CHAT, permission_keys: ['app.acl.view_layout'], - }, mockPush) + }, mockPush, { isRbacEnabled: false }) }) it('shows an error toast when importing the template fails', async () => { diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 04e759575b9..c97b8b5fcb5 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -5,6 +5,7 @@ import type { App } from '@/models/explore' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { RiRobot2Line } from '@remixicon/react' +import { useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import * as React from 'react' import { useMemo, useState } from 'react' @@ -17,6 +18,7 @@ import Loading from '@/app/components/base/loading' import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { useAppContext } from '@/context/app-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { DSLImportMode } from '@/models/app' import { useRouter } from '@/next/navigation' import { importDSL } from '@/service/apps' @@ -45,7 +47,9 @@ const Apps = ({ onCreateFromBlank, }: AppsProps) => { const { t } = useTranslation() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const canCreateAppFromTemplate = hasPermission(workspacePermissionKeys, 'app.create_and_management') const { push } = useRouter() const invalidateAppList = useInvalidateAppList() @@ -144,7 +148,7 @@ const Apps = ({ setNeedRefresh('1') invalidateAppList() if (app.app_id) - getRedirection({ id: app.app_id, mode: app.app_mode, permission_keys: app.permission_keys }, push) + getRedirection({ id: app.app_id, mode: app.app_mode, permission_keys: app.permission_keys }, push, { isRbacEnabled }) } catch { toast.error(t('newApp.appCreateFailed', { ns: 'app' })) diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index 47c8fb5c46c..d583dadedda 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -1,7 +1,8 @@ import type { App } from '@/types/app' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' @@ -177,6 +178,7 @@ describe('CreateAppModal', () => { currentUserId: 'user-1', resourceMaintainer: 'user-1', workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: false, }), ) }) diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index ce3f11f24c3..a53f07d5b12 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -9,6 +9,7 @@ import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' +import { useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,6 +21,7 @@ import Input from '@/app/components/base/input' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useTheme from '@/hooks/use-theme' import { useRouter } from '@/next/navigation' import { createApp } from '@/service/apps' @@ -56,7 +58,9 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: const { plan, enableBilling } = useProviderContext() const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { userProfile, workspacePermissionKeys } = useAppContext() + const isRbacEnabled = systemFeatures.rbac_enabled const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const invalidateAppList = useInvalidateAppList() @@ -100,13 +104,14 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: currentUserId: userProfile?.id, resourceMaintainer: app.maintainer, workspacePermissionKeys, + isRbacEnabled, }) } catch (error) { toast.error(error instanceof Error ? error.message : t('newApp.appCreateFailed', { ns: 'app' })) } isCreatingRef.current = false - }, [canCreateApp, name, t, appMode, appIcon, description, onSuccess, onClose, push, userProfile?.id, workspacePermissionKeys, setNeedRefresh, invalidateAppList]) + }, [canCreateApp, name, t, appMode, appIcon, description, onSuccess, onClose, push, userProfile?.id, workspacePermissionKeys, isRbacEnabled, setNeedRefresh, invalidateAppList]) const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) useHotkey('Mod+Enter', () => { diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index 45bb635edcb..fb58e964b75 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -2,10 +2,10 @@ import { act, fireEvent, - render, screen, waitFor, } from '@testing-library/react' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { AppModeEnum } from '@/types/app' @@ -247,6 +247,7 @@ describe('CreateFromDSLModal', () => { expect(mockGetRedirection).toHaveBeenCalledWith( { id: 'app-1', mode: 'chat', permission_keys: ['app.acl.view_layout'] }, mockPush, + { isRbacEnabled: false }, ) }) diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index c44e6f3769c..8414b7c6f4f 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -7,6 +7,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' +import { useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -15,6 +16,7 @@ import Input from '@/app/components/base/input' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { DSLImportMode, DSLImportStatus, @@ -55,6 +57,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS const [importId, setImportId] = useState() const { handleCheckPluginDependencies } = usePluginDependencies() const setNeedRefresh = useSetNeedRefreshAppList() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const readFile = useCallback((file: File) => { const reader = new FileReader() @@ -129,7 +133,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS invalidateAppList() if (app_id) { await handleCheckPluginDependencies(app_id) - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push) + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) } } else if (status === DSLImportStatus.PENDING) { @@ -184,7 +188,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS setNeedRefresh('1') invalidateAppList() if (app_id) - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push) + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) } else if (status === DSLImportStatus.FAILED) { toast.error(response.error || t('newApp.appCreateFailed', { ns: 'app' })) diff --git a/web/app/components/app/log/__tests__/empty-element.spec.tsx b/web/app/components/app/log/__tests__/empty-element.spec.tsx index 41f82375f9a..f53225c5554 100644 --- a/web/app/components/app/log/__tests__/empty-element.spec.tsx +++ b/web/app/components/app/log/__tests__/empty-element.spec.tsx @@ -1,5 +1,6 @@ import type { App } from '@/types/app' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import { AppModeEnum } from '@/types/app' import EmptyElement from '../empty-element' diff --git a/web/app/components/app/log/empty-element.tsx b/web/app/components/app/log/empty-element.tsx index 232e913b942..70813290319 100644 --- a/web/app/components/app/log/empty-element.tsx +++ b/web/app/components/app/log/empty-element.tsx @@ -1,9 +1,11 @@ 'use client' import type { FC, SVGProps } from 'react' import type { App } from '@/types/app' +import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import Link from '@/next/link' import { AppModeEnum } from '@/types/app' import { getRedirectionPath } from '@/utils/app-redirection' @@ -21,6 +23,8 @@ const EmptyElement: FC<{ appDetail: App }> = ({ appDetail }) => { const { t } = useTranslation() const currentUserId = useAppContextWithSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const getWebAppType = (appType: AppModeEnum) => { if (appType !== AppModeEnum.COMPLETION && appType !== AppModeEnum.WORKFLOW) @@ -54,6 +58,7 @@ const EmptyElement: FC<{ appDetail: App }> = ({ appDetail }) => { currentUserId, resourceMaintainer: appDetail.maintainer, workspacePermissionKeys, + isRbacEnabled, })} className="text-util-colors-blue-blue-600" /> diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index 8d3684c0d03..eda5f2cc11a 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -1,7 +1,8 @@ import type { App } from '@/types/app' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import { useStore as useAppStore } from '@/app/components/app/store' import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage' import { Plan } from '@/app/components/billing/type' diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 40eb7252746..694623d3e54 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -16,6 +16,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' +import { useSuspenseQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' @@ -25,6 +26,7 @@ import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/aler import Input from '@/app/components/base/input' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { deleteApp, switchApp } from '@/service/apps' import { AppModeEnum } from '@/types/app' @@ -43,6 +45,8 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo const { push, replace } = useRouter() const { t } = useTranslation() const setAppDetail = useAppStore(s => s.setAppDetail) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const { plan, enableBilling } = useProviderContext() const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) @@ -86,6 +90,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo permission_keys, }, removeOriginal ? replace : push, + { isRbacEnabled }, ) } catch { diff --git a/web/app/components/app/workflow-log/__tests__/index.spec.tsx b/web/app/components/app/workflow-log/__tests__/index.spec.tsx index af5f7ed85a8..79afa706293 100644 --- a/web/app/components/app/workflow-log/__tests__/index.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/index.spec.tsx @@ -19,9 +19,9 @@ import type { MockedFunction } from 'vitest' import type { ILogsProps } from '../index' import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' import type { App, AppIconType, AppModeEnum } from '@/types/app' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { APP_PAGE_LIMIT } from '@/config' import { WorkflowRunTriggeredFrom } from '@/models/log' import * as useLogModule from '@/service/use-log' @@ -101,21 +101,8 @@ const mockedUseWorkflowLogs = useLogModule.useWorkflowLogs as MockedFunction new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}) - const renderWithQueryClient = (ui: React.ReactElement) => { - const queryClient = createQueryClient() - return render( - - {ui} - , - ) + return renderWithSystemFeatures(ui) } // ============================================================================ diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index 78c47fc865c..f1a2c4e67a9 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -13,11 +13,13 @@ import { AppCard } from '../app-card' import { StarredAppCard } from '../starred-app-card' let mockWebappAuthEnabled = false +let mockRbacEnabled = true const render = (ui: React.ReactElement) => renderWithSystemFeatures(ui, { systemFeatures: { webapp_auth: { enabled: mockWebappAuthEnabled }, branding: { enabled: false }, + rbac_enabled: mockRbacEnabled, }, }) @@ -377,6 +379,7 @@ describe('AppCard', () => { vi.clearAllMocks() mockOpenAsyncWindow.mockReset() mockWebappAuthEnabled = false + mockRbacEnabled = true mockDeleteMutationPending = false mockToggleStarMutationPending = false mockAppContext.isCurrentWorkspaceEditor = true @@ -390,6 +393,70 @@ describe('AppCard', () => { expect(screen.getByRole('link', { name: 'Test App' })).toBeInTheDocument() }) + it('should render preview-only app card as a dimmed information-only card', () => { + const previewOnlyApp = createMockApp({ + name: 'Preview Only App', + description: 'Only visible metadata', + author_name: 'Readonly Author', + created_by: 'another-user', + maintainer: 'another-user', + tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'app' as const, binding_count: 0 }], + permission_keys: [AppACLPermission.Preview], + }) + + render() + + const card = screen.getByRole('button', { name: 'Preview Only App' }) + expect(card).toHaveClass('opacity-60') + expect(card).toHaveAttribute('aria-disabled', 'true') + expect(screen.getByText('Only visible metadata')).toBeInTheDocument() + expect(screen.getByText('Readonly Author')).toBeInTheDocument() + const tagSelector = screen.getByLabelText('tag-selector') + expect(tagSelector).toBeInTheDocument() + expect(tagSelector).toHaveAttribute('data-can-bind-or-unbind-tags', 'false') + expect(screen.queryByRole('link', { name: 'Preview Only App' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'app.studio.starApp' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.operation.more' })).not.toBeInTheDocument() + + fireEvent.click(tagSelector) + + expect(toastMocks.record).not.toHaveBeenCalled() + + fireEvent.click(card) + + expect(toastMocks.record).toHaveBeenCalledWith({ + type: 'warning', + message: 'app.noAccessResourcePermission', + }) + }) + + it('should render preview-only starred app card as a dimmed information-only card', () => { + const previewOnlyApp = createMockApp({ + name: 'Preview Only Starred App', + author_name: 'Readonly Author', + created_by: 'another-user', + maintainer: 'another-user', + permission_keys: [AppACLPermission.Preview], + }) + + render() + + const card = screen.getByRole('button', { name: 'Preview Only Starred App' }) + expect(card).toHaveClass('opacity-60') + expect(card).toHaveAttribute('aria-disabled', 'true') + expect(screen.getByText('Readonly Author')).toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'Preview Only Starred App' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'app.studio.starApp' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.operation.more' })).not.toBeInTheDocument() + + fireEvent.click(card) + + expect(toastMocks.record).toHaveBeenCalledWith({ + type: 'warning', + message: 'app.noAccessResourcePermission', + }) + }) + it('should display app name', () => { render() expect(screen.getByText('Test App')).toBeInTheDocument() @@ -473,7 +540,22 @@ describe('AppCard', () => { expect(screen.getByLabelText('tag-selector')).toHaveAttribute('data-can-bind-or-unbind-tags', 'true') }) - it('should not render tag selector without app edit or workspace tag management permission', () => { + it('should allow workspace app tag management permission to bind tags without app edit permission', () => { + mockAppContext.isCurrentWorkspaceEditor = false + mockAppContext.workspacePermissionKeys = ['app.tag.manage'] + mockAppContext.userProfile = { id: 'user-2' } + const tagManageApp = createMockApp({ + maintainer: 'user-1', + tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: 0 }], + permission_keys: [AppACLPermission.ViewLayout], + }) + + render() + + expect(screen.getByLabelText('tag-selector')).toHaveAttribute('data-can-bind-or-unbind-tags', 'true') + }) + + it('should render existing app tags as readonly without app edit or workspace tag management permission', () => { mockAppContext.isCurrentWorkspaceEditor = false mockAppContext.workspacePermissionKeys = [] mockAppContext.userProfile = { id: 'user-2' } @@ -485,7 +567,7 @@ describe('AppCard', () => { render() - expect(screen.queryByLabelText('tag-selector')).not.toBeInTheDocument() + expect(screen.getByLabelText('tag-selector')).toHaveAttribute('data-can-bind-or-unbind-tags', 'false') }) it('should render with onRefresh callback', () => { @@ -1767,6 +1849,23 @@ describe('AppCard', () => { expect(screen.getByText('common.settings.resourceAccess')).toBeInTheDocument() }) + it('should hide resource access option when RBAC is disabled', async () => { + mockRbacEnabled = false + const appWithAccessConfigPermission = createMockApp({ + created_by: 'another-user', + maintainer: 'another-user', + permission_keys: [AppACLPermission.AccessConfig, AppACLPermission.Delete], + }) + render() + + fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + + await waitFor(() => { + expect(screen.getByText('common.operation.delete')).toBeInTheDocument() + }) + expect(screen.queryByText('common.settings.resourceAccess')).not.toBeInTheDocument() + }) + it('should navigate to app access config when resource access is clicked', async () => { const appWithAccessConfigPermission = createMockApp({ created_by: 'another-user', diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 7e2f173ee3f..92868958505 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FormEvent, FormEventHandler, MouseEvent } from 'react' +import type { FormEvent, FormEventHandler, KeyboardEvent, MouseEvent } from 'react' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' @@ -56,7 +56,7 @@ import { fetchWorkflowDraft } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import { getRedirection, getRedirectionPath } from '@/utils/app-redirection' import { downloadBlob } from '@/utils/download' -import { getAppACLCapabilities, hasPermission } from '@/utils/permission' +import { getAppACLCapabilities, hasOnlyAppPreviewPermission, hasPermission } from '@/utils/permission' import { formatTime } from '@/utils/time' import { basePath } from '@/utils/var' @@ -297,6 +297,7 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) { const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const currentUserId = useAppContextSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys) + const isRbacEnabled = systemFeatures.rbac_enabled const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() @@ -316,8 +317,10 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) { currentUserId, resourceMaintainer, workspacePermissionKeys, - }), [currentUserId, resourceMaintainer, workspacePermissionKeys]) + isRbacEnabled, + }), [currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys]) const appACLCapabilities = useMemo(() => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions), [app.permission_keys, maintainerPermissionOptions]) + const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const onConfirmDelete = useCallback(async () => { @@ -441,6 +444,7 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) { currentUserId, resourceMaintainer: getAppResourceMaintainer(newApp), workspacePermissionKeys, + isRbacEnabled, }) } catch { @@ -526,101 +530,103 @@ export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) { return ( <> -
- - - - - )} - /> - {starActionLabel} - - {shouldShowOperationsMenu && ( - - + + + + )} - onClick={(e) => { - e.stopPropagation() - e.preventDefault() - }} - > - {t('operation.more', { ns: 'common' })} - - - - {systemFeatures.webapp_auth.enabled - ? ( - - ) - : ( - - )} - - - )} -
+ /> + {starActionLabel} + + {shouldShowOperationsMenu && ( + + { + e.stopPropagation() + e.preventDefault() + }} + > + {t('operation.more', { ns: 'common' })} + + + + {systemFeatures.webapp_auth.enabled + ? ( + + ) + : ( + + )} + + + )} +
+ )} {showEditModal && ( state.userProfile?.id) const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys) + const isRbacEnabled = systemFeatures.rbac_enabled const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() @@ -743,8 +750,10 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement currentUserId, resourceMaintainer, workspacePermissionKeys, - }), [currentUserId, resourceMaintainer, workspacePermissionKeys]) + isRbacEnabled, + }), [currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys]) const appACLCapabilities = useMemo(() => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions), [app.permission_keys, maintainerPermissionOptions]) + const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const canManageAppTags = hasPermission(workspacePermissionKeys, 'app.tag.manage') @@ -871,6 +880,7 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement currentUserId, resourceMaintainer: getAppResourceMaintainer(newApp), workspacePermissionKeys, + isRbacEnabled, }) } catch { @@ -951,7 +961,7 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement const shouldShowAccessConfigOption = appACLCapabilities.canAccessConfig const shouldShowDeleteOption = appACLCapabilities.canDelete const shouldShowOperationsMenu = shouldShowEditOption || shouldShowDuplicateOption || shouldShowExportOption || shouldShowSwitchOption || shouldShowAccessControlOption || shouldShowAccessConfigOption || shouldShowDeleteOption - const shouldShowAppTags = appACLCapabilities.canEdit || canManageAppTags + const canBindOrUnbindTags = !isPreviewOnly && (canManageAppTags || appACLCapabilities.canEdit) const operationsMenuWidthClassName = shouldShowSwitchOption ? 'w-[256px]' : 'w-[216px]' const editTimeText = useMemo(() => { @@ -995,174 +1005,212 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement const appNameId = useId() const appDescriptionId = useId() const appHref = getRedirectionPath(app, maintainerPermissionOptions) + const appCardClassName = cn( + 'inline-flex h-full w-full touch-manipulation flex-col overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs outline-hidden transition-shadow duration-200 ease-in-out', + isPreviewOnly + ? 'cursor-not-allowed opacity-60 focus-visible:ring-2 focus-visible:ring-state-accent-solid' + : 'cursor-pointer hover:shadow-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid', + ) const starActionLabel = app.is_starred ? t('studio.unstarApp', { ns: 'app' }) : t('studio.starApp', { ns: 'app' }) + const showPreviewOnlyAccessWarning = useCallback(() => { + toast.warning(t('noAccessResourcePermission', { ns: 'app' })) + }, [t]) + const handlePreviewOnlyCardKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') + return + + event.preventDefault() + showPreviewOnlyAccessWarning() + }, [showPreviewOnlyAccessWarning]) + const appCardContent = ( + <> +
+
+ + +
+
+
+
{app.name}
+
+
{appModeLabel}
+
+ {onlinePresenceUsers.length > 0 && ( +
+ +
+ )} +
+
+
+ {app.description} +
+
+
+
+
+
{app.author_name}
+
·
+
{editTimeText}
+
+
+ + ) return ( <>
- + {appCardContent} + + ) + : ( + + {appCardContent} + + )} +
{ + e.stopPropagation() + e.preventDefault() + }} > -
-
- +
+ + {!isPreviewOnly && ( +
+ + + + + )} /> - -
-
-
-
{app.name}
-
-
{appModeLabel}
-
- {onlinePresenceUsers.length > 0 && ( -
- -
+ {starActionLabel} + + {shouldShowOperationsMenu && ( + + { + e.stopPropagation() + e.preventDefault() + }} + > + {t('operation.more', { ns: 'common' })} + + + + {systemFeatures.webapp_auth.enabled + ? ( + + ) + : ( + + )} + + )}
-
-
- {app.description} -
-
-
-
-
-
{app.author_name}
-
·
-
{editTimeText}
-
-
- - {shouldShowAppTags && ( -
{ - e.stopPropagation() - e.preventDefault() - }} - > - -
)} - -
- - - - - )} - /> - {starActionLabel} - - {shouldShowOperationsMenu && ( - - { - e.stopPropagation() - e.preventDefault() - }} - > - {t('operation.more', { ns: 'common' })} - - - - {systemFeatures.webapp_auth.enabled - ? ( - - ) - : ( - - )} - - - )} -
{showEditModal && ( state.userProfile?.id) const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled + const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const editTimeText = useMemo(() => { const timestamp = app.updated_at || app.created_at @@ -36,34 +45,69 @@ export function StarredAppCard({ app, onRefresh }: StarredAppCardProps) { currentUserId, resourceMaintainer: app.maintainer, workspacePermissionKeys, + isRbacEnabled, }) + const cardClassName = cn( + 'flex h-[72px] min-w-0 items-center gap-3 overflow-hidden rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg px-4 py-3 shadow-xs outline-hidden transition-shadow duration-200', + isPreviewOnly + ? 'cursor-not-allowed opacity-60 focus-visible:ring-2 focus-visible:ring-state-accent-solid' + : 'hover:shadow-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid', + ) + const showPreviewOnlyAccessWarning = useCallback(() => { + toast.warning(t('noAccessResourcePermission', { ns: 'app' })) + }, [t]) + const handlePreviewOnlyCardKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') + return + + event.preventDefault() + showPreviewOnlyAccessWarning() + }, [showPreviewOnlyAccessWarning]) + const cardContent = ( + <> +
+ + +
+
+
{app.name}
+
+ {app.author_name && {app.author_name}} + {app.author_name && editTimeText && ·} + {editTimeText && {editTimeText}} +
+
+ + ) return (
- -
- - -
-
-
{app.name}
-
- {app.author_name && {app.author_name}} - {app.author_name && editTimeText && ·} - {editTimeText && {editTimeText}} -
-
- - + {isPreviewOnly + ? ( +
+ {cardContent} +
+ ) + : ( + + {cardContent} + + )} + {!isPreviewOnly && }
) } diff --git a/web/app/components/datasets/access-config/__tests__/index.spec.tsx b/web/app/components/datasets/access-config/__tests__/index.spec.tsx index e93c0ef9433..eeeec0dcf88 100644 --- a/web/app/components/datasets/access-config/__tests__/index.spec.tsx +++ b/web/app/components/datasets/access-config/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import type { AccessRulesEditorProps } from '@/app/components/access-rules-editor' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { useDatasetAccessRules, useDatasetUserAccessSettings, @@ -27,6 +28,14 @@ const mockAppContextState = vi.hoisted(() => ({ workspacePermissionKeys: [] as string[], })) +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) + const mockAccessRulesEditor = vi.hoisted(() => ({ props: null as AccessRulesEditorProps | null, })) @@ -93,6 +102,7 @@ describe('DatasetAccessConfigPage', () => { } mockAppContextState.userProfile = { id: 'user-1' } mockAppContextState.workspacePermissionKeys = [] + mockIsRbacEnabled = true mockAccessRulesEditor.props = null }) @@ -162,6 +172,16 @@ describe('DatasetAccessConfigPage', () => { expect(vi.mocked(useDatasetUserAccessSettings)).toHaveBeenCalledWith('dataset-1', expect.any(String), { enabled: false }) }) + it('should disable access config queries and hide the editor when RBAC is disabled', () => { + mockIsRbacEnabled = false + + render() + + expect(screen.queryByTestId('access-rules-editor')).not.toBeInTheDocument() + expect(vi.mocked(useDatasetAccessRules)).toHaveBeenCalledWith('dataset-1', expect.any(String), { enabled: false }) + expect(vi.mocked(useDatasetUserAccessSettings)).toHaveBeenCalledWith('dataset-1', expect.any(String), { enabled: false }) + }) + it('should wire open scope and user policy updates', () => { render() diff --git a/web/app/components/datasets/access-config/index.tsx b/web/app/components/datasets/access-config/index.tsx index fd9a851ccf1..b1df8e3eea7 100644 --- a/web/app/components/datasets/access-config/index.tsx +++ b/web/app/components/datasets/access-config/index.tsx @@ -2,6 +2,7 @@ import type { ResourceOpenScope } from '@/models/access-control' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import AccessRulesEditor from '@/app/components/access-rules-editor' @@ -9,6 +10,7 @@ import Loading from '@/app/components/base/loading' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useLocale } from '@/context/i18n' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getAccessControlTemplateLanguage } from '@/i18n-config/language' import { useDatasetAccessRules, @@ -30,11 +32,14 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) => const dataset = useDatasetDetailContextWithSelector(state => state.dataset) const currentUserId = useAppContextWithSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const canAccessConfig = useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys, { currentUserId, resourceMaintainer: dataset?.maintainer, workspacePermissionKeys, - }).canAccessConfig, [currentUserId, dataset?.maintainer, dataset?.permission_keys, workspacePermissionKeys]) + isRbacEnabled, + }).canAccessConfig, [currentUserId, dataset?.maintainer, dataset?.permission_keys, isRbacEnabled, workspacePermissionKeys]) const { data: datasetAccessRulesResponse, isLoading: isLoadingDatasetAccessRules } = useDatasetAccessRules(datasetId, language, { enabled: canAccessConfig }) const { data: datasetUserAccessSettingsResponse, isLoading: isLoadingDatasetUserAccessSettings } = useDatasetUserAccessSettings(datasetId, language, { enabled: canAccessConfig }) const { mutate: updateDatasetOpenScope, isPending: isUpdatingDatasetOpenScope } = useUpdateDatasetOpenScope(datasetId) diff --git a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx index 4f85e926ea7..46fe308a720 100644 --- a/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { DatasetACLPermission } from '@/utils/permission' import DatasetCardFooter from '../components/dataset-card-footer' import Description from '../components/description' import DatasetCard from '../index' @@ -21,6 +22,23 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ const mockPush = vi.fn() const mockOpenAccessConfig = vi.fn() const mockCloseAccessConfig = vi.fn() +const toastMocks = vi.hoisted(() => { + const record = vi.fn() + const api = Object.assign(vi.fn((message: unknown, options?: Record) => record({ message, ...options })), { + success: vi.fn((message: unknown, options?: Record) => record({ type: 'success', message, ...options })), + error: vi.fn((message: unknown, options?: Record) => record({ type: 'error', message, ...options })), + warning: vi.fn((message: unknown, options?: Record) => record({ type: 'warning', message, ...options })), + info: vi.fn((message: unknown, options?: Record) => record({ type: 'info', message, ...options })), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }) + return { record, api } +}) + +vi.mock('@langgenius/dify-ui/toast', () => ({ + toast: toastMocks.api, +})) vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: mockPush }), @@ -263,6 +281,11 @@ describe('DatasetCard Integration', () => { describe('DatasetCard Component', () => { beforeEach(() => { vi.clearAllMocks() + mockAppContextState = { + isCurrentWorkspaceDatasetOperator: false, + userProfile: { id: 'user-1' }, + workspacePermissionKeys: [], + } }) it('should render and navigate to documents when clicked', () => { @@ -273,6 +296,52 @@ describe('DatasetCard Component', () => { expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents') }) + it('should render preview-only dataset as a dimmed information-only card', () => { + const dataset = createMockDataset({ + name: 'Preview Only Dataset', + permission_keys: [DatasetACLPermission.Preview], + tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'knowledge' as const, binding_count: 0 }], + }) + render() + + const card = screen.getByRole('button', { name: 'Preview Only Dataset' }) + expect(card).toHaveClass('opacity-60') + expect(card).toHaveAttribute('aria-disabled', 'true') + expect(screen.getByText('Preview Only Dataset')).toBeInTheDocument() + const tagArea = screen.getByTestId('tag-area') + expect(tagArea).toHaveAttribute('data-can-bind-or-unbind-tags', 'false') + expect(screen.queryByTestId('operations-dropdown')).not.toBeInTheDocument() + + fireEvent.click(tagArea) + + expect(mockPush).not.toHaveBeenCalled() + expect(toastMocks.record).not.toHaveBeenCalled() + + fireEvent.click(card) + + expect(mockPush).not.toHaveBeenCalled() + expect(toastMocks.record).toHaveBeenCalledWith({ + type: 'warning', + message: 'app.noAccessResourcePermission', + }) + }) + + it('should not navigate preview-only external dataset to a detail page', () => { + const dataset = createMockDataset({ + provider: 'external', + permission_keys: [DatasetACLPermission.Preview], + }) + render() + + fireEvent.click(screen.getByRole('button', { name: 'Test Dataset' })) + + expect(mockPush).not.toHaveBeenCalled() + expect(toastMocks.record).toHaveBeenCalledWith({ + type: 'warning', + message: 'app.noAccessResourcePermission', + }) + }) + it('should use the hover background treatment', () => { const dataset = createMockDataset() render() @@ -338,6 +407,19 @@ describe('DatasetCard Component', () => { expect(screen.getByTestId('tag-area')).toHaveAttribute('data-can-bind-or-unbind-tags', 'true') }) + it('should allow tag binding with workspace dataset tag management permission', () => { + mockAppContextState = { + isCurrentWorkspaceDatasetOperator: false, + userProfile: { id: 'user-1' }, + workspacePermissionKeys: ['dataset.tag.manage'], + } + const dataset = createMockDataset({ permission_keys: ['dataset.acl.readonly'] }) + + render() + + expect(screen.getByTestId('tag-area')).toHaveAttribute('data-can-bind-or-unbind-tags', 'true') + }) + it('should not allow tag binding when dataset lacks edit ACL', () => { const dataset = createMockDataset({ permission_keys: ['dataset.acl.readonly'] }) diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx index 5a6303d83e9..e15135b4281 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx @@ -1,11 +1,29 @@ import type { DataSet } from '@/models/datasets' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { DatasetACLPermission } from '@/utils/permission' import OperationsDropdown from '../operations-dropdown' +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { id: 'user-1' }, + workspacePermissionKeys: [] as string[], +})) + +let mockIsRbacEnabled = true + +const render = (ui: Parameters[0]) => renderWithSystemFeatures(ui, { + systemFeatures: { + rbac_enabled: mockIsRbacEnabled, + }, +}) + +vi.mock('@/context/app-context', () => ({ + useSelector: vi.fn((selector: (state: typeof mockAppContextState) => unknown) => selector(mockAppContextState)), +})) + describe('OperationsDropdown', () => { const createMockDataset = (overrides: Partial = {}): DataSet => ({ id: 'dataset-1', @@ -45,6 +63,9 @@ describe('OperationsDropdown', () => { beforeEach(() => { vi.clearAllMocks() + mockAppContextState.userProfile = { id: 'user-1' } + mockAppContextState.workspacePermissionKeys = [] + mockIsRbacEnabled = true }) describe('Rendering', () => { @@ -119,6 +140,19 @@ describe('OperationsDropdown', () => { expect(screen.getByText('common.settings.resourceAccess')).toBeInTheDocument() }) + + it('should hide resource access option when RBAC is disabled', () => { + mockIsRbacEnabled = false + const dataset = createMockDataset({ + permission_keys: [DatasetACLPermission.AccessConfig, DatasetACLPermission.Delete], + }) + render() + + fireEvent.click(screen.getByLabelText('Dataset operations')) + + expect(screen.getByText('common.operation.delete')).toBeInTheDocument() + expect(screen.queryByText('common.settings.resourceAccess')).not.toBeInTheDocument() + }) }) describe('Styles', () => { diff --git a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx index 4032810a07a..4c7ded2651e 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx @@ -5,8 +5,10 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getDatasetACLCapabilities } from '@/utils/permission' import Operations from '../operations' @@ -28,11 +30,14 @@ const OperationsDropdown = ({ const [open, setOpen] = React.useState(false) const currentUserId = useAppContextWithSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset.permission_keys, { currentUserId, resourceMaintainer: dataset.maintainer, workspacePermissionKeys, - }), [dataset.maintainer, dataset.permission_keys, currentUserId, workspacePermissionKeys]) + isRbacEnabled, + }), [dataset.maintainer, dataset.permission_keys, currentUserId, isRbacEnabled, workspacePermissionKeys]) const canShowOperations = datasetACLCapabilities.canEdit || datasetACLCapabilities.canImportExportDSL || datasetACLCapabilities.canAccessConfig diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index aee6b54fd93..0e03449aaf5 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -1,10 +1,14 @@ 'use client' +import type { KeyboardEvent, MouseEvent } from 'react' import type { DataSet } from '@/models/datasets' +import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags' import { useRouter } from '@/next/navigation' -import { getDatasetACLCapabilities } from '@/utils/permission' +import { getDatasetACLCapabilities, hasOnlyDatasetPreviewPermission, hasPermission } from '@/utils/permission' import CornerLabels from './components/corner-labels' import DatasetCardFooter from './components/dataset-card-footer' import DatasetCardHeader from './components/dataset-card-header' @@ -26,6 +30,7 @@ const DatasetCard = ({ onSuccess, onOpenTagManagement = () => {}, }: DatasetCardProps) => { + const { t } = useTranslation() const { push } = useRouter() const currentUserId = useAppContextWithSelector(state => state.userProfile?.id) const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) @@ -47,14 +52,26 @@ const DatasetCard = ({ const isPipelineUnpublished = useMemo(() => { return dataset.runtime_mode === 'rag_pipeline' && !dataset.is_published }, [dataset.runtime_mode, dataset.is_published]) + const isPreviewOnly = hasOnlyDatasetPreviewPermission(dataset.permission_keys) const datasetACLCapabilities = useMemo(() => getDatasetACLCapabilities(dataset.permission_keys, { currentUserId, resourceMaintainer: dataset.maintainer, workspacePermissionKeys, }), [dataset.maintainer, dataset.permission_keys, currentUserId, workspacePermissionKeys]) + const canManageAppTags = hasPermission(workspacePermissionKeys, 'dataset.tag.manage') + const canBindOrUnbindTags = !isPreviewOnly && (canManageAppTags || datasetACLCapabilities.canEdit) - const handleCardClick = (e: React.MouseEvent) => { + const showPreviewOnlyAccessWarning = () => { + toast.warning(t('noAccessResourcePermission', { ns: 'app' })) + } + + const handleCardClick = (e: MouseEvent) => { e.preventDefault() + if (isPreviewOnly) { + showPreviewOnlyAccessWarning() + return + } + if (isExternalProvider) { push(datasetACLCapabilities.canRetrievalRecall ? `/datasets/${dataset.id}/hitTesting` @@ -68,17 +85,36 @@ const DatasetCard = ({ } } - const handleTagAreaClick = (e: React.MouseEvent) => { + const handlePreviewOnlyCardKeyDown = (e: KeyboardEvent) => { + if (!isPreviewOnly || (e.key !== 'Enter' && e.key !== ' ')) + return + + e.preventDefault() + showPreviewOnlyAccessWarning() + } + + const handleTagAreaClick = (e: MouseEvent) => { e.stopPropagation() e.preventDefault() } + const cardClassName = cn( + 'group relative col-span-1 flex h-41.5 flex-col overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-[background-color,box-shadow] duration-200 ease-in-out', + isPreviewOnly + ? 'cursor-not-allowed opacity-60 focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden' + : 'cursor-pointer hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5', + ) return ( <>
@@ -90,16 +126,18 @@ const DatasetCard = ({ onClick={handleTagAreaClick} onOpenTagManagement={onOpenTagManagement} onTagsChange={onSuccess} - canBindOrUnbindTags={datasetACLCapabilities.canEdit} + canBindOrUnbindTags={canBindOrUnbindTags} /> - + {!isPreviewOnly && ( + + )}
state.userProfile?.id) const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys) + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const updatedAt = (app.updated_at || app.created_at) * 1000 const href = getRedirectionPath(app, { currentUserId, resourceMaintainer: app.maintainer, workspacePermissionKeys, + isRbacEnabled, }) return ( diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index d59199769ed..ec1ac9887d2 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -183,11 +183,13 @@ describe('AccountSetting', () => { initialTab?: AccountSettingTab onCancel?: () => void onTabChange?: (tab: AccountSettingTab) => void + rbacEnabled?: boolean }) => { const { initialTab = ACCOUNT_SETTING_TAB.MEMBERS, onCancel = mockOnCancel, onTabChange = mockOnTabChange, + rbacEnabled = true, } = props ?? {} const StatefulAccountSetting = () => { @@ -211,6 +213,7 @@ describe('AccountSetting', () => { branding: { enabled: false }, enable_marketplace: true, enable_collaboration_mode: false, + rbac_enabled: rbacEnabled, }, }) } @@ -404,6 +407,26 @@ describe('AccountSetting', () => { expect(screen.queryByRole('button', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() }) + it('should hide role and resource access entries when RBAC is disabled', () => { + // Act + renderAccountSetting({ rbacEnabled: false }) + + // Assert + expect(screen.getByRole('button', { name: 'common.settings.members' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.settings.rolesAndPermissions' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.settings.resourceAccess' })).not.toBeInTheDocument() + }) + + it('should not render direct role pages when RBAC is disabled', () => { + // Act + renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.ACCESS_RULES, rbacEnabled: false }) + + // Assert + expect(screen.queryByTestId('access-rules-page')).not.toBeInTheDocument() + expect(screen.queryByTestId('permissions-page')).not.toBeInTheDocument() + expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(0) + }) + it('should hide billing and custom tabs when disabled', () => { // Arrange vi.mocked(useProviderContext).mockReturnValue({ diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index 3d9b0b1a8c9..b04404bde17 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -3,6 +3,7 @@ import type { AccountSettingTab } from '@/app/components/header/account-setting/ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import BillingPage from '@/app/components/billing/billing-page' @@ -13,6 +14,7 @@ import { import MenuDialog from '@/app/components/header/account-setting/menu-dialog' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { BillingPermission, hasPermission } from '@/utils/permission' import AccessRulesPage from './access-rules-page' @@ -51,12 +53,18 @@ export default function AccountSetting({ const resetModelProviderListExpanded = useResetModelProviderListExpanded() const { t } = useTranslation() const { enableBilling, enableReplaceWebAppLogo } = useProviderContext() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { workspacePermissionKeys } = useAppContext() - const canManageWorkspaceRoles = hasPermission(workspacePermissionKeys, 'workspace.role.manage') + const isRbacEnabled = systemFeatures.rbac_enabled + const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage') const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View) - const activeMenu = activeTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling - ? ACCOUNT_SETTING_TAB.LANGUAGE - : activeTab + const activeMenu = (() => { + if (activeTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling) + return ACCOUNT_SETTING_TAB.LANGUAGE + if ((activeTab === ACCOUNT_SETTING_TAB.PERMISSIONS || activeTab === ACCOUNT_SETTING_TAB.ACCESS_RULES) && !canManageWorkspaceRoles) + return ACCOUNT_SETTING_TAB.MEMBERS + return activeTab + })() const scrollContainerRef = useRef(null) const settingItems: GroupItem[] = [ diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index fade26f0482..e96fe5306ae 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -15,6 +15,7 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' +import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' @@ -25,6 +26,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import WorkflowPreview from '@/app/components/workflow/workflow-preview' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useRouter } from '@/next/navigation' import { generateWorkflow } from '@/service/debug' import { fetchWorkflowDraft } from '@/service/workflow' @@ -97,6 +99,8 @@ const RecoveryDialog = ({ open, onOpenChange, title, description, cancelLabel, c const WorkflowGeneratorModal: React.FC = () => { const { t } = useTranslation('workflow') const router = useRouter() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const isOpen = useWorkflowGeneratorStore(s => s.isOpen) const mode = useWorkflowGeneratorStore(s => s.mode) @@ -347,7 +351,7 @@ const WorkflowGeneratorModal: React.FC = () => { }) toast.success(t('workflowGenerator.applied')) closeGenerator() - router.push(getRedirectionPath({ id: appId, mode: appMode, permission_keys: permissionKeys })) + router.push(getRedirectionPath({ id: appId, mode: appMode, permission_keys: permissionKeys }, { isRbacEnabled })) } catch (e: unknown) { if (e instanceof WorkflowApplyOrphanError) { @@ -364,7 +368,7 @@ const WorkflowGeneratorModal: React.FC = () => { finally { setApplyingFalse() } - }, [current, instruction, mode, router, closeGenerator, t, isApplying, setApplyingTrue, setApplyingFalse]) + }, [current, instruction, mode, router, closeGenerator, t, isApplying, isRbacEnabled, setApplyingTrue, setApplyingFalse]) const handleApplyToCurrentConfirmed = useCallback(async () => { if (!current?.graph || !currentAppId || isApplying) diff --git a/web/hooks/use-import-dsl.ts b/web/hooks/use-import-dsl.ts index 97a1f9e93f2..0f7004048fd 100644 --- a/web/hooks/use-import-dsl.ts +++ b/web/hooks/use-import-dsl.ts @@ -4,6 +4,7 @@ import type { } from '@/models/app' import type { AppIconType } from '@/types/app' import { toast } from '@langgenius/dify-ui/toast' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useRef, @@ -12,6 +13,7 @@ import { import { useTranslation } from 'react-i18next' import { useSetNeedRefreshAppList } from '@/app/components/apps/storage' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' +import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' import { @@ -42,6 +44,8 @@ export const useImportDSL = () => { const { handleCheckPluginDependencies } = usePluginDependencies() const { push } = useRouter() const invalidateAppList = useInvalidateAppList() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const isRbacEnabled = systemFeatures.rbac_enabled const [versions, setVersions] = useState<{ importedVersion: string, systemVersion: string }>() const importIdRef = useRef('') const setNeedRefresh = useSetNeedRefreshAppList() @@ -91,7 +95,7 @@ export const useImportDSL = () => { setNeedRefresh('1') invalidateAppList() await handleCheckPluginDependencies(app_id) - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push) + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) } else if (status === DSLImportStatus.PENDING) { setVersions({ @@ -113,7 +117,7 @@ export const useImportDSL = () => { finally { setIsFetching(false) } - }, [isFetching, t, handleCheckPluginDependencies, push, setNeedRefresh, invalidateAppList]) + }, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, push, setNeedRefresh, invalidateAppList]) const handleImportDSLConfirm = useCallback(async ( { @@ -142,7 +146,7 @@ export const useImportDSL = () => { await handleCheckPluginDependencies(app_id) setNeedRefresh('1') invalidateAppList() - getRedirection({ id: app_id, mode: app_mode, permission_keys }, push) + getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, { isRbacEnabled }) } else if (status === DSLImportStatus.FAILED) { toast.error(t('newApp.appCreateFailed', { ns: 'app' })) @@ -156,7 +160,7 @@ export const useImportDSL = () => { finally { setIsFetching(false) } - }, [isFetching, t, handleCheckPluginDependencies, setNeedRefresh, push, invalidateAppList]) + }, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, setNeedRefresh, push, invalidateAppList]) return { handleImportDSL, diff --git a/web/i18n/ar-TN/app.json b/web/i18n/ar-TN/app.json index 3a295fe102a..a019ffcedad 100644 --- a/web/i18n/ar-TN/app.json +++ b/web/i18n/ar-TN/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "سير العمل", "newAppFromTemplate.sidebar.Writing": "كتابة", "noAccessPermission": "لا يوجد إذن للوصول إلى تطبيق الويب", + "noAccessResourcePermission": "لا يوجد إذن للوصول إلى هذا المورد", "noUserInputNode": "عقدة إدخال المستخدم مفقودة", "notPublishedYet": "التطبيق لم ينشر بعد", "openInExplore": "فتح في الاستكشاف", diff --git a/web/i18n/de-DE/app.json b/web/i18n/de-DE/app.json index 08d7b86f085..011c2e6ca39 100644 --- a/web/i18n/de-DE/app.json +++ b/web/i18n/de-DE/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Arbeitsablauf", "newAppFromTemplate.sidebar.Writing": "Schrift", "noAccessPermission": "Keine Berechtigung zum Zugriff auf die Webanwendung", + "noAccessResourcePermission": "Keine Berechtigung zum Zugriff auf diese Ressource", "noUserInputNode": "Fehlender Benutzereingabeknoten", "notPublishedYet": "App ist noch nicht veröffentlicht", "openInExplore": "In Explore öffnen", diff --git a/web/i18n/en-US/app.json b/web/i18n/en-US/app.json index d10dfebaf3a..5ca4edbfa39 100644 --- a/web/i18n/en-US/app.json +++ b/web/i18n/en-US/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Workflow", "newAppFromTemplate.sidebar.Writing": "Writing", "noAccessPermission": "No permission to access web app", + "noAccessResourcePermission": "No permission to access this resource", "noUserInputNode": "Missing user input node", "notPublishedYet": "App is not published yet", "openInExplore": "Open in Explore", diff --git a/web/i18n/es-ES/app.json b/web/i18n/es-ES/app.json index 2fd93664c4d..e89aea906ee 100644 --- a/web/i18n/es-ES/app.json +++ b/web/i18n/es-ES/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Flujo de trabajo", "newAppFromTemplate.sidebar.Writing": "Escritura", "noAccessPermission": "No se permite el acceso a la aplicación web", + "noAccessResourcePermission": "No tienes permiso para acceder a este recurso", "noUserInputNode": "Nodo de entrada de usuario faltante", "notPublishedYet": "La aplicación aún no está publicada", "openInExplore": "Abrir en Explorar", diff --git a/web/i18n/fa-IR/app.json b/web/i18n/fa-IR/app.json index c69a52adebe..0263f42ebdb 100644 --- a/web/i18n/fa-IR/app.json +++ b/web/i18n/fa-IR/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "گردش", "newAppFromTemplate.sidebar.Writing": "نوشتن", "noAccessPermission": "دسترسی به برنامه وب مجاز نیست", + "noAccessResourcePermission": "مجوز دسترسی به این منبع را ندارید", "noUserInputNode": "ورودی کاربر پیدا نشد", "notPublishedYet": "اپ هنوز منتشر نشده است", "openInExplore": "باز کردن در کاوش", diff --git a/web/i18n/fr-FR/app.json b/web/i18n/fr-FR/app.json index 58309a1dfbd..24ba5206f31 100644 --- a/web/i18n/fr-FR/app.json +++ b/web/i18n/fr-FR/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Flux de travail", "newAppFromTemplate.sidebar.Writing": "Écriture", "noAccessPermission": "Pas de permission d'accéder à l'application web", + "noAccessResourcePermission": "Aucune autorisation pour accéder à cette ressource", "noUserInputNode": "Nœud d'entrée utilisateur manquant", "notPublishedYet": "L'application n'est pas encore publiée", "openInExplore": "Ouvrir dans Explorer", diff --git a/web/i18n/hi-IN/app.json b/web/i18n/hi-IN/app.json index ea4b2f070d4..faf28b6ced3 100644 --- a/web/i18n/hi-IN/app.json +++ b/web/i18n/hi-IN/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "कार्यप्रवाह", "newAppFromTemplate.sidebar.Writing": "कृतियाँ", "noAccessPermission": "वेब एप्लिकेशन तक पहुँचने की अनुमति नहीं है", + "noAccessResourcePermission": "इस संसाधन तक पहुँचने की अनुमति नहीं है", "noUserInputNode": "उपयोगकर्ता इनपुट नोड गायब है", "notPublishedYet": "ऐप अभी प्रकाशित नहीं हुआ है", "openInExplore": "एक्सप्लोर में खोलें", diff --git a/web/i18n/id-ID/app.json b/web/i18n/id-ID/app.json index f1a27e2f9c1..1c8e21a9207 100644 --- a/web/i18n/id-ID/app.json +++ b/web/i18n/id-ID/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Alur Kerja", "newAppFromTemplate.sidebar.Writing": "Tulisan", "noAccessPermission": "Tidak ada izin untuk mengakses aplikasi web", + "noAccessResourcePermission": "Tidak ada izin untuk mengakses sumber daya ini", "noUserInputNode": "Node input pengguna hilang", "notPublishedYet": "Aplikasi belum diterbitkan", "openInExplore": "Buka di Jelajahi", diff --git a/web/i18n/it-IT/app.json b/web/i18n/it-IT/app.json index c35f6be9aae..785ed4ca848 100644 --- a/web/i18n/it-IT/app.json +++ b/web/i18n/it-IT/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Flusso di lavoro", "newAppFromTemplate.sidebar.Writing": "Scrittura", "noAccessPermission": "Nessun permesso per accedere all'app web", + "noAccessResourcePermission": "Nessuna autorizzazione per accedere a questa risorsa", "noUserInputNode": "Nodo di input utente mancante", "notPublishedYet": "L'app non è ancora pubblicata", "openInExplore": "Apri in Esplora", diff --git a/web/i18n/ja-JP/app.json b/web/i18n/ja-JP/app.json index d2f338a84de..600c75509ba 100644 --- a/web/i18n/ja-JP/app.json +++ b/web/i18n/ja-JP/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "ワークフロー", "newAppFromTemplate.sidebar.Writing": "ライティング", "noAccessPermission": "Web アプリにアクセス権限がありません", + "noAccessResourcePermission": "このリソースにアクセスする権限がありません", "noUserInputNode": "ユーザー入力ノードが見つかりません", "notPublishedYet": "アプリはまだ公開されていません", "openInExplore": "\"探索\" で開く", diff --git a/web/i18n/ko-KR/app.json b/web/i18n/ko-KR/app.json index a1995c21b46..c8e294277d7 100644 --- a/web/i18n/ko-KR/app.json +++ b/web/i18n/ko-KR/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "워크플로", "newAppFromTemplate.sidebar.Writing": "쓰기", "noAccessPermission": "웹 앱에 대한 접근 권한이 없습니다.", + "noAccessResourcePermission": "이 리소스에 액세스할 권한이 없습니다", "noUserInputNode": "사용자 입력 노드가 없습니다", "notPublishedYet": "앱이 아직 출시되지 않았습니다", "openInExplore": "Explore 에서 열기", diff --git a/web/i18n/nl-NL/app.json b/web/i18n/nl-NL/app.json index e62ead85e82..cbaa3641cf7 100644 --- a/web/i18n/nl-NL/app.json +++ b/web/i18n/nl-NL/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Workflow", "newAppFromTemplate.sidebar.Writing": "Writing", "noAccessPermission": "No permission to access web app", + "noAccessResourcePermission": "Geen toestemming om deze resource te openen", "noUserInputNode": "Missing user input node", "notPublishedYet": "App is not published yet", "openInExplore": "Open in Explore", diff --git a/web/i18n/pl-PL/app.json b/web/i18n/pl-PL/app.json index 58156671cf7..024f71be62e 100644 --- a/web/i18n/pl-PL/app.json +++ b/web/i18n/pl-PL/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Przepływ pracy", "newAppFromTemplate.sidebar.Writing": "Pismo", "noAccessPermission": "Brak uprawnień do dostępu do aplikacji internetowej", + "noAccessResourcePermission": "Brak uprawnień dostępu do tego zasobu", "noUserInputNode": "Brak węzła wejściowego użytkownika", "notPublishedYet": "Aplikacja nie została jeszcze opublikowana", "openInExplore": "Otwieranie w obszarze Eksploruj", diff --git a/web/i18n/pt-BR/app.json b/web/i18n/pt-BR/app.json index 21e2437d1d2..a558fbb5d48 100644 --- a/web/i18n/pt-BR/app.json +++ b/web/i18n/pt-BR/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Fluxo de trabalho", "newAppFromTemplate.sidebar.Writing": "Escrita", "noAccessPermission": "Sem permissão para acessar o aplicativo web", + "noAccessResourcePermission": "Sem permissão para acessar este recurso", "noUserInputNode": "Nodo de entrada do usuário ausente", "notPublishedYet": "O aplicativo ainda não foi publicado", "openInExplore": "Abrir no Explore", diff --git a/web/i18n/ro-RO/app.json b/web/i18n/ro-RO/app.json index a434a8e1a01..bbec39538c4 100644 --- a/web/i18n/ro-RO/app.json +++ b/web/i18n/ro-RO/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Flux de lucru", "newAppFromTemplate.sidebar.Writing": "Scriere", "noAccessPermission": "Nici o permisiune pentru a accesa aplicația web", + "noAccessResourcePermission": "Nu ai permisiunea de a accesa această resursă", "noUserInputNode": "Lipsă nod de intrare pentru utilizator", "notPublishedYet": "Aplicația nu este încă publicată", "openInExplore": "Deschide în Explorează", diff --git a/web/i18n/ru-RU/app.json b/web/i18n/ru-RU/app.json index 07ad6215a45..113f175ea95 100644 --- a/web/i18n/ru-RU/app.json +++ b/web/i18n/ru-RU/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Рабочий процесс", "newAppFromTemplate.sidebar.Writing": "Пишущий", "noAccessPermission": "Нет разрешения на доступ к веб-приложению", + "noAccessResourcePermission": "Нет разрешения на доступ к этому ресурсу", "noUserInputNode": "Отсутствует узел ввода пользователя", "notPublishedYet": "Приложение ещё не опубликовано", "openInExplore": "Открыть в разделе «Обзор»", diff --git a/web/i18n/sl-SI/app.json b/web/i18n/sl-SI/app.json index f4599524d71..f9b21e2169f 100644 --- a/web/i18n/sl-SI/app.json +++ b/web/i18n/sl-SI/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Potek dela", "newAppFromTemplate.sidebar.Writing": "Pisanje", "noAccessPermission": "Brez dovoljenja za dostop do spletne aplikacije", + "noAccessResourcePermission": "Ni dovoljenja za dostop do tega vira", "noUserInputNode": "Manjka vozel uporabniškega vnosa", "notPublishedYet": "Aplikacija še ni objavljena", "openInExplore": "Odpri v razišči", diff --git a/web/i18n/th-TH/app.json b/web/i18n/th-TH/app.json index a6f5034b80c..ad7df05fccc 100644 --- a/web/i18n/th-TH/app.json +++ b/web/i18n/th-TH/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "เวิร์กโฟลว์", "newAppFromTemplate.sidebar.Writing": "การเขียน", "noAccessPermission": "ไม่มีสิทธิ์เข้าถึงเว็บแอป", + "noAccessResourcePermission": "ไม่มีสิทธิ์เข้าถึงทรัพยากรนี้", "noUserInputNode": "ไม่มีโหนดป้อนข้อมูลผู้ใช้", "notPublishedYet": "แอปยังไม่ได้เผยแพร่", "openInExplore": "เปิดใน Explore", diff --git a/web/i18n/tr-TR/app.json b/web/i18n/tr-TR/app.json index ffb77396d3c..df83be5099d 100644 --- a/web/i18n/tr-TR/app.json +++ b/web/i18n/tr-TR/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "İş Akışı", "newAppFromTemplate.sidebar.Writing": "Yazı", "noAccessPermission": "Web uygulamasına erişim izni yok", + "noAccessResourcePermission": "Bu kaynağa erişim izniniz yok", "noUserInputNode": "Eksik kullanıcı girdi düğümü", "notPublishedYet": "Uygulama henüz yayımlanmadı", "openInExplore": "Keşfet'te Aç", diff --git a/web/i18n/uk-UA/app.json b/web/i18n/uk-UA/app.json index 2bdcb3b71bb..8e094f1a976 100644 --- a/web/i18n/uk-UA/app.json +++ b/web/i18n/uk-UA/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Робочий процес", "newAppFromTemplate.sidebar.Writing": "Написання", "noAccessPermission": "Немає дозволу на доступ до веб-додатку", + "noAccessResourcePermission": "Немає дозволу на доступ до цього ресурсу", "noUserInputNode": "Відсутній вузол введення користувача", "notPublishedYet": "Додаток ще не опублікований", "openInExplore": "Відкрити в Огляді", diff --git a/web/i18n/vi-VN/app.json b/web/i18n/vi-VN/app.json index 41ca574c10b..7f24485445a 100644 --- a/web/i18n/vi-VN/app.json +++ b/web/i18n/vi-VN/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "Quy trình làm việc", "newAppFromTemplate.sidebar.Writing": "Văn", "noAccessPermission": "Không được phép truy cập ứng dụng web", + "noAccessResourcePermission": "Không có quyền truy cập tài nguyên này", "noUserInputNode": "Thiếu nút nhập liệu của người dùng", "notPublishedYet": "Ứng dụng chưa được phát hành", "openInExplore": "Mở trong Khám phá", diff --git a/web/i18n/zh-Hans/app.json b/web/i18n/zh-Hans/app.json index 640b54e0987..7ccfc67174e 100644 --- a/web/i18n/zh-Hans/app.json +++ b/web/i18n/zh-Hans/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "工作流", "newAppFromTemplate.sidebar.Writing": "写作", "noAccessPermission": "没有权限访问 web 应用", + "noAccessResourcePermission": "当前无权限访问该资源", "noUserInputNode": "缺少用户输入节点", "notPublishedYet": "应用暂未发布", "openInExplore": "在“探索”中打开", diff --git a/web/i18n/zh-Hant/app.json b/web/i18n/zh-Hant/app.json index b989a327960..5d5dc6e26a1 100644 --- a/web/i18n/zh-Hant/app.json +++ b/web/i18n/zh-Hant/app.json @@ -226,6 +226,7 @@ "newAppFromTemplate.sidebar.Workflow": "工作流", "newAppFromTemplate.sidebar.Writing": "寫作", "noAccessPermission": "沒有權限訪問網絡應用程式", + "noAccessResourcePermission": "目前沒有權限訪問此資源", "noUserInputNode": "缺少使用者輸入節點", "notPublishedYet": "應用程式尚未發布", "openInExplore": "在“探索”中打開", diff --git a/web/utils/app-redirection.spec.ts b/web/utils/app-redirection.spec.ts index d3d9cb02b9f..944cc893e3a 100644 --- a/web/utils/app-redirection.spec.ts +++ b/web/utils/app-redirection.spec.ts @@ -71,7 +71,13 @@ describe('app-redirection', () => { it('returns access config path when app ACL can only configure access', () => { const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [AppACLPermission.AccessConfig] } - expect(getRedirectionPath(app)).toBe('/app/app-123/access-config') + expect(getRedirectionPath(app, { isRbacEnabled: true })).toBe('/app/app-123/access-config') + }) + + it('returns develop path for access config only apps when RBAC is disabled', () => { + const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [AppACLPermission.AccessConfig] } + + expect(getRedirectionPath(app, { isRbacEnabled: false })).toBe('/app/app-123/develop') }) it('returns overview path when app ACL can only monitor the app', () => { diff --git a/web/utils/permission.spec.ts b/web/utils/permission.spec.ts index 311796cc14c..b6f8447ffc5 100644 --- a/web/utils/permission.spec.ts +++ b/web/utils/permission.spec.ts @@ -9,6 +9,8 @@ import { getAppACLCapabilities, getDatasetACLCapabilities, hasEditPermissionForDataset, + hasOnlyAppPreviewPermission, + hasOnlyDatasetPreviewPermission, hasPermission, } from './permission' @@ -132,6 +134,32 @@ describe('permission', () => { }) }) + describe('hasOnlyAppPreviewPermission', () => { + it('should return true when app ACL contains only preview permission', () => { + expect(hasOnlyAppPreviewPermission([AppACLPermission.Preview])).toBe(true) + }) + + it('should return false when app ACL contains preview permission and another permission', () => { + expect(hasOnlyAppPreviewPermission([ + AppACLPermission.Preview, + AppACLPermission.ViewLayout, + ])).toBe(false) + }) + }) + + describe('hasOnlyDatasetPreviewPermission', () => { + it('should return true when dataset ACL contains only preview permission', () => { + expect(hasOnlyDatasetPreviewPermission([DatasetACLPermission.Preview])).toBe(true) + }) + + it('should return false when dataset ACL contains preview permission and another permission', () => { + expect(hasOnlyDatasetPreviewPermission([ + DatasetACLPermission.Preview, + DatasetACLPermission.Readonly, + ])).toBe(false) + }) + }) + describe('app maintainer capabilities', () => { it('grants all app ACL capabilities without injecting app ACL permission keys', () => { const permissionKeys: string[] = [] @@ -139,6 +167,7 @@ describe('permission', () => { currentUserId: 'user-1', resourceMaintainer: 'user-1', workspacePermissionKeys: ['app.create_and_management'], + isRbacEnabled: true, }) expect(capabilities.canViewLayout).toBe(true) @@ -163,6 +192,14 @@ describe('permission', () => { expect(capabilities.canEdit).toBe(false) expect(capabilities.canDelete).toBe(false) }) + + it('does not grant app access config when RBAC is disabled', () => { + const capabilities = getAppACLCapabilities([AppACLPermission.AccessConfig], { + isRbacEnabled: false, + }) + + expect(capabilities.canAccessConfig).toBe(false) + }) }) describe('dataset maintainer capabilities', () => { @@ -172,6 +209,7 @@ describe('permission', () => { currentUserId: 'user-1', resourceMaintainer: 'user-1', workspacePermissionKeys: ['dataset.create_and_management'], + isRbacEnabled: true, }) expect(capabilities.canReadonly).toBe(true) @@ -199,5 +237,13 @@ describe('permission', () => { expect(capabilities.canEdit).toBe(false) expect(capabilities.canDelete).toBe(false) }) + + it('does not grant dataset access config when RBAC is disabled', () => { + const capabilities = getDatasetACLCapabilities([DatasetACLPermission.AccessConfig], { + isRbacEnabled: false, + }) + + expect(capabilities.canAccessConfig).toBe(false) + }) }) }) diff --git a/web/utils/permission.ts b/web/utils/permission.ts index 733e58611ce..ddc3d7b2864 100644 --- a/web/utils/permission.ts +++ b/web/utils/permission.ts @@ -2,6 +2,7 @@ import type { PermissionKey } from '@/models/access-control' import { DatasetPermission } from '@/models/datasets' export const AppACLPermission = { + Preview: 'app.acl.preview', ViewLayout: 'app.acl.view_layout', TestAndRun: 'app.acl.test_and_run', Edit: 'app.acl.edit', @@ -13,6 +14,7 @@ export const AppACLPermission = { } as const export const DatasetACLPermission = { + Preview: 'dataset.acl.preview', Readonly: 'dataset.acl.readonly', Edit: 'dataset.acl.edit', ImportExportDSL: 'dataset.acl.import_export_dsl', @@ -36,6 +38,7 @@ export type ResourceMaintainerPermissionOptions = { currentUserId?: string | null resourceMaintainer?: string | null workspacePermissionKeys?: readonly PermissionKey[] | null + isRbacEnabled?: boolean } type AppACLCapabilities = { @@ -94,6 +97,14 @@ export const hasPermission = (permissionKeys: readonly PermissionKey[] | null | return permissionKeys.includes(singlePermissionKey) } +export const hasOnlyAppPreviewPermission = (permissionKeys: readonly PermissionKey[] | null | undefined) => { + return permissionKeys?.length === 1 && permissionKeys[0] === AppACLPermission.Preview +} + +export const hasOnlyDatasetPreviewPermission = (permissionKeys: readonly PermissionKey[] | null | undefined) => { + return permissionKeys?.length === 1 && permissionKeys[0] === DatasetACLPermission.Preview +} + const shouldGrantMaintainerPermissions = ( options: ResourceMaintainerPermissionOptions | undefined, createPermissionKey: PermissionKey, @@ -131,7 +142,7 @@ export const getAppACLCapabilities = ( canDelete: hasResourcePermission(permissionKeys, AppACLPermission.Delete, hasMaintainerPermissions), canReleaseAndVersion: hasResourcePermission(permissionKeys, AppACLPermission.ReleaseAndVersion, hasMaintainerPermissions), canMonitor: hasResourcePermission(permissionKeys, AppACLPermission.Monitor, hasMaintainerPermissions), - canAccessConfig: hasResourcePermission(permissionKeys, AppACLPermission.AccessConfig, hasMaintainerPermissions), + canAccessConfig: Boolean(options?.isRbacEnabled) && hasResourcePermission(permissionKeys, AppACLPermission.AccessConfig, hasMaintainerPermissions), } } @@ -152,6 +163,6 @@ export const getDatasetACLCapabilities = ( canDeleteFile: hasResourcePermission(permissionKeys, DatasetACLPermission.DeleteFile, hasMaintainerPermissions), canPipelineRelease: hasResourcePermission(permissionKeys, DatasetACLPermission.PipelineRelease, hasMaintainerPermissions), canDelete: hasResourcePermission(permissionKeys, DatasetACLPermission.Delete, hasMaintainerPermissions), - canAccessConfig: hasResourcePermission(permissionKeys, DatasetACLPermission.AccessConfig, hasMaintainerPermissions), + canAccessConfig: Boolean(options?.isRbacEnabled) && hasResourcePermission(permissionKeys, DatasetACLPermission.AccessConfig, hasMaintainerPermissions), } } From 7aa20d6d94f4df79a08ffd508f734f9027cbe382 Mon Sep 17 00:00:00 2001 From: L1nSn0w Date: Mon, 22 Jun 2026 15:34:47 +0800 Subject: [PATCH 14/35] fix(cli): apply --think filtering to workflow app outputs (#37736) --- .../app/_strategies/streaming-structured.ts | 16 ++++++- cli/src/commands/run/app/run.test.ts | 37 +++++++++++++++ .../commands/run/app/stream-handlers.test.ts | 31 +++++++++++++ cli/src/commands/run/app/stream-handlers.ts | 21 +++++++-- cli/src/sys/io/think-filter.test.ts | 46 ++++++++++++++++++- cli/src/sys/io/think-filter.ts | 22 +++++++++ cli/test/fixtures/dify-mock/scenarios.ts | 1 + cli/test/fixtures/dify-mock/server.ts | 7 +++ 8 files changed, 174 insertions(+), 7 deletions(-) diff --git a/cli/src/commands/run/app/_strategies/streaming-structured.ts b/cli/src/commands/run/app/_strategies/streaming-structured.ts index b6fedae2c41..3ed602a3410 100644 --- a/cli/src/commands/run/app/_strategies/streaming-structured.ts +++ b/cli/src/commands/run/app/_strategies/streaming-structured.ts @@ -1,14 +1,14 @@ import type { RunContext, RunStrategy } from './index' import type { SseEvent } from '@/http/sse' import { buildRunBody } from '@/api/app-run' -import { CHAT_MODES, chatConversationHint, newAppRunObject } from '@/commands/run/app/handlers' +import { CHAT_MODES, chatConversationHint, newAppRunObject, RUN_MODES } from '@/commands/run/app/handlers' import { renderHitlHint, renderHitlOutput } from '@/commands/run/app/hitl-render' import { collect, HitlPauseError } from '@/commands/run/app/sse-collector' import { formatted, stringifyOutput } from '@/framework/output' import { handle, unhandle } from '@/sys/index' import { colorEnabled, colorScheme } from '@/sys/io/color' import { startSpinner } from '@/sys/io/spinner' -import { extractThinkBlocks, stripThinkBlocks } from '@/sys/io/think-filter' +import { extractThinkBlocks, filterThinkInOutputs, stripThinkBlocks } from '@/sys/io/think-filter' async function* captureTaskId( iter: AsyncIterable, @@ -86,6 +86,18 @@ export class StreamingStructuredStrategy implements RunStrategy { processedResp = { ...processedResp, answer: stripThinkBlocks(processedResp.answer) } } } + else if (mode === RUN_MODES.Workflow) { + const data = processedResp.data + if (data !== null && typeof data === 'object' && 'outputs' in data) { + const raw = (data as { outputs: unknown }).outputs + if (raw !== null && typeof raw === 'object' && !Array.isArray(raw)) { + const { outputs, thinking } = filterThinkInOutputs(raw as Record, ctx.think) + if (ctx.think && thinking !== '') + deps.io.err.write(`${thinking}\n`) + processedResp = { ...processedResp, data: { ...(data as Record), outputs } } + } + } + } const respMode = typeof processedResp.mode === 'string' && processedResp.mode !== '' ? processedResp.mode : mode deps.io.out.write(stringifyOutput(formatted({ format, data: newAppRunObject(respMode, processedResp) }))) diff --git a/cli/src/commands/run/app/run.test.ts b/cli/src/commands/run/app/run.test.ts index 4fff2d02873..592241cac57 100644 --- a/cli/src/commands/run/app/run.test.ts +++ b/cli/src/commands/run/app/run.test.ts @@ -165,6 +165,43 @@ describe('runApp', () => { expect(parsed.data.status).toBe('succeeded') }) + it('workflow: strips from outputs by default', async () => { + mock.setScenario('workflow-think') + const io = bufferStreams() + const cache = await loadAppInfoCache({ store: getCache(CACHE_APP_INFO) }) + await runApp( + { appId: 'app-2', inputs: { x: '1' } }, + { active: active(), http: testHttpClient(mock.url, 'dfoa_test'), host: mock.url, io, cache }, + ) + expect(io.outBuf()).toBe('final answer\n') + expect(io.errBuf()).not.toContain('secret reasoning') + }) + + it('workflow --think: routes to stderr, clean stdout', async () => { + mock.setScenario('workflow-think') + const io = bufferStreams() + const cache = await loadAppInfoCache({ store: getCache(CACHE_APP_INFO) }) + await runApp( + { appId: 'app-2', inputs: { x: '1' }, think: true }, + { active: active(), http: testHttpClient(mock.url, 'dfoa_test'), host: mock.url, io, cache }, + ) + expect(io.outBuf()).toBe('final answer\n') + expect(io.errBuf()).toContain('secret reasoning') + }) + + it('--stream workflow -o json --think: strips outputs and routes thinking to stderr', async () => { + mock.setScenario('workflow-think') + const io = bufferStreams() + const cache = await loadAppInfoCache({ store: getCache(CACHE_APP_INFO) }) + await runApp( + { appId: 'app-2', inputs: { x: '1' }, stream: true, format: 'json', think: true }, + { active: active(), http: testHttpClient(mock.url, 'dfoa_test'), host: mock.url, io, cache }, + ) + const parsed = JSON.parse(io.outBuf()) as { data: { outputs: { result: string } } } + expect(parsed.data.outputs.result).toBe('final answer') + expect(io.errBuf()).toContain('secret reasoning') + }) + it('stream-error scenario: error event surfaces typed BaseError', async () => { mock.setScenario('stream-error') const io = bufferStreams() diff --git a/cli/src/commands/run/app/stream-handlers.test.ts b/cli/src/commands/run/app/stream-handlers.test.ts index 885b059abf2..bab45144a01 100644 --- a/cli/src/commands/run/app/stream-handlers.test.ts +++ b/cli/src/commands/run/app/stream-handlers.test.ts @@ -74,6 +74,37 @@ describe('streamPrinterFor — workflow', () => { }) }) +describe('streamPrinterFor — workflow think filtering', () => { + it('think: false (default) strips from string outputs, nothing to stderr', () => { + const sp = streamPrinterFor('workflow') + const cap = captures() + sp.onEvent(cap.out, cap.err, ev('workflow_finished', { data: { outputs: { text: 'hidden\nresult' } } })) + sp.onEnd(cap.out, cap.err) + const parsed = JSON.parse(cap.outBuf().trim()) as { text: string } + expect(parsed.text).toBe('result') + expect(cap.errBuf()).toBe('') + }) + + it('think: true strips from string outputs and routes thinking to stderr', () => { + const sp = streamPrinterFor('workflow', true) + const cap = captures() + sp.onEvent(cap.out, cap.err, ev('workflow_finished', { data: { outputs: { text: 'reasoning\nresult' } } })) + sp.onEnd(cap.out, cap.err) + const parsed = JSON.parse(cap.outBuf().trim()) as { text: string } + expect(parsed.text).toBe('result') + expect(cap.errBuf()).toContain('') + expect(cap.errBuf()).toContain('reasoning') + }) + + it('array outputs pass through unchanged (not reshaped into an object)', () => { + const sp = streamPrinterFor('workflow', true) + const cap = captures() + sp.onEvent(cap.out, cap.err, ev('workflow_finished', { data: { outputs: ['a', 'b'] } })) + sp.onEnd(cap.out, cap.err) + expect(cap.outBuf().trim()).toBe('["a","b"]') + }) +}) + describe('streamPrinterFor — unknown mode', () => { it('throws', () => { expect(() => streamPrinterFor('whatever')).toThrow() diff --git a/cli/src/commands/run/app/stream-handlers.ts b/cli/src/commands/run/app/stream-handlers.ts index 355813d82b9..1955d5996bf 100644 --- a/cli/src/commands/run/app/stream-handlers.ts +++ b/cli/src/commands/run/app/stream-handlers.ts @@ -4,7 +4,7 @@ import type { SseEvent } from '@/http/sse' import { newError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' import { colorEnabled, colorScheme } from '@/sys/io/color' -import { ThinkChunkFilter } from '@/sys/io/think-filter' +import { filterThinkInOutputs, ThinkChunkFilter } from '@/sys/io/think-filter' import { RUN_MODES } from './handlers' import { HitlPauseError } from './sse-collector' @@ -106,6 +106,11 @@ class CompletionStreamPrinter implements StreamPrinter { class WorkflowStreamPrinter implements StreamPrinter { private final: Record | undefined + private readonly think: boolean + constructor(think: boolean) { + this.think = think + } + onEvent(_out: NodeJS.WritableStream, errOut: NodeJS.WritableStream, ev: SseEvent): void { if (handleCommonEvents(ev)) return @@ -132,12 +137,20 @@ class WorkflowStreamPrinter implements StreamPrinter { } } - onEnd(out: NodeJS.WritableStream): void { + onEnd(out: NodeJS.WritableStream, errOut: NodeJS.WritableStream): void { if (this.final === undefined) return const data = this.final.data if (data !== null && typeof data === 'object' && 'outputs' in data) { - out.write(`${JSON.stringify((data as { outputs: unknown }).outputs)}\n`) + const raw = (data as { outputs: unknown }).outputs + if (raw !== null && typeof raw === 'object' && !Array.isArray(raw)) { + const { outputs, thinking } = filterThinkInOutputs(raw as Record, this.think) + if (this.think && thinking !== '') + errOut.write(`${thinking}\n`) + out.write(`${JSON.stringify(outputs)}\n`) + return + } + out.write(`${JSON.stringify(raw)}\n`) return } out.write(`${JSON.stringify(this.final)}\n`) @@ -149,7 +162,7 @@ const FACTORIES: Record StreamPrinte [RUN_MODES.AdvancedChat]: (think, isTTY) => new ChatStreamPrinter(think, isTTY), [RUN_MODES.AgentChat]: (think, isTTY) => new ChatStreamPrinter(think, isTTY), [RUN_MODES.Completion]: (think, _isTTY) => new CompletionStreamPrinter(think), - [RUN_MODES.Workflow]: (_think, _isTTY) => new WorkflowStreamPrinter(), + [RUN_MODES.Workflow]: (think, _isTTY) => new WorkflowStreamPrinter(think), } export function streamPrinterFor(mode: string, think = false, isTTY = false): StreamPrinter { diff --git a/cli/src/sys/io/think-filter.test.ts b/cli/src/sys/io/think-filter.test.ts index 72ec036abdd..db07d65cd90 100644 --- a/cli/src/sys/io/think-filter.test.ts +++ b/cli/src/sys/io/think-filter.test.ts @@ -1,7 +1,7 @@ import { Buffer } from 'node:buffer' import { PassThrough } from 'node:stream' import { describe, expect, it } from 'vitest' -import { extractThinkBlocks, stripThinkBlocks, ThinkChunkFilter } from './think-filter' +import { extractThinkBlocks, filterThinkInOutputs, stripThinkBlocks, ThinkChunkFilter } from './think-filter' function captures() { const out = new PassThrough() @@ -63,6 +63,50 @@ describe('extractThinkBlocks', () => { }) }) +// --- workflow outputs helper --- + +describe('filterThinkInOutputs', () => { + it('no think block — outputs unchanged, thinking empty', () => { + const r = filterThinkInOutputs({ text: 'hello' }, true) + expect(r.outputs).toEqual({ text: 'hello' }) + expect(r.thinking).toBe('') + }) + + it('showThink: false — strips from string field, thinking empty', () => { + const r = filterThinkInOutputs({ text: 'reasoning\nanswer' }, false) + expect(r.outputs).toEqual({ text: 'answer' }) + expect(r.thinking).toBe('') + }) + + it('showThink: true — strips from string field, captures thinking', () => { + const r = filterThinkInOutputs({ text: 'step 1\nfinal' }, true) + expect(r.outputs).toEqual({ text: 'final' }) + expect(r.thinking).toBe('\nstep 1\n') + }) + + it('multiple string fields — thinking joined with separator', () => { + const r = filterThinkInOutputs( + { a: 'x\nfoo', b: 'y\nbar' }, + true, + ) + expect(r.outputs).toEqual({ a: 'foo', b: 'bar' }) + expect(r.thinking).toBe('\nx\n\n---\n\ny\n') + }) + + it('non-string values pass through untouched', () => { + const outputs = { n: 42, flag: true, nested: { k: 'v\nx' }, arr: ['a'], nil: null } + const r = filterThinkInOutputs(outputs, true) + expect(r.outputs).toEqual(outputs) + expect(r.thinking).toBe('') + }) + + it('empty outputs — empty result', () => { + const r = filterThinkInOutputs({}, true) + expect(r.outputs).toEqual({}) + expect(r.thinking).toBe('') + }) +}) + // --- streaming chunk filter --- describe('ThinkChunkFilter — showThink: false (strip)', () => { diff --git a/cli/src/sys/io/think-filter.ts b/cli/src/sys/io/think-filter.ts index 401ea728f24..88c2fb7c717 100644 --- a/cli/src/sys/io/think-filter.ts +++ b/cli/src/sys/io/think-filter.ts @@ -14,6 +14,28 @@ export function extractThinkBlocks(s: string): { clean: string, thinking: string return { clean, thinking: parts.join('\n---\n') } } +// Workflow outputs carry their answer text in top-level string fields rather than +// a single `answer`, so think filtering navigates the outputs object. Nested +// strings (inside arrays/objects) are left untouched. +export function filterThinkInOutputs( + outputs: Record, + showThink: boolean, +): { outputs: Record, thinking: string } { + const thoughts: string[] = [] + const clean: Record = {} + for (const [key, value] of Object.entries(outputs)) { + if (typeof value !== 'string') { + clean[key] = value + continue + } + const extracted = extractThinkBlocks(value) + clean[key] = extracted.clean + if (showThink && extracted.thinking !== '') + thoughts.push(extracted.thinking) + } + return { outputs: clean, thinking: thoughts.join('\n---\n') } +} + function splitAtPotentialTag(s: string, tag: string): [string, string] { const maxHold = tag.length - 1 for (let len = Math.min(maxHold, s.length); len > 0; len--) { diff --git a/cli/test/fixtures/dify-mock/scenarios.ts b/cli/test/fixtures/dify-mock/scenarios.ts index 8ee839381c5..221ccbb6b81 100644 --- a/cli/test/fixtures/dify-mock/scenarios.ts +++ b/cli/test/fixtures/dify-mock/scenarios.ts @@ -14,6 +14,7 @@ export type Scenario | 'server-version-empty' | 'server-version-unsupported' | 'run-422-stale' + | 'workflow-think' | 'import-pending' | 'import-failed' diff --git a/cli/test/fixtures/dify-mock/server.ts b/cli/test/fixtures/dify-mock/server.ts index 96edc96f9ba..c9d6fe7a865 100644 --- a/cli/test/fixtures/dify-mock/server.ts +++ b/cli/test/fixtures/dify-mock/server.ts @@ -337,6 +337,13 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono { if (scenario === 'hitl-pause') { return new Response(hitlPauseResponse(), { status: 200, headers: { 'content-type': 'text/event-stream' } }) } + if (scenario === 'workflow-think') { + const thinkSse = sseChunks([ + { event: 'workflow_started', data: { id: 'wf-run-1', workflow_id: 'wf-1' } }, + { event: 'workflow_finished', data: { id: 'wf-run-1', workflow_id: 'wf-1', data: { id: 'wf-run-1', status: 'succeeded', outputs: { result: 'secret reasoning\nfinal answer' } } } }, + ]) + return new Response(thinkSse, { status: 200, headers: { 'content-type': 'text/event-stream' } }) + } const sse = streamingRunResponse(app.mode, query, isAgent) return new Response(sse, { status: 200, headers: { 'content-type': 'text/event-stream' } }) }) From 1d74bff311184abec840677d9d84f05ec4276ec3 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:42:13 -0700 Subject: [PATCH 15/35] fix(cli): make auth devices revoke --yes a real flag (#37740) --- .../auth/devices/_shared/devices.test.ts | 37 +++++++++++++++++++ .../commands/auth/devices/_shared/devices.ts | 12 ++++++ cli/src/commands/auth/devices/revoke/index.ts | 2 +- cli/src/commands/delete/member/run.ts | 14 +------ cli/src/sys/io/prompt.ts | 12 ++++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cli/src/commands/auth/devices/_shared/devices.test.ts b/cli/src/commands/auth/devices/_shared/devices.test.ts index fb510ef1af1..0b64ac8b14e 100644 --- a/cli/src/commands/auth/devices/_shared/devices.test.ts +++ b/cli/src/commands/auth/devices/_shared/devices.test.ts @@ -146,6 +146,43 @@ describe('runDevicesRevoke', () => { expect(saved?.hosts[mock.url]).toBeUndefined() }) + it('TTY without --yes: prompts and aborts on decline (no revoke)', async () => { + const base = bufferStreams('n\n') + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await expect(runDevicesRevoke({ io, reg, active, store, http, all: true })) + .rejects + .toThrow(/aborted by user/) + expect(base.errBuf()).toContain('Revoke 2 session(s)? [y/N]') + expect(base.outBuf()).not.toContain('Revoked') + }) + + it('TTY without --yes: proceeds on accept', async () => { + const base = bufferStreams('y\n') + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await runDevicesRevoke({ io, reg, active, store, http, target: 'tok-2', all: false }) + expect(base.outBuf()).toContain('Revoked 1 session(s)') + }) + + it('TTY with --yes: skips prompt entirely', async () => { + const base = bufferStreams() + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await runDevicesRevoke({ io, reg, active, store, http, target: 'tok-2', all: false, yes: true }) + expect(base.errBuf()).not.toContain('[y/N]') + expect(base.outBuf()).toContain('Revoked 1 session(s)') + }) + it('no target + no --all: throws UsageMissingArg', async () => { const io = bufferStreams() const store = new MemStore() diff --git a/cli/src/commands/auth/devices/_shared/devices.ts b/cli/src/commands/auth/devices/_shared/devices.ts index 15f17e0a3e3..b328e59f93f 100644 --- a/cli/src/commands/auth/devices/_shared/devices.ts +++ b/cli/src/commands/auth/devices/_shared/devices.ts @@ -8,6 +8,7 @@ import { BaseError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' import { LIMIT_DEFAULT, LIMIT_MAX, parseLimit } from '@/limit/limit' import { colorEnabled, colorScheme } from '@/sys/io/color' +import { promptConfirm } from '@/sys/io/prompt' import { runWithSpinner } from '@/sys/io/spinner' export type DevicesListOptions = { @@ -96,6 +97,17 @@ export async function runDevicesRevoke(opts: DevicesRevokeOptions): Promise { diff --git a/cli/src/commands/delete/member/run.ts b/cli/src/commands/delete/member/run.ts index 07ec3ad7720..f11e3ee4b93 100644 --- a/cli/src/commands/delete/member/run.ts +++ b/cli/src/commands/delete/member/run.ts @@ -1,11 +1,11 @@ import type { ActiveContext } from '@/auth/hosts' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' -import * as readline from 'node:readline' import { MembersClient } from '@/api/members' import { BaseError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' import { colorEnabled, colorScheme } from '@/sys/io/color' +import { promptConfirm } from '@/sys/io/prompt' import { runWithSpinner } from '@/sys/io/spinner' import { nullStreams } from '@/sys/io/streams' import { resolveWorkspaceId } from '@/workspace/resolver' @@ -76,15 +76,3 @@ export async function runDeleteMember( workspaceId: wsId, } } - -async function promptConfirm(io: IOStreams, message: string): Promise { - io.err.write(message) - const rl = readline.createInterface({ input: io.in, output: io.err, terminal: false }) - try { - const line: string = await new Promise(resolve => rl.once('line', resolve)) - return line.trim().toLowerCase() === 'y' - } - finally { - rl.close() - } -} diff --git a/cli/src/sys/io/prompt.ts b/cli/src/sys/io/prompt.ts index d5cc2498b96..15b3d95eb5e 100644 --- a/cli/src/sys/io/prompt.ts +++ b/cli/src/sys/io/prompt.ts @@ -33,6 +33,18 @@ function normalize(raw: string, opts: Pick, 'default' return trimmed } +export async function promptConfirm(io: IOStreams, message: string): Promise { + io.err.write(message) + const rl = readline.createInterface({ input: io.in, output: io.err, terminal: false }) + try { + const line = await new Promise(resolve => rl.once('line', resolve)) + return line.trim().toLowerCase() === 'y' + } + finally { + rl.close() + } +} + export async function promptText(opts: PromptTextOptions): Promise { const prompt = buildPromptLine(opts) const cs = colorScheme(colorEnabled(opts.io.isErrTTY)) From 084f1228142ff119b760c2630472e392d79b5462 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:46:59 -0700 Subject: [PATCH 16/35] refactor(openapi/cli): split app usage-face from studio-app build-face (#37641) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/openapi/__init__.py | 6 +- api/controllers/openapi/_models.py | 13 +- api/controllers/openapi/apps.py | 92 ++++------ .../openapi/apps_permitted_external.py | 23 ++- api/openapi/markdown/openapi-openapi.md | 32 ++-- .../openapi/test_app_describe_builder.py | 73 ++++++++ .../openapi/test_app_list_query.py | 11 +- .../openapi/test_pagination_envelope.py | 10 +- cli/scripts/generate-command-tree.test.ts | 11 ++ cli/scripts/generate-command-tree.ts | 15 +- cli/src/api/app-meta.ts | 6 +- cli/src/api/app-reader.test.ts | 30 ++++ cli/src/api/app-reader.ts | 35 ++++ cli/src/api/apps.test.ts | 6 +- cli/src/api/apps.ts | 12 +- cli/src/api/permitted-external-apps.test.ts | 27 +++ cli/src/api/permitted-external-apps.ts | 34 ++++ cli/src/cache/app-info.test.ts | 2 - cli/src/commands/agent-guides.test.ts | 4 + cli/src/commands/describe/app/handlers.ts | 10 +- cli/src/commands/describe/app/run.test.ts | 14 +- cli/src/commands/describe/app/run.ts | 4 +- cli/src/commands/export/studio-app/guide.ts | 12 ++ .../export/{app => studio-app}/index.ts | 19 +- .../export/{app => studio-app}/run.test.ts | 0 .../export/{app => studio-app}/run.ts | 5 +- cli/src/commands/get/app/handlers.ts | 10 +- cli/src/commands/get/app/index.ts | 2 - cli/src/commands/get/app/run.test.ts | 43 +++-- cli/src/commands/get/app/run.ts | 26 +-- cli/src/commands/import/studio-app/guide.ts | 17 ++ .../import/{app => studio-app}/index.ts | 19 +- .../import/{app => studio-app}/run.test.ts | 0 .../import/{app => studio-app}/run.ts | 0 cli/src/commands/resume/app/run.test.ts | 66 +++++++ cli/src/commands/resume/app/run.ts | 42 +---- cli/src/commands/run/app/input-flags.ts | 42 +++++ cli/src/commands/run/app/run.test.ts | 34 +++- cli/src/commands/run/app/run.ts | 42 +---- cli/src/commands/tree.generated.ts | 8 +- cli/src/help/topics.ts | 13 ++ cli/src/http/error-mapper.test.ts | 19 ++ cli/src/http/error-mapper.ts | 8 + cli/src/http/orpc.test.ts | 4 +- cli/src/types/app-meta.test.ts | 2 - cli/test/e2e/setup/global-setup.ts | 6 +- .../suites/agent/agent-skill-workflow.e2e.ts | 17 +- cli/test/e2e/suites/auth/whoami.e2e.ts | 12 +- .../e2e/suites/discovery/describe-app.e2e.ts | 27 +-- .../discovery/get-app-all-workspaces.e2e.ts | 14 +- .../e2e/suites/discovery/get-app-list.e2e.ts | 127 +------------- .../suites/discovery/get-app-single.e2e.ts | 13 +- ...rt-app.e2e.ts => export-studio-app.e2e.ts} | 38 ++-- .../error-handling/error-messages.e2e.ts | 18 +- .../e2e/suites/output/table-output.e2e.ts | 40 +---- cli/test/fixtures/dify-mock/server.ts | 30 +++- .../generated/api/openapi/orpc.gen.ts | 42 ++++- .../generated/api/openapi/types.gen.ts | 40 +++-- .../generated/api/openapi/zod.gen.ts | 162 +++++++++--------- 59 files changed, 878 insertions(+), 611 deletions(-) create mode 100644 api/tests/unit_tests/controllers/openapi/test_app_describe_builder.py create mode 100644 cli/src/api/app-reader.test.ts create mode 100644 cli/src/api/app-reader.ts create mode 100644 cli/src/api/permitted-external-apps.test.ts create mode 100644 cli/src/api/permitted-external-apps.ts create mode 100644 cli/src/commands/export/studio-app/guide.ts rename cli/src/commands/export/{app => studio-app}/index.ts (71%) rename cli/src/commands/export/{app => studio-app}/run.test.ts (100%) rename cli/src/commands/export/{app => studio-app}/run.ts (89%) create mode 100644 cli/src/commands/import/studio-app/guide.ts rename cli/src/commands/import/{app => studio-app}/index.ts (80%) rename cli/src/commands/import/{app => studio-app}/run.test.ts (100%) rename cli/src/commands/import/{app => studio-app}/run.ts (100%) create mode 100644 cli/src/commands/resume/app/run.test.ts create mode 100644 cli/src/commands/run/app/input-flags.ts rename cli/test/e2e/suites/dsl/{export-app.e2e.ts => export-studio-app.e2e.ts} (82%) diff --git a/api/controllers/openapi/__init__.py b/api/controllers/openapi/__init__.py index e8406ea00cb..c11019cf627 100644 --- a/api/controllers/openapi/__init__.py +++ b/api/controllers/openapi/__init__.py @@ -31,7 +31,7 @@ from controllers.openapi._models import ( AppDslExportQuery, AppDslExportResponse, AppDslImportPayload, - AppInfoResponse, + AppInfo, AppListQuery, AppListResponse, AppListRow, @@ -62,7 +62,6 @@ from controllers.openapi._models import ( SessionListQuery, SessionListResponse, SessionRow, - TagItem, TaskStopResponse, UsageInfo, WorkflowRunData, @@ -96,12 +95,11 @@ register_response_schema_models( openapi_ns, ErrorBody, EventStreamResponse, - TagItem, UsageInfo, MessageMetadata, AppListRow, AppListResponse, - AppInfoResponse, + AppInfo, AppDescribeInfo, AppDescribeResponse, AppDslExportResponse, diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py index 7c225c85f65..e846db3ea75 100644 --- a/api/controllers/openapi/_models.py +++ b/api/controllers/openapi/_models.py @@ -38,18 +38,12 @@ class PaginationEnvelope[T](BaseModel): return cls(page=page, limit=limit, total=total, has_more=page * limit < total, data=items) -class TagItem(BaseModel): - name: str - - class AppListRow(BaseModel): id: str name: str description: str | None = None mode: AppMode - tags: list[TagItem] = [] updated_at: str | None = None - created_by_name: str | None = None workspace_id: str | None = None workspace_name: str | None = None @@ -70,16 +64,14 @@ class PermittedExternalAppsListResponse(BaseModel): data: list[AppListRow] -class AppInfoResponse(BaseModel): +class AppInfo(BaseModel): id: str name: str description: str | None = None mode: str - author: str | None = None - tags: list[TagItem] = [] -class AppDescribeInfo(AppInfoResponse): +class AppDescribeInfo(AppInfo): updated_at: str | None = None service_api_enabled: bool is_agent: bool = False @@ -294,7 +286,6 @@ class AppListQuery(BaseModel): limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT) mode: AppMode | None = None name: str | None = Field(None, max_length=200) - tag: str | None = Field(None, max_length=100) class AppRunRequest(BaseModel): diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index c4796313c0b..98ef91a3a35 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -19,7 +19,6 @@ from controllers.openapi._models import ( AppListQuery, AppListResponse, AppListRow, - TagItem, ) from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData @@ -28,9 +27,9 @@ from core.app.app_config.common.parameters_mapping import get_parameters_from_fe from extensions.ext_database import db from libs.oauth_bearer import Scope, TokenType from models import App +from models.model import AppMode from services.account_service import TenantService from services.app_service import AppListParams, AppService -from services.tag_service import TagService _ALLOWED_DESCRIBE_FIELDS: frozenset[str] = frozenset({"info", "parameters", "input_schema"}) @@ -84,6 +83,42 @@ def parameters_payload(app: App) -> dict: return Parameters.model_validate(parameters).model_dump(mode="json") +def build_app_describe_response(app: App, fields: set[str] | None) -> AppDescribeResponse: + """Public projection of an app (name / params / input schema) — never internal config.""" + want_info = fields is None or "info" in fields + want_params = fields is None or "parameters" in fields + want_schema = fields is None or "input_schema" in fields + + info = ( + AppDescribeInfo( + id=str(app.id), + name=app.name, + mode=app.mode, + description=app.description, + updated_at=app.updated_at.isoformat() if app.updated_at else None, + service_api_enabled=bool(app.enable_api), + is_agent=app.mode in (AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT), + ) + if want_info + else None + ) + + parameters: dict[str, Any] | None = None + input_schema: dict[str, Any] | None = None + if want_params: + try: + parameters = parameters_payload(app) + except AppUnavailableError: + parameters = dict(_EMPTY_PARAMETERS) + if want_schema: + try: + input_schema = build_input_schema(app) + except AppUnavailableError: + input_schema = dict(EMPTY_INPUT_SCHEMA) + + return AppDescribeResponse(info=info, parameters=parameters, input_schema=input_schema) + + @openapi_ns.route("/apps//describe") class AppDescribeApi(AppReadResource): @auth_router.guard(scope=Scope.APPS_READ, allowed_token_types=frozenset({TokenType.OAUTH_ACCOUNT})) @@ -92,46 +127,7 @@ class AppDescribeApi(AppReadResource): def get(self, app_id: str, *, auth_data: AuthData, query: AppDescribeQuery): # describe is UUID-only (workspace_id query param dropped in #37212). app = self._load(app_id) - - requested = query.fields - want_info = requested is None or "info" in requested - want_params = requested is None or "parameters" in requested - want_schema = requested is None or "input_schema" in requested - - info = ( - AppDescribeInfo( - id=str(app.id), - name=app.name, - mode=app.mode, - description=app.description, - tags=[TagItem(name=t.name) for t in app.tags], - author=app.author_name, - updated_at=app.updated_at.isoformat() if app.updated_at else None, - service_api_enabled=bool(app.enable_api), - is_agent=app.mode in ("agent-chat", "advanced-chat"), - ) - if want_info - else None - ) - - parameters: dict[str, Any] | None = None - input_schema: dict[str, Any] | None = None - if want_params: - try: - parameters = parameters_payload(app) - except AppUnavailableError: - parameters = dict(_EMPTY_PARAMETERS) - if want_schema: - try: - input_schema = build_input_schema(app) - except AppUnavailableError: - input_schema = dict(EMPTY_INPUT_SCHEMA) - - return AppDescribeResponse( - info=info, - parameters=parameters, - input_schema=input_schema, - ) + return build_app_describe_response(app, query.fields) @openapi_ns.route("/apps") @@ -163,28 +159,18 @@ class AppListApi(Resource): name=app.name, description=app.description, mode=app.mode, - tags=[TagItem(name=t.name) for t in app.tags], updated_at=app.updated_at.isoformat() if app.updated_at else None, - created_by_name=getattr(app, "author_name", None), workspace_id=str(workspace_id), workspace_name=tenant_name, ) env = AppListResponse(page=1, limit=1, total=1, has_more=False, data=[item]) return env - tag_ids: list[str] | None = None - if query.tag: - tags = TagService.get_tag_by_tag_name("app", workspace_id, query.tag, db.session) - if not tags: - return empty - tag_ids = [tag.id for tag in tags] - params = AppListParams( page=query.page, limit=query.limit, mode=query.mode.value if query.mode else "all", # type:ignore name=query.name, - tag_ids=tag_ids, status="normal", # Visibility gate pushed into the query — pagination.total stays # consistent across pages because invisible rows never count. @@ -205,9 +191,7 @@ class AppListApi(Resource): name=r.name, description=r.description, mode=r.mode, - tags=[TagItem(name=t.name) for t in r.tags], updated_at=r.updated_at.isoformat() if r.updated_at else None, - created_by_name=getattr(r, "author_name", None), workspace_id=str(workspace_id), workspace_name=tenant_name, ) diff --git a/api/controllers/openapi/apps_permitted_external.py b/api/controllers/openapi/apps_permitted_external.py index 0e889a2951c..9bc400e5cc7 100644 --- a/api/controllers/openapi/apps_permitted_external.py +++ b/api/controllers/openapi/apps_permitted_external.py @@ -8,14 +8,18 @@ EE blueprint chain so this module is unreachable there. from __future__ import annotations from flask_restx import Resource +from werkzeug.exceptions import NotFound from controllers.openapi import openapi_ns from controllers.openapi._contract import accepts, returns from controllers.openapi._models import ( + AppDescribeQuery, + AppDescribeResponse, AppListRow, PermittedExternalAppsListQuery, PermittedExternalAppsListResponse, ) +from controllers.openapi.apps import build_app_describe_response from controllers.openapi.auth.composition import auth_router from controllers.openapi.auth.data import AuthData, Edition from extensions.ext_database import db @@ -67,9 +71,7 @@ class PermittedExternalAppsListApi(Resource): name=app.name, description=app.description, mode=app.mode, - tags=[], # tenant-scoped; not surfaced cross-tenant updated_at=app.updated_at.isoformat() if app.updated_at else None, - created_by_name=None, # cross-tenant author leak prevention workspace_id=str(app.tenant_id), workspace_name=tenant.name if tenant else None, ) @@ -82,3 +84,20 @@ class PermittedExternalAppsListApi(Resource): data=items, ) return env + + +@openapi_ns.route("/permitted-external-apps//describe") +class PermittedExternalAppDescribeApi(Resource): + @auth_router.guard( + scope=Scope.APPS_READ_PERMITTED_EXTERNAL, + allowed_token_types=frozenset({TokenType.OAUTH_EXTERNAL_SSO}), + edition=frozenset({Edition.EE}), + ) + @returns(200, AppDescribeResponse, description="Permitted external app description") + @accepts(query=AppDescribeQuery) + def get(self, app_id: str, *, auth_data: AuthData, query: AppDescribeQuery): + # App already loaded and ACL-checked by the external_sso pipeline; project it. + app = auth_data.app + if app is None: + raise NotFound("app not found") + return build_app_describe_response(app, query.fields) diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index ce0150e8e88..bd93557edcf 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -83,7 +83,6 @@ User-scoped operations | mode | query | | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" | | name | query | | No | string | | page | query | | No | integer,
**Default:** 1 | -| tag | query | | No | string | | workspace_id | query | | Yes | string | #### Responses @@ -331,6 +330,22 @@ Upload a file to use as an input variable when running the app | 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| | default | Error | **application/json**: [ErrorBody](#errorbody)
| +### [GET] /permitted-external-apps/{app_id}/describe +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| fields | query | | No | string | +| app_id | path | | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Permitted external app description | **application/json**: [AppDescribeResponse](#appdescriberesponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| + ### [GET] /workspaces #### Responses @@ -507,14 +522,12 @@ Upload a file to use as an input variable when running the app | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| author | string | | No | | description | string | | No | | id | string | | Yes | | is_agent | boolean | | No | | mode | string | | Yes | | name | string | | Yes | | service_api_enabled | boolean | | Yes | -| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | | updated_at | string | | No | #### AppDescribeQuery @@ -568,16 +581,14 @@ Request body for POST /workspaces//apps/imports. | yaml_content | string | Inline YAML DSL string (required when mode is yaml-content) | No | | yaml_url | string | Remote URL to fetch YAML from (required when mode is yaml-url) | No | -#### AppInfoResponse +#### AppInfo | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| author | string | | No | | description | string | | No | | id | string | | Yes | | mode | string | | Yes | | name | string | | Yes | -| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | #### AppListQuery @@ -589,7 +600,6 @@ mode is a closed enum. | mode | [AppMode](#appmode) | | No | | name | string | | No | | page | integer,
**Default:** 1 | | No | -| tag | string | | No | | workspace_id | string | | Yes | #### AppListResponse @@ -606,12 +616,10 @@ mode is a closed enum. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| created_by_name | string | | No | | description | string | | No | | id | string | | Yes | | mode | [AppMode](#appmode) | | Yes | | name | string | | Yes | -| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | | updated_at | string | | No | | workspace_id | string | | No | | workspace_name | string | | No | @@ -982,12 +990,6 @@ Pagination for GET /account/sessions. Strict (extra='forbid'). | last_used_at | string | | No | | prefix | string | | Yes | -#### TagItem - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| name | string | | Yes | - #### TaskStopResponse 200 body for POST /apps//tasks//stop. The handler always returns diff --git a/api/tests/unit_tests/controllers/openapi/test_app_describe_builder.py b/api/tests/unit_tests/controllers/openapi/test_app_describe_builder.py new file mode 100644 index 00000000000..708a0e59865 --- /dev/null +++ b/api/tests/unit_tests/controllers/openapi/test_app_describe_builder.py @@ -0,0 +1,73 @@ +from types import SimpleNamespace + +from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA +from controllers.openapi.apps import _EMPTY_PARAMETERS, build_app_describe_response +from controllers.service_api.app.error import AppUnavailableError + + +class _FakeApp(SimpleNamespace): + pass + + +def _app() -> _FakeApp: + from datetime import datetime + + return _FakeApp( + id="11111111-1111-1111-1111-111111111111", + name="Demo", + mode="chat", + description="d", + tags=[], + author_name="me", + updated_at=datetime(2026, 1, 1), + enable_api=True, + ) + + +def test_fields_none_returns_all_blocks(monkeypatch): + monkeypatch.setattr("controllers.openapi.apps.parameters_payload", lambda app: {"k": "v"}) + monkeypatch.setattr("controllers.openapi.apps.build_input_schema", lambda app: {"s": 1}) + resp = build_app_describe_response(_app(), None) + assert resp.info is not None + assert resp.info.name == "Demo" + assert resp.parameters == {"k": "v"} + assert resp.input_schema == {"s": 1} + + +def test_fields_subset_limits_blocks(monkeypatch): + monkeypatch.setattr("controllers.openapi.apps.parameters_payload", lambda app: {"k": "v"}) + monkeypatch.setattr("controllers.openapi.apps.build_input_schema", lambda app: {"s": 1}) + resp = build_app_describe_response(_app(), ["info"]) + assert resp.info is not None + assert resp.parameters is None + assert resp.input_schema is None + + +def test_info_omits_author_and_tags(monkeypatch): + monkeypatch.setattr("controllers.openapi.apps.parameters_payload", lambda app: {}) + monkeypatch.setattr("controllers.openapi.apps.build_input_schema", lambda app: {}) + resp = build_app_describe_response(_app(), ["info"]) + assert resp.info is not None + # Usage-face describe must not expose creator identity or tags (cross-tenant leak). + assert not hasattr(resp.info, "author") + assert not hasattr(resp.info, "tags") + + +def test_parameters_fallback_on_app_unavailable(monkeypatch): + def _raise(app): + raise AppUnavailableError() + + monkeypatch.setattr("controllers.openapi.apps.parameters_payload", _raise) + monkeypatch.setattr("controllers.openapi.apps.build_input_schema", lambda app: {"s": 1}) + resp = build_app_describe_response(_app(), ["parameters"]) + assert resp.parameters == dict(_EMPTY_PARAMETERS) + + +def test_input_schema_fallback_on_app_unavailable(monkeypatch): + def _raise(app): + raise AppUnavailableError() + + monkeypatch.setattr("controllers.openapi.apps.parameters_payload", lambda app: {"k": "v"}) + monkeypatch.setattr("controllers.openapi.apps.build_input_schema", _raise) + resp = build_app_describe_response(_app(), ["input_schema"]) + assert resp.input_schema == dict(EMPTY_INPUT_SCHEMA) diff --git a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py index 9d207b1930a..e0b15585323 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py @@ -5,7 +5,7 @@ Runs against the model directly, not the HTTP layer. Pins: - workspace_id is required. - numeric bounds enforced (page >= 1, limit in [1, MAX_PAGE_LIMIT]). - mode validates against the AppMode enum. -- name and tag have length caps. +- name has a length cap. """ from __future__ import annotations @@ -24,7 +24,6 @@ def test_defaults(): assert q.limit == 20 assert q.mode is None assert q.name is None - assert q.tag is None def test_workspace_id_required(): @@ -80,12 +79,6 @@ def test_name_length_capped(): AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201}) -def test_tag_length_capped(): - AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 100}) - with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 101}) - - def test_all_fields_accept_valid_values(): """Pin the happy-path acceptance for every field in one place.""" q = AppListQuery.model_validate( @@ -95,7 +88,6 @@ def test_all_fields_accept_valid_values(): "limit": 50, "mode": "workflow", "name": "search", - "tag": "prod", } ) assert q.workspace_id == "00000000-0000-0000-0000-000000000001" @@ -104,4 +96,3 @@ def test_all_fields_accept_valid_values(): assert q.mode is not None assert q.mode.value == "workflow" assert q.name == "search" - assert q.tag == "prod" diff --git a/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py b/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py index 930647608fa..7b84911d788 100644 --- a/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py +++ b/api/tests/unit_tests/controllers/openapi/test_pagination_envelope.py @@ -63,23 +63,19 @@ def test_envelope_uses_pep695_generics(): def test_app_info_response_dump_matches_spec(): - from controllers.openapi._models import AppInfoResponse + from controllers.openapi._models import AppInfo - obj = AppInfoResponse( + obj = AppInfo( id="app1", name="X", description="d", mode="chat", - author="alice", - tags=[{"name": "prod"}], ) assert obj.model_dump(mode="json") == { "id": "app1", "name": "X", "description": "d", "mode": "chat", - "author": "alice", - "tags": [{"name": "prod"}], } @@ -91,8 +87,6 @@ def test_app_describe_response_nests_info_and_parameters(): name="X", mode="chat", description=None, - tags=[], - author=None, updated_at="2026-05-05T00:00:00+00:00", service_api_enabled=True, ) diff --git a/cli/scripts/generate-command-tree.test.ts b/cli/scripts/generate-command-tree.test.ts index cf25ddcf71a..c811de8b4fe 100644 --- a/cli/scripts/generate-command-tree.test.ts +++ b/cli/scripts/generate-command-tree.test.ts @@ -137,6 +137,17 @@ export const commandTree: CommandTree = { const verIdx = out.indexOf('Version') expect(authIdx).toBeLessThan(verIdx) }) + + it('quotes hyphenated keys and leaves plain identifier keys unquoted', () => { + const entries: CommandEntry[] = [ + { tokens: ['export', 'app'], identifier: 'ExportApp', importPath: '@/commands/export/app/index' }, + { tokens: ['export', 'studio-app'], identifier: 'ExportStudioApp', importPath: '@/commands/export/studio-app/index' }, + ] + const out = formatModule(entries, buildTree(entries)) + expect(out).toContain(`'studio-app': { command: ExportStudioApp, subcommands: {} },`) + expect(out).toContain(`app: { command: ExportApp, subcommands: {} },`) + expect(out).not.toContain(`'app':`) + }) }) function makeFixture(): string { diff --git a/cli/scripts/generate-command-tree.ts b/cli/scripts/generate-command-tree.ts index 769490df834..9f3357bd6ea 100644 --- a/cli/scripts/generate-command-tree.ts +++ b/cli/scripts/generate-command-tree.ts @@ -141,13 +141,24 @@ function emitNode(node: TreeNode, indent: string): string { return parts.join('\n') } +function needsQuoting(key: string): boolean { + // A bare object key must be a valid JS identifier: the start class excludes digits + // (letter/_/$ only), so a leading digit fails the match and the key gets quoted. + return !/^[A-Z_$][\w$]*$/i.test(key) +} + +function emitKey(key: string): string { + return needsQuoting(key) ? `'${key}'` : key +} + function emitEntry(key: string, node: TreeNode, indent: string): string { + const k = emitKey(key) const isLeaf = node.subcommands.size === 0 && node.command !== undefined if (isLeaf) - return `${indent}${key}: { command: ${node.command}, subcommands: {} },` + return `${indent}${k}: { command: ${node.command}, subcommands: {} },` return [ - `${indent}${key}: {`, + `${indent}${k}: {`, emitNode(node, indent), `${indent}},`, ].join('\n') diff --git a/cli/src/api/app-meta.ts b/cli/src/api/app-meta.ts index c2ca9a5aa9b..03ae59bd392 100644 --- a/cli/src/api/app-meta.ts +++ b/cli/src/api/app-meta.ts @@ -1,17 +1,17 @@ -import type { AppsClient } from './apps' +import type { AppReader } from './app-reader' import type { AppInfoCache } from '@/cache/app-info' import type { AppMeta, AppMetaFieldKey } from '@/types/app-meta' import { covers, fromDescribe, mergeMeta } from '@/types/app-meta' export type AppMetaClientOptions = { - readonly apps: AppsClient + readonly apps: AppReader readonly host: string readonly cache?: AppInfoCache readonly now?: () => Date } export class AppMetaClient { - private readonly apps: AppsClient + private readonly apps: AppReader private readonly host: string private readonly cache: AppInfoCache | undefined private readonly now: () => Date diff --git a/cli/src/api/app-reader.test.ts b/cli/src/api/app-reader.test.ts new file mode 100644 index 00000000000..ff584c85c49 --- /dev/null +++ b/cli/src/api/app-reader.test.ts @@ -0,0 +1,30 @@ +import type { ActiveContext } from '@/auth/hosts' +import type { HttpClient } from '@/http/types' +import { describe, expect, it } from 'vitest' +import { selectAppReader, SubjectKind, subjectOf } from './app-reader' +import { AppsClient } from './apps' +import { PermittedExternalAppsClient } from './permitted-external-apps' + +const http = { baseURL: 'https://x', request: async () => new Response() } as unknown as HttpClient + +function ctx(external: boolean): ActiveContext { + return { + host: 'h', + email: 'e', + ctx: { + account: { id: 'a', email: 'e', name: 'n' }, + external_subject: external ? { email: 'e', issuer: 'i' } : undefined, + }, + } +} + +describe('selectAppReader', () => { + it('account login → AppsClient', () => { + expect(selectAppReader(ctx(false), http)).toBeInstanceOf(AppsClient) + expect(subjectOf(ctx(false))).toBe(SubjectKind.Account) + }) + it('external_subject present → PermittedExternalAppsClient', () => { + expect(selectAppReader(ctx(true), http)).toBeInstanceOf(PermittedExternalAppsClient) + expect(subjectOf(ctx(true))).toBe(SubjectKind.External) + }) +}) diff --git a/cli/src/api/app-reader.ts b/cli/src/api/app-reader.ts new file mode 100644 index 00000000000..fe41e35bfe9 --- /dev/null +++ b/cli/src/api/app-reader.ts @@ -0,0 +1,35 @@ +import type { AppDescribeResponse, AppListResponse } from '@dify/contracts/api/openapi/types.gen' +import type { ListQuery } from './apps' +import type { ActiveContext } from '@/auth/hosts' +import type { HttpClient } from '@/http/types' +import { AppsClient } from './apps' +import { PermittedExternalAppsClient } from './permitted-external-apps' + +export type AppReader = { + list: (q: ListQuery) => Promise + describe: (appId: string, fields?: readonly string[]) => Promise +} + +// The auth subject behind an openapi bearer token. Each kind reads apps from its own surface. +export const SubjectKind = { + Account: 'account', + External: 'external', +} as const + +export type SubjectKindValue = (typeof SubjectKind)[keyof typeof SubjectKind] + +export function subjectOf(active: ActiveContext): SubjectKindValue { + return active.ctx.external_subject !== undefined ? SubjectKind.External : SubjectKind.Account +} + +type AppReaderFactory = (http: HttpClient) => AppReader + +// Maps each auth subject to the app reader for its surface. +const APP_READER_BY_SUBJECT: Readonly> = { + [SubjectKind.Account]: http => new AppsClient(http), + [SubjectKind.External]: http => new PermittedExternalAppsClient(http), +} + +export function selectAppReader(active: ActiveContext, http: HttpClient): AppReader { + return APP_READER_BY_SUBJECT[subjectOf(active)](http) +} diff --git a/cli/src/api/apps.test.ts b/cli/src/api/apps.test.ts index 68e7bcc86a3..861f60feb26 100644 --- a/cli/src/api/apps.test.ts +++ b/cli/src/api/apps.test.ts @@ -36,7 +36,6 @@ describe('AppsClient.list', () => { // Optional filters are omitted entirely when not supplied. expect(q.has('mode')).toBe(false) expect(q.has('name')).toBe(false) - expect(q.has('tag')).toBe(false) }) it('forwards explicit pagination and filters', async () => { @@ -48,7 +47,6 @@ describe('AppsClient.list', () => { limit: 50, mode: 'chat', name: 'support bot', - tag: 'prod', }) const q = queryOf(stub.captured.url) @@ -56,18 +54,16 @@ describe('AppsClient.list', () => { expect(q.get('limit')).toBe('50') expect(q.get('mode')).toBe('chat') expect(q.get('name')).toBe('support bot') - expect(q.get('tag')).toBe('prod') }) it('treats empty-string filters as absent (not blank query params)', async () => { stub = await startStubServer(cap => jsonResponder(200, LIST_BODY, cap)) - await makeClient(stub.url).list({ workspaceId: 'ws-1', mode: '', name: '', tag: '' }) + await makeClient(stub.url).list({ workspaceId: 'ws-1', mode: '', name: '' }) const q = queryOf(stub.captured.url) expect(q.has('mode')).toBe(false) expect(q.has('name')).toBe(false) - expect(q.has('tag')).toBe(false) }) it('propagates server 403 as a classified BaseError', async () => { diff --git a/cli/src/api/apps.ts b/cli/src/api/apps.ts index ea0e41c252f..01b18d9a9da 100644 --- a/cli/src/api/apps.ts +++ b/cli/src/api/apps.ts @@ -1,4 +1,5 @@ import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen' +import type { AppReader } from './app-reader' import type { OpenApiClient } from '@/http/orpc' import type { HttpClient } from '@/http/types' import { createOpenApiClient } from '@/http/orpc' @@ -9,10 +10,14 @@ export type ListQuery = { readonly limit?: number readonly mode?: AppMode | '' readonly name?: string - readonly tag?: string } -export class AppsClient { +// An absent or empty mode filter means "any mode" — collapse both to undefined for the query. +export function normalizeMode(mode: AppMode | '' | undefined): AppMode | undefined { + return mode !== undefined && mode !== '' ? mode : undefined +} + +export class AppsClient implements AppReader { private readonly orpc: OpenApiClient constructor(http: HttpClient) { @@ -25,9 +30,8 @@ export class AppsClient { workspace_id: q.workspaceId, page: q.page ?? 1, limit: q.limit ?? 20, - mode: q.mode !== undefined && q.mode !== '' ? q.mode : undefined, + mode: normalizeMode(q.mode), name: q.name !== undefined && q.name !== '' ? q.name : undefined, - tag: q.tag !== undefined && q.tag !== '' ? q.tag : undefined, }, }) } diff --git a/cli/src/api/permitted-external-apps.test.ts b/cli/src/api/permitted-external-apps.test.ts new file mode 100644 index 00000000000..f6fa38cb3eb --- /dev/null +++ b/cli/src/api/permitted-external-apps.test.ts @@ -0,0 +1,27 @@ +import type { HttpClient } from '@/http/types' +import { describe, expect, it, vi } from 'vitest' +import { PermittedExternalAppsClient } from './permitted-external-apps' + +function fakeHttp() { + return { baseURL: 'https://x', request: vi.fn() } as unknown as HttpClient +} + +type WithOrpc = { orpc: unknown } + +describe('PermittedExternalAppsClient', () => { + it('list calls permittedExternalApps.get with paging/filter query', async () => { + const c = new PermittedExternalAppsClient(fakeHttp()) + const get = vi.fn().mockResolvedValue({ page: 1, limit: 20, total: 0, has_more: false, data: [] }) + ;(c as unknown as WithOrpc).orpc = { permittedExternalApps: { get, byAppId: { describe: { get: vi.fn() } } } } + await c.list({ workspaceId: '', page: 2, limit: 5, mode: undefined, name: 'a' }) + expect(get).toHaveBeenCalledWith({ query: { page: 2, limit: 5, mode: undefined, name: 'a' } }) + }) + + it('describe calls permittedExternalApps.byAppId.describe.get with app_id + fields', async () => { + const c = new PermittedExternalAppsClient(fakeHttp()) + const dget = vi.fn().mockResolvedValue({ info: null, parameters: null, input_schema: null }) + ;(c as unknown as WithOrpc).orpc = { permittedExternalApps: { get: vi.fn(), byAppId: { describe: { get: dget } } } } + await c.describe('app-1', ['info']) + expect(dget).toHaveBeenCalledWith({ params: { app_id: 'app-1' }, query: { fields: 'info' } }) + }) +}) diff --git a/cli/src/api/permitted-external-apps.ts b/cli/src/api/permitted-external-apps.ts new file mode 100644 index 00000000000..497c398d0ba --- /dev/null +++ b/cli/src/api/permitted-external-apps.ts @@ -0,0 +1,34 @@ +import type { AppDescribeResponse, AppListResponse } from '@dify/contracts/api/openapi/types.gen' +import type { AppReader } from './app-reader' +import type { ListQuery } from './apps' +import type { OpenApiClient } from '@/http/orpc' +import type { HttpClient } from '@/http/types' +import { createOpenApiClient } from '@/http/orpc' +import { normalizeMode } from './apps' + +export class PermittedExternalAppsClient implements AppReader { + private readonly orpc: OpenApiClient + + constructor(http: HttpClient) { + this.orpc = createOpenApiClient(http) + } + + // workspaceId is ignored: the external grant is not workspace-scoped. + async list(q: ListQuery): Promise { + return this.orpc.permittedExternalApps.get({ + query: { + page: q.page ?? 1, + limit: q.limit ?? 20, + mode: normalizeMode(q.mode), + name: q.name !== undefined && q.name !== '' ? q.name : undefined, + }, + }) + } + + async describe(appId: string, fields?: readonly string[]): Promise { + return this.orpc.permittedExternalApps.byAppId.describe.get({ + params: { app_id: appId }, + query: { fields: fields !== undefined && fields.length > 0 ? fields.join(',') : undefined }, + }) + } +} diff --git a/cli/src/cache/app-info.test.ts b/cli/src/cache/app-info.test.ts index 50ac1c67d35..ae6f15f249d 100644 --- a/cli/src/cache/app-info.test.ts +++ b/cli/src/cache/app-info.test.ts @@ -21,8 +21,6 @@ function metaInfoOnly(): AppMeta { name: 'Greeter', description: '', mode: 'chat', - author: 'tester', - tags: [], updated_at: undefined, service_api_enabled: false, is_agent: false, diff --git a/cli/src/commands/agent-guides.test.ts b/cli/src/commands/agent-guides.test.ts index e1bcb0925aa..3ad18f540f1 100644 --- a/cli/src/commands/agent-guides.test.ts +++ b/cli/src/commands/agent-guides.test.ts @@ -2,7 +2,9 @@ import type { CommandConstructor } from '@/framework/command' import { describe, expect, it } from 'vitest' import Login from '@/commands/auth/login/index' import DescribeApp from '@/commands/describe/app/index' +import ExportStudioApp from '@/commands/export/studio-app/index' import GetApp from '@/commands/get/app/index' +import ImportStudioApp from '@/commands/import/studio-app/index' import ResumeApp from '@/commands/resume/app/index' import RunApp from '@/commands/run/app/index' @@ -13,6 +15,8 @@ const GUIDED_COMMANDS: ReadonlyArray = [ ['resume app', ResumeApp], ['describe app', DescribeApp], ['get app', GetApp], + ['export studio-app', ExportStudioApp], + ['import studio-app', ImportStudioApp], ['auth login', Login], ] diff --git a/cli/src/commands/describe/app/handlers.ts b/cli/src/commands/describe/app/handlers.ts index 6b934c87148..133260e6509 100644 --- a/cli/src/commands/describe/app/handlers.ts +++ b/cli/src/commands/describe/app/handlers.ts @@ -1,4 +1,4 @@ -import type { AppDescribeInfo, TagItem } from '@dify/contracts/api/openapi/types.gen' +import type { AppDescribeInfo } from '@dify/contracts/api/openapi/types.gen' import type { AppMeta } from '@/types/app-meta' export const APP_DESCRIBE_MODE_KEY = 'app-describe' @@ -28,10 +28,8 @@ export class AppDescribeOutput { ['Name', info.name], ['ID', info.id], ['Mode', info.mode], - ['Author', info.author ?? ''], ['Updated', info.updated_at ?? ''], ['Service API', info.service_api_enabled ? 'true' : 'false'], - ['Tags', joinTags(info.tags ?? [])], ] if (info.description !== '' && info.description !== undefined) rows.push(['Description', info.description ?? '']) @@ -55,12 +53,6 @@ export class AppDescribeOutput { } } -function joinTags(tags: readonly TagItem[]): string { - if (tags.length === 0) - return '' - return tags.map(t => t.name).join(',') -} - function alignedRows(rows: readonly [string, string][]): string[] { const widest = rows.reduce((m, [k]) => Math.max(m, k.length), 0) return rows.map(([k, v]) => `${`${k}:`.padEnd(widest + 2)}${v}`) diff --git a/cli/src/commands/describe/app/run.test.ts b/cli/src/commands/describe/app/run.test.ts index 4ed7cedc7a5..96dfae5acb5 100644 --- a/cli/src/commands/describe/app/run.test.ts +++ b/cli/src/commands/describe/app/run.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { startMock } from '@test/fixtures/dify-mock/server' import { testHttpClient } from '@test/fixtures/http-client' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { loadAppInfoCache } from '@/cache/app-info' import { formatted, stringifyOutput } from '@/framework/output' import { ENV_CACHE_DIR } from '@/store/dir' @@ -34,6 +34,7 @@ describe('runDescribeApp', () => { process.env[ENV_CACHE_DIR] = dir }) afterEach(async () => { + vi.restoreAllMocks() if (prevCacheDir === undefined) delete process.env[ENV_CACHE_DIR] else @@ -60,8 +61,6 @@ describe('runDescribeApp', () => { expect(out).toContain('Mode:') expect(out).toContain('chat') expect(out).toContain('Service API:') - expect(out).toContain('Tags:') - expect(out).toContain('demo') expect(out).toContain('Description:') expect(out).toContain('Parameters:') }) @@ -115,4 +114,13 @@ describe('runDescribeApp', () => { }, )).rejects.toThrow() }) + + it('external login resolves describe via the permitted-external route', async () => { + const activeExt: ActiveContext = { host: mock.url, email: 'e', ctx: { account: { id: 'a', email: 'e', name: 'n' }, external_subject: { email: 'e', issuer: 'i' } } } + const out = await runDescribeApp( + { appId: 'app-1' }, + { active: activeExt, http: testHttpClient(mock.url, 'dfoe_test'), host: mock.url }, + ) + expect(out.payload.info?.id).toBe('app-1') + }) }) diff --git a/cli/src/commands/describe/app/run.ts b/cli/src/commands/describe/app/run.ts index 8af2a5dc441..0d5eea784d4 100644 --- a/cli/src/commands/describe/app/run.ts +++ b/cli/src/commands/describe/app/run.ts @@ -3,7 +3,7 @@ import type { AppInfoCache } from '@/cache/app-info' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' import { AppMetaClient } from '@/api/app-meta' -import { AppsClient } from '@/api/apps' +import { selectAppReader } from '@/api/app-reader' import { runWithSpinner } from '@/sys/io/spinner' import { nullStreams } from '@/sys/io/streams' import { FieldInfo, FieldInputSchema, FieldParameters } from '@/types/app-meta' @@ -26,7 +26,7 @@ export type DescribeAppDeps = { } export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeAppDeps): Promise { - const apps = new AppsClient(deps.http) + const apps = selectAppReader(deps.active, deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) const io = deps.io ?? nullStreams() const result = await runWithSpinner( diff --git a/cli/src/commands/export/studio-app/guide.ts b/cli/src/commands/export/studio-app/guide.ts new file mode 100644 index 00000000000..d41b502b191 --- /dev/null +++ b/cli/src/commands/export/studio-app/guide.ts @@ -0,0 +1,12 @@ +export const agentGuide = ` +WHEN TO USE + A studio app is what you build and edit in Studio on the web console, + inside a workspace — the app's source definition, not the published app + that 'run app' invokes. Export pulls that definition as YAML to back it + up, diff it, or recreate the app elsewhere with 'import studio-app'. To + run or inspect an app instead, use the 'app' noun. + +ERROR RECOVERY + app not found (404) difyctl get app + not logged in (exit 4) difyctl auth login +` diff --git a/cli/src/commands/export/app/index.ts b/cli/src/commands/export/studio-app/index.ts similarity index 71% rename from cli/src/commands/export/app/index.ts rename to cli/src/commands/export/studio-app/index.ts index 7afd0234982..69bdcf09aa3 100644 --- a/cli/src/commands/export/app/index.ts +++ b/cli/src/commands/export/studio-app/index.ts @@ -1,16 +1,17 @@ import { DifyCommand } from '@/commands/_shared/dify-command' import { httpRetryFlag } from '@/commands/_shared/global-flags' import { Args, Flags } from '@/framework/flags' +import { agentGuide } from './guide' import { runExportApp } from './run' -export default class ExportApp extends DifyCommand { - static override description = 'Export an app\'s DSL configuration as YAML' +export default class ExportStudioApp extends DifyCommand { + static override description = 'Export a studio app\'s DSL configuration as YAML' static override examples = [ - '<%= config.bin %> export app ', - '<%= config.bin %> export app --output ./my-app.yaml', - '<%= config.bin %> export app --include-secret', - '<%= config.bin %> export app --workflow-id ', + '<%= config.bin %> export studio-app ', + '<%= config.bin %> export studio-app --output ./my-app.yaml', + '<%= config.bin %> export studio-app --include-secret', + '<%= config.bin %> export studio-app --workflow-id ', ] static override args = { @@ -26,7 +27,7 @@ export default class ExportApp extends DifyCommand { } async run(argv: string[]) { - const { args, flags } = this.parse(ExportApp, argv) + const { args, flags } = this.parse(ExportStudioApp, argv) const ctx = await this.authedCtx({ retryFlag: flags['http-retry'] }) const result = await runExportApp({ appId: args.id, @@ -42,4 +43,8 @@ export default class ExportApp extends DifyCommand { ctx.io.out.write('\n') } } + + override agentGuide(): string { + return agentGuide + } } diff --git a/cli/src/commands/export/app/run.test.ts b/cli/src/commands/export/studio-app/run.test.ts similarity index 100% rename from cli/src/commands/export/app/run.test.ts rename to cli/src/commands/export/studio-app/run.test.ts diff --git a/cli/src/commands/export/app/run.ts b/cli/src/commands/export/studio-app/run.ts similarity index 89% rename from cli/src/commands/export/app/run.ts rename to cli/src/commands/export/studio-app/run.ts index 93abe2f8b46..351a2fc349f 100644 --- a/cli/src/commands/export/app/run.ts +++ b/cli/src/commands/export/studio-app/run.ts @@ -35,9 +35,8 @@ export async function runExportApp(opts: ExportAppOptions, deps: ExportAppDeps): const io = deps.io ?? nullStreams() const dslFactory = deps.dslFactory ?? ((h: HttpClient) => new AppDslClient(h)) - // workspace is needed to satisfy the auth pipeline; resolving it here - // mirrors what other commands do even though the export endpoint does not - // take workspace_id as a query parameter (it loads tenant from app). + // workspace is resolved to satisfy the auth pipeline; the export endpoint itself + // takes no workspace_id query parameter (it loads tenant from the app). resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) const client = dslFactory(deps.http) diff --git a/cli/src/commands/get/app/handlers.ts b/cli/src/commands/get/app/handlers.ts index ac7008fa537..9c91057d26d 100644 --- a/cli/src/commands/get/app/handlers.ts +++ b/cli/src/commands/get/app/handlers.ts @@ -1,4 +1,4 @@ -import type { AppListResponse, AppListRow, TagItem } from '@dify/contracts/api/openapi/types.gen' +import type { AppListResponse, AppListRow } from '@dify/contracts/api/openapi/types.gen' import type { TableCell, TableColumn } from '@/framework/output' export const APP_MODE_KEY = 'app' @@ -7,9 +7,7 @@ export const APP_COLUMNS: readonly TableColumn[] = [ { name: 'NAME', priority: 0 }, { name: 'ID', priority: 0 }, { name: 'MODE', priority: 0 }, - { name: 'TAGS', priority: 0 }, { name: 'UPDATED', priority: 0 }, - { name: 'AUTHOR', priority: 1 }, { name: 'WORKSPACE', priority: 1 }, ] @@ -25,9 +23,7 @@ export class AppRow { this.data.name, this.data.id, this.data.mode, - joinTags(this.data.tags ?? []), this.data.updated_at ?? '', - this.data.created_by_name ?? '', this.data.workspace_name ?? '', ] } @@ -70,7 +66,3 @@ export class AppListOutput { return this.envelope } } - -function joinTags(tags: readonly TagItem[]): string { - return tags.map(t => t.name).join(',') -} diff --git a/cli/src/commands/get/app/index.ts b/cli/src/commands/get/app/index.ts index 47594813704..ffce31b7c49 100644 --- a/cli/src/commands/get/app/index.ts +++ b/cli/src/commands/get/app/index.ts @@ -42,7 +42,6 @@ export default class GetApp extends DifyCommand { 'limit': Flags.string({ description: 'page size [1..200]' }), 'mode': Flags.string({ description: 'filter by app mode', options: APP_MODE_VALUES }), 'name': Flags.string({ description: 'filter by app name (server-side substring)' }), - 'tag': Flags.string({ description: 'filter by tag name (server-side exact match)' }), 'http-retry': httpRetryFlag, 'output': Flags.outputFormat({ options: [OutputFormat.JSON, OutputFormat.YAML, OutputFormat.NAME, OutputFormat.WIDE], default: '' }), } @@ -59,7 +58,6 @@ export default class GetApp extends DifyCommand { limitRaw: flags.limit, mode: flags.mode as AppMode | undefined, name: flags.name, - tag: flags.tag, format, }, { active: ctx.active, http: ctx.http, io: ctx.io }) return table({ diff --git a/cli/src/commands/get/app/run.test.ts b/cli/src/commands/get/app/run.test.ts index 7c0cc76c009..d517859addd 100644 --- a/cli/src/commands/get/app/run.test.ts +++ b/cli/src/commands/get/app/run.test.ts @@ -1,8 +1,9 @@ import type { DifyMock } from '@test/fixtures/dify-mock/server' import type { ActiveContext } from '@/auth/hosts' +import type { HttpClient } from '@/http/types' import { startMock } from '@test/fixtures/dify-mock/server' import { testHttpClient } from '@test/fixtures/http-client' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { stringifyOutput, table } from '@/framework/output' import { AppListOutput } from './handlers.js' import { runGetApp } from './run.js' @@ -25,6 +26,7 @@ describe('runGetApp', () => { }) afterEach(async () => { + vi.restoreAllMocks() await mock.stop() }) @@ -40,13 +42,12 @@ describe('runGetApp', () => { })) } - it('list (no id, default format) renders table with NAME ID MODE TAGS UPDATED', async () => { + it('list (no id, default format) renders table with NAME ID MODE UPDATED', async () => { const out = await render() - expect(out).toMatch(/^NAME\s+ID\s+MODE\s+TAGS\s+UPDATED/) + expect(out).toMatch(/^NAME\s+ID\s+MODE\s+UPDATED/) expect(out).toContain('Greeter') expect(out).toContain('app-1') expect(out).toContain('chat') - expect(out).toContain('demo') expect(out).toContain('Workflow') expect(out).not.toContain('app-3') }) @@ -56,9 +57,7 @@ describe('runGetApp', () => { 'NAME', 'ID', 'MODE', - 'TAGS', 'UPDATED', - 'AUTHOR', 'WORKSPACE', ]) }) @@ -76,12 +75,6 @@ describe('runGetApp', () => { expect(out).not.toContain('Greeter') }) - it('--tag filters server-side', async () => { - const out = await render({ tag: 'demo' }) - expect(out).toContain('Greeter') - expect(out).not.toContain('Workflow') - }) - it('-A all-workspaces aggregates across workspaces sorted by id', async () => { const out = await render({ allWorkspaces: true }) expect(out).toContain('app-1') @@ -110,10 +103,9 @@ describe('runGetApp', () => { expect(out.trim().split('\n').sort()).toEqual(['app-1', 'app-2']) }) - it('-o wide includes AUTHOR and WORKSPACE columns', async () => { + it('-o wide includes the WORKSPACE column', async () => { const out = await render({ format: 'wide' }) - expect(out).toMatch(/^NAME\s+ID\s+MODE\s+TAGS\s+UPDATED\s+AUTHOR\s+WORKSPACE/) - expect(out).toContain('tester') + expect(out).toMatch(/^NAME\s+ID\s+MODE\s+UPDATED\s+WORKSPACE/) expect(out).toContain('Default') }) @@ -138,4 +130,25 @@ describe('runGetApp', () => { } await expect(runGetApp({}, { active: minimal, http: http() })).rejects.toThrow(/no workspace/) }) + + it('external login lists via permitted-external client without workspace', async () => { + const list = vi.fn().mockResolvedValue({ page: 1, limit: 20, total: 1, has_more: false, data: [{ id: 'x', name: 'X', description: null, mode: 'chat', updated_at: null, workspace_id: 'w', workspace_name: 'W' }] }) + const { PermittedExternalAppsClient } = await import('@/api/permitted-external-apps') + vi.spyOn(PermittedExternalAppsClient.prototype, 'list').mockImplementation(list) + const active: ActiveContext = { host: 'h', email: 'e', ctx: { account: { id: 'a', email: 'e', name: 'n' }, external_subject: { email: 'e', issuer: 'i' } } } + const http = { baseURL: 'https://x', request: vi.fn() } as unknown as HttpClient + const res = await runGetApp({}, { active, http }) + expect(list).toHaveBeenCalled() + const firstCallArg = list.mock.calls[0]![0] as { workspaceId: string } + expect(firstCallArg.workspaceId).toBe('') + expect(res.data).toBeDefined() + }) + + it('--all-workspaces throws UsageInvalidFlag for external logins', async () => { + const active: ActiveContext = { host: 'h', email: 'e', ctx: { account: { id: 'a', email: 'e', name: 'n' }, external_subject: { email: 'e', issuer: 'i' } } } + const httpClient = { baseURL: 'https://x', request: vi.fn() } as unknown as HttpClient + await expect(runGetApp({ allWorkspaces: true }, { active, http: httpClient })) + .rejects + .toThrow(/--all-workspaces is not available for external logins/) + }) }) diff --git a/cli/src/commands/get/app/run.ts b/cli/src/commands/get/app/run.ts index 102cf066499..c4a7911e0db 100644 --- a/cli/src/commands/get/app/run.ts +++ b/cli/src/commands/get/app/run.ts @@ -1,9 +1,12 @@ import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen' +import type { AppReader } from '@/api/app-reader' import type { ActiveContext } from '@/auth/hosts' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' -import { AppsClient } from '@/api/apps' +import { selectAppReader, SubjectKind, subjectOf } from '@/api/app-reader' import { WorkspacesClient } from '@/api/workspaces' +import { newError } from '@/errors/base' +import { ErrorCode } from '@/errors/codes' import { LIMIT_DEFAULT, parseLimit } from '@/limit/limit' import { getEnv } from '@/sys/index' import { runWithSpinner } from '@/sys/io/spinner' @@ -19,7 +22,6 @@ export type GetAppOptions = { readonly limitRaw?: string readonly mode?: AppMode readonly name?: string - readonly tag?: string readonly format?: string } @@ -28,7 +30,6 @@ export type GetAppDeps = { readonly http: HttpClient readonly io?: IOStreams readonly envLookup?: (k: string) => string | undefined - readonly appsFactory?: (http: HttpClient) => AppsClient readonly workspacesFactory?: (http: HttpClient) => WorkspacesClient } @@ -40,10 +41,10 @@ export type GetAppResult = { export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise { const env = deps.envLookup ?? getEnv - const appsFactory = deps.appsFactory ?? ((h: HttpClient) => new AppsClient(h)) const wsFactory = deps.workspacesFactory ?? ((h: HttpClient) => new WorkspacesClient(h)) - const apps = appsFactory(deps.http) + const external = subjectOf(deps.active) === SubjectKind.External + const apps = selectAppReader(deps.active, deps.http) const pageSize = resolveLimit(opts.limitRaw, env) const page = opts.page === undefined || opts.page <= 0 ? 1 : opts.page const label = opts.appId !== undefined && opts.appId !== '' ? 'Fetching app' : 'Fetching apps' @@ -53,15 +54,20 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise< { io, label }, async (): Promise => { if (opts.allWorkspaces === true) { + if (external) + throw newError(ErrorCode.UsageInvalidFlag, '--all-workspaces is not available for external logins') const ws = wsFactory(deps.http) return runAllWorkspaces(apps, ws, opts, page, pageSize) } if (opts.appId !== undefined && opts.appId !== '') { - const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) - const wsName = workspaceNameForId(deps.active, wsId) + const wsId = external ? '' : resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) + const wsName = external ? '' : workspaceNameForId(deps.active, wsId) const desc = await apps.describe(opts.appId, ['info']) return describeToEnvelope(desc, wsId, wsName) } + if (external) { + return apps.list({ workspaceId: '', page, limit: pageSize, mode: opts.mode, name: opts.name }) + } const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) return apps.list({ workspaceId: wsId, @@ -69,7 +75,6 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise< limit: pageSize, mode: opts.mode, name: opts.name, - tag: opts.tag, }) }, ) @@ -102,9 +107,7 @@ function describeToEnvelope(desc: AppDescribeResponse, wsId: string, wsName: str name: desc.info.name, description: desc.info.description, mode: desc.info.mode as AppMode, - tags: desc.info.tags, updated_at: desc.info.updated_at, - created_by_name: desc.info.author === '' ? undefined : desc.info.author, workspace_id: wsId, workspace_name: wsName === '' ? undefined : wsName, }], @@ -118,7 +121,7 @@ function workspaceNameForId(active: ActiveContext, id: string): string { } async function runAllWorkspaces( - apps: AppsClient, + apps: AppReader, ws: WorkspacesClient, opts: GetAppOptions, page: number, @@ -139,7 +142,6 @@ async function runAllWorkspaces( limit, mode: opts.mode, name: opts.name, - tag: opts.tag, }) merged.total += env.total merged.data = [...merged.data, ...env.data] diff --git a/cli/src/commands/import/studio-app/guide.ts b/cli/src/commands/import/studio-app/guide.ts new file mode 100644 index 00000000000..66ecdda9aa5 --- /dev/null +++ b/cli/src/commands/import/studio-app/guide.ts @@ -0,0 +1,17 @@ +export const agentGuide = ` +WHEN TO USE + A studio app is what you build and edit in Studio on the web console, + inside a workspace — the app's source definition. Import materialises a + DSL YAML into a new (or existing) studio app; pair it with + 'export studio-app' to move an app between workspaces or instances. To + run or inspect the result, switch to the 'app' noun. + +BEHAVIOUR + A DSL version mismatch is auto-confirmed; no second command needed. + Missing plugin dependencies are listed on stderr — install them before + running the app. + +ERROR RECOVERY + workspace required difyctl get workspace + not logged in (exit 4) difyctl auth login +` diff --git a/cli/src/commands/import/app/index.ts b/cli/src/commands/import/studio-app/index.ts similarity index 80% rename from cli/src/commands/import/app/index.ts rename to cli/src/commands/import/studio-app/index.ts index fddda88f441..6e0b491199b 100644 --- a/cli/src/commands/import/app/index.ts +++ b/cli/src/commands/import/studio-app/index.ts @@ -1,16 +1,17 @@ import { DifyCommand } from '@/commands/_shared/dify-command' import { httpRetryFlag } from '@/commands/_shared/global-flags' import { Flags } from '@/framework/flags' +import { agentGuide } from './guide' import { pluginDependencyLabel, runImportApp } from './run' -export default class ImportApp extends DifyCommand { - static override description = 'Import an app from a DSL YAML file or URL' +export default class ImportStudioApp extends DifyCommand { + static override description = 'Import a studio app from a DSL YAML file or URL' static override examples = [ - '<%= config.bin %> import app --from-file ./app.yaml', - '<%= config.bin %> import app --from-file /path/to/app.yaml --name "My App"', - '<%= config.bin %> import app --from-url https://example.com/my-app.yaml', - '<%= config.bin %> import app --from-file ./app.yaml --app-id ', + '<%= config.bin %> import studio-app --from-file ./app.yaml', + '<%= config.bin %> import studio-app --from-file /path/to/app.yaml --name "My App"', + '<%= config.bin %> import studio-app --from-url https://example.com/my-app.yaml', + '<%= config.bin %> import studio-app --from-file ./app.yaml --app-id ', ] static override flags = { @@ -27,7 +28,7 @@ export default class ImportApp extends DifyCommand { } async run(argv: string[]) { - const { flags } = this.parse(ImportApp, argv) + const { flags } = this.parse(ImportStudioApp, argv) if (flags['from-file'] === undefined && flags['from-url'] === undefined) this.error('one of --from-file or --from-url is required', { exit: 1 }) if (flags['from-file'] !== undefined && flags['from-url'] !== undefined) @@ -57,4 +58,8 @@ export default class ImportApp extends DifyCommand { ctx.io.err.write(` - ${pluginDependencyLabel(dep)}\n`) } } + + override agentGuide(): string { + return agentGuide + } } diff --git a/cli/src/commands/import/app/run.test.ts b/cli/src/commands/import/studio-app/run.test.ts similarity index 100% rename from cli/src/commands/import/app/run.test.ts rename to cli/src/commands/import/studio-app/run.test.ts diff --git a/cli/src/commands/import/app/run.ts b/cli/src/commands/import/studio-app/run.ts similarity index 100% rename from cli/src/commands/import/app/run.ts rename to cli/src/commands/import/studio-app/run.ts diff --git a/cli/src/commands/resume/app/run.test.ts b/cli/src/commands/resume/app/run.test.ts new file mode 100644 index 00000000000..a72b0a93b25 --- /dev/null +++ b/cli/src/commands/resume/app/run.test.ts @@ -0,0 +1,66 @@ +import type { ActiveContext } from '@/auth/hosts' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { AppRunClient } from '@/api/app-run' +import { AppsClient } from '@/api/apps' +import { PermittedExternalAppsClient } from '@/api/permitted-external-apps' +import { bufferStreams } from '@/sys/io/streams' +import { resumeApp } from './run.js' + +const DESCRIBE_RESULT = { + info: { id: 'app-2', name: 'X', mode: 'workflow', description: '', updated_at: null, service_api_enabled: true, is_agent: false }, + parameters: null, + input_schema: null, +} + +const FORM_RESP = { user_actions: [{ id: 'submit' }] } + +function makeExternalActive(): ActiveContext { + return { + host: 'http://localhost', + email: 'sso@x.io', + ctx: { + account: { id: 'acct-1', email: 'sso@x.io', name: 'SSO User' }, + external_subject: { email: 'sso@x.io', issuer: 'https://issuer.example.com' }, + }, + } as unknown as ActiveContext +} + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('resumeApp pre-flight subject strategy', () => { + it('external login: mode pre-flight calls PermittedExternalAppsClient.describe, not AppsClient.describe', async () => { + const externalDescribe = vi.fn().mockResolvedValue(DESCRIBE_RESULT) + const externalSpy = vi.spyOn(PermittedExternalAppsClient.prototype, 'describe').mockImplementation(externalDescribe) + const accountSpy = vi.spyOn(AppsClient.prototype, 'describe') + + vi.spyOn(AppRunClient.prototype, 'submitHumanInput').mockResolvedValue(undefined as never) + + const io = bufferStreams() + const http = { + baseURL: 'http://localhost', + request: vi.fn().mockImplementation((opts: { path: string }) => { + if (typeof opts.path === 'string' && opts.path.includes('form/human_input')) { + return Promise.resolve(FORM_RESP) + } + // reconnect stream — return an async iterable that ends immediately + const iter: AsyncIterable = { [Symbol.asyncIterator]: () => ({ next: () => Promise.resolve({ done: true, value: undefined as never }) }) } + return Promise.resolve(iter) + }), + } as unknown as import('@/http/types').HttpClient + + try { + await resumeApp( + { appId: 'app-2', formToken: 'ft-1', workflowRunId: 'wf-run-1', action: 'submit', inputs: {} }, + { active: makeExternalActive(), http, host: 'http://localhost', io }, + ) + } + catch { + // run may fail after pre-flight due to stream mock; we only check which describe was called + } + + expect(externalSpy).toHaveBeenCalled() + expect(accountSpy).not.toHaveBeenCalled() + }) +}) diff --git a/cli/src/commands/resume/app/run.ts b/cli/src/commands/resume/app/run.ts index 81eb4e01f00..1dc2855a118 100644 --- a/cli/src/commands/resume/app/run.ts +++ b/cli/src/commands/resume/app/run.ts @@ -4,10 +4,11 @@ import type { RunContext } from '@/commands/run/app/_strategies/index' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' import { AppMetaClient } from '@/api/app-meta' +import { selectAppReader } from '@/api/app-reader' import { AppRunClient } from '@/api/app-run' -import { AppsClient } from '@/api/apps' import { pickStrategy } from '@/commands/run/app/_strategies/index' import { RUN_MODES } from '@/commands/run/app/handlers' +import { resolveInputs, TEXT_FORMATS } from '@/commands/run/app/input-flags' import { processExit } from '@/sys/index' import { colorEnabled, colorScheme } from '@/sys/io/color' import { FieldInfo } from '@/types/app-meta' @@ -37,45 +38,8 @@ export type ResumeAppDeps = { readonly exit?: (code: number) => never } -const TEXT_FORMATS = new Set(['', 'text']) - -async function resolveInputs( - inputsJson: string | undefined, - inputsFile: string | undefined, - directInputs: Readonly> | undefined, -): Promise> { - if (inputsJson !== undefined && inputsFile !== undefined) - throw new Error('--inputs and --inputs-file are mutually exclusive') - if (inputsJson !== undefined) { - let parsed: unknown - try { - parsed = JSON.parse(inputsJson) - } - catch { - throw new Error('--inputs must be valid JSON') - } - if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) - throw new Error('--inputs must be a JSON object') - return parsed as Record - } - if (inputsFile !== undefined) { - const { readFile } = await import('node:fs/promises') - let parsed: unknown - try { - parsed = JSON.parse(await readFile(inputsFile, 'utf8')) - } - catch { - throw new Error('--inputs-file must contain valid JSON') - } - if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) - throw new Error('--inputs-file must be a JSON object') - return parsed as Record - } - return { ...(directInputs ?? {}) } -} - export async function resumeApp(opts: ResumeAppOptions, deps: ResumeAppDeps): Promise { - const apps = new AppsClient(deps.http) + const apps = selectAppReader(deps.active, deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) const m = await meta.get(opts.appId, [FieldInfo]) const mode = m.info?.mode ?? RUN_MODES.Workflow diff --git a/cli/src/commands/run/app/input-flags.ts b/cli/src/commands/run/app/input-flags.ts new file mode 100644 index 00000000000..b6d296ad7c0 --- /dev/null +++ b/cli/src/commands/run/app/input-flags.ts @@ -0,0 +1,42 @@ +import { BaseError } from '@/errors/base' +import { ErrorCode } from '@/errors/codes' + +// Output formats that render the run/resume result as plain text rather than JSON/YAML. +export const TEXT_FORMATS = new Set(['', 'text']) + +// Shared by `run app` and `resume app`: --inputs (inline JSON) / --inputs-file (JSON file) / +// direct inputs are mutually exclusive ways to supply the run's variable map. +export async function resolveInputs( + inputsJson: string | undefined, + inputsFile: string | undefined, + directInputs: Readonly> | undefined, +): Promise> { + if (inputsJson !== undefined && inputsFile !== undefined) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs and --inputs-file are mutually exclusive' }) + if (inputsJson !== undefined) { + let parsed: unknown + try { + parsed = JSON.parse(inputsJson) + } + catch { + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs must be valid JSON' }) + } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs must be a JSON object' }) + return parsed as Record + } + if (inputsFile !== undefined) { + const { readFile } = await import('node:fs/promises') + let parsed: unknown + try { + parsed = JSON.parse(await readFile(inputsFile, 'utf8')) + } + catch { + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs-file must contain valid JSON' }) + } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs-file must be a JSON object' }) + return parsed as Record + } + return { ...(directInputs ?? {}) } +} diff --git a/cli/src/commands/run/app/run.test.ts b/cli/src/commands/run/app/run.test.ts index 592241cac57..57b02aeb47d 100644 --- a/cli/src/commands/run/app/run.test.ts +++ b/cli/src/commands/run/app/run.test.ts @@ -1,11 +1,12 @@ import type { DifyMock } from '@test/fixtures/dify-mock/server' import type { ActiveContext } from '@/auth/hosts' +import type { HttpClient } from '@/http/types' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { startMock } from '@test/fixtures/dify-mock/server' import { testHttpClient } from '@test/fixtures/http-client' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { loadAppInfoCache } from '@/cache/app-info' import { resumeApp } from '@/commands/resume/app/run' import { ENV_CACHE_DIR } from '@/store/dir' @@ -418,4 +419,35 @@ describe('runApp', () => { expect(docInput.transfer_method).toBe('remote_url') expect(docInput.url).toBe('https://example.com/override.pdf') }) + + it('external login: mode pre-flight calls PermittedExternalAppsClient.describe, not AppsClient.describe', async () => { + const describeResult = { info: { id: 'app-1', name: 'X', mode: 'chat', description: '', updated_at: null, service_api_enabled: true, is_agent: false }, parameters: null, input_schema: null } + const externalDescribe = vi.fn().mockResolvedValue(describeResult) + const { PermittedExternalAppsClient } = await import('@/api/permitted-external-apps') + const { AppsClient } = await import('@/api/apps') + const externalSpy = vi.spyOn(PermittedExternalAppsClient.prototype, 'describe').mockImplementation(externalDescribe) + const accountSpy = vi.spyOn(AppsClient.prototype, 'describe') + const io = bufferStreams() + const http = { baseURL: mock.url, request: vi.fn().mockResolvedValue({ answer: 'echo: hi', conversation_id: 'conv-1', message_id: 'msg-1', mode: 'chat', metadata: {} }) } as unknown as HttpClient + const activeExt: ActiveContext = { + host: mock.url, + email: 'sso@x.io', + ctx: { + account: { id: 'acct-1', email: 'sso@x.io', name: 'SSO User' }, + external_subject: { email: 'sso@x.io', issuer: 'https://issuer.example.com' }, + }, + } + try { + await runApp( + { appId: 'app-1', message: 'hi' }, + { active: activeExt, http, host: mock.url, io }, + ) + } + catch { + // run may fail due to mocked http; we only care about which describe was called + } + expect(externalSpy).toHaveBeenCalled() + expect(accountSpy).not.toHaveBeenCalled() + vi.restoreAllMocks() + }) }) diff --git a/cli/src/commands/run/app/run.ts b/cli/src/commands/run/app/run.ts index 8eb767c5dbe..ab468678a72 100644 --- a/cli/src/commands/run/app/run.ts +++ b/cli/src/commands/run/app/run.ts @@ -3,8 +3,8 @@ import type { AppInfoCache } from '@/cache/app-info' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' import { AppMetaClient } from '@/api/app-meta' +import { selectAppReader } from '@/api/app-reader' import { AppRunClient } from '@/api/app-run' -import { AppsClient } from '@/api/apps' import { FileUploadClient } from '@/api/file-upload' import { pickStrategy } from '@/commands/run/app/_strategies/index' import { BaseError, HttpClientError } from '@/errors/base' @@ -13,6 +13,7 @@ import { processExit } from '@/sys/index' import { FieldInfo } from '@/types/app-meta' import { resolveFileInputs } from './file-flags.js' import { RUN_MODES } from './handlers.js' +import { resolveInputs, TEXT_FORMATS } from './input-flags.js' export type RunAppOptions = { readonly appId: string @@ -40,45 +41,8 @@ export type RunAppDeps = { readonly exit?: (code: number) => never } -const TEXT_FORMATS = new Set(['', 'text']) - -async function resolveInputs( - inputsJson: string | undefined, - inputsFile: string | undefined, - directInputs: Readonly> | undefined, -): Promise> { - if (inputsJson !== undefined && inputsFile !== undefined) - throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs and --inputs-file are mutually exclusive' }) - if (inputsJson !== undefined) { - let parsed: unknown - try { - parsed = JSON.parse(inputsJson) - } - catch { - throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs must be valid JSON' }) - } - if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) - throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs must be a JSON object' }) - return parsed as Record - } - if (inputsFile !== undefined) { - const { readFile } = await import('node:fs/promises') - let parsed: unknown - try { - parsed = JSON.parse(await readFile(inputsFile, 'utf8')) - } - catch { - throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs-file must contain valid JSON' }) - } - if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) - throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: '--inputs-file must be a JSON object' }) - return parsed as Record - } - return { ...(directInputs ?? {}) } -} - export async function runApp(opts: RunAppOptions, deps: RunAppDeps): Promise { - const apps = new AppsClient(deps.http) + const apps = selectAppReader(deps.active, deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) try { diff --git a/cli/src/commands/tree.generated.ts b/cli/src/commands/tree.generated.ts index 03963865e56..fdaf8f269ed 100644 --- a/cli/src/commands/tree.generated.ts +++ b/cli/src/commands/tree.generated.ts @@ -17,11 +17,11 @@ import CreateMember from '@/commands/create/member/index' import DeleteMember from '@/commands/delete/member/index' import DescribeApp from '@/commands/describe/app/index' import EnvList from '@/commands/env/list/index' -import ExportApp from '@/commands/export/app/index' +import ExportStudioApp from '@/commands/export/studio-app/index' import GetApp from '@/commands/get/app/index' import GetMember from '@/commands/get/member/index' import GetWorkspace from '@/commands/get/workspace/index' -import ImportApp from '@/commands/import/app/index' +import ImportStudioApp from '@/commands/import/studio-app/index' import ResumeApp from '@/commands/resume/app/index' import RunApp from '@/commands/run/app/index' import SetMember from '@/commands/set/member/index' @@ -77,7 +77,7 @@ export const commandTree: CommandTree = { }, export: { subcommands: { - app: { command: ExportApp, subcommands: {} }, + 'studio-app': { command: ExportStudioApp, subcommands: {} }, }, }, get: { @@ -89,7 +89,7 @@ export const commandTree: CommandTree = { }, import: { subcommands: { - app: { command: ImportApp, subcommands: {} }, + 'studio-app': { command: ImportStudioApp, subcommands: {} }, }, }, resume: { diff --git a/cli/src/help/topics.ts b/cli/src/help/topics.ts index 6ccfd1044ee..de2becdfa36 100644 --- a/cli/src/help/topics.ts +++ b/cli/src/help/topics.ts @@ -22,6 +22,9 @@ const ACCOUNT_HELP_TEXT = `difyctl: account-bearer onboarding difyctl run app "hello" -o json Tips: + * Two app nouns: 'studio-app' is what you build and edit in Studio on the + web console inside a workspace (its source definition — export or move it); + 'app' is a published app you run and inspect. * 'difyctl auth list' shows your authenticated contexts; 'difyctl use host' and 'difyctl use account' switch between them. * Pass --workspace to target a non-default workspace. @@ -74,6 +77,16 @@ OUTPUT Pass -o json (or -o yaml) on every command — the JSON shape is stable and documented. Without it you get human tables meant for a terminal. +APP vs STUDIO-APP + Two nouns, two faces of the same app: + studio-app what you build and edit in Studio on the web console, + inside a workspace — the app's source definition. + app a published app, live and runnable. + Use 'studio-app' to work with the definition you manage on the website + (export it, move it between workspaces or instances); use 'app' to run + and inspect a published one. The COMMANDS list shows the verbs each + noun supports. + DISCOVERY difyctl help -o json full command tree + this contract, machine-readable difyctl get app -o json list apps (ids + modes) diff --git a/cli/src/http/error-mapper.test.ts b/cli/src/http/error-mapper.test.ts index 0a487723352..3244222da07 100644 --- a/cli/src/http/error-mapper.test.ts +++ b/cli/src/http/error-mapper.test.ts @@ -72,6 +72,25 @@ describe('classifyResponse — canonical ErrorBody', () => { }) }) +describe('classifyResponse 403', () => { + it('maps 403 to AccessDenied (exit 4 bucket)', async () => { + const req403 = new Request('https://x/openapi/v1/apps/abc/export') + const res403 = new Response( + JSON.stringify({ code: 'unsupported_token_type', message: 'unsupported_token_type', status: 403 }), + { status: 403, headers: { 'content-type': 'application/json' } }, + ) + const err = await classifyResponse(req403, res403) + expect(err.code).toBe(ErrorCode.AccessDenied) + expect(err.message).toBe('unsupported_token_type') + }) + + it('403 with no parseable ErrorBody falls back to generic denied message', async () => { + const err = await classified(403, 'not json') + expect(err.code).toBe(ErrorCode.AccessDenied) + expect(err.message).toBe('not permitted') + }) +}) + describe('classifyResponse — non-conforming bodies (no fallback by design)', () => { it('non-JSON body yields no serverError, classification by status', async () => { const err = await classified(502, 'bad gateway') diff --git a/cli/src/http/error-mapper.ts b/cli/src/http/error-mapper.ts index aca1a7e6184..34d7637d4e0 100644 --- a/cli/src/http/error-mapper.ts +++ b/cli/src/http/error-mapper.ts @@ -44,9 +44,17 @@ const RATE_LIMITED_CLASS: StatusClass = { includeRaw: false, } +const ACCESS_DENIED_CLASS: StatusClass = { + code: ErrorCode.AccessDenied, + fallbackMessage: () => 'not permitted', + includeRaw: false, +} + function statusClass(status: number): StatusClass { if (status === 401) return AUTH_EXPIRED_CLASS + if (status === 403) + return ACCESS_DENIED_CLASS if (status === 429) return RATE_LIMITED_CLASS if (status >= 500) diff --git a/cli/src/http/orpc.test.ts b/cli/src/http/orpc.test.ts index c99232b975f..0d1f2e12d57 100644 --- a/cli/src/http/orpc.test.ts +++ b/cli/src/http/orpc.test.ts @@ -44,10 +44,10 @@ describe('createOpenApiClient error mapping', () => { } it('recovers Dify message from a canonical ErrorBody 4xx response', async () => { - const caught = await classifiedError(403, { code: 'access_denied', message: 'no access', status: 403 }) + const caught = await classifiedError(422, { code: 'invalid_param', message: 'no access', status: 422 }) expect(caught.code).toBe(ErrorCode.Server4xxOther) - expect(caught.httpStatus).toBe(403) + expect(caught.httpStatus).toBe(422) expect(caught.message).toBe('no access') // Parity with the transport path: the migrated endpoint's error keeps the request // method/url and the raw body, so formatted errors still print the `request:` line diff --git a/cli/src/types/app-meta.test.ts b/cli/src/types/app-meta.test.ts index b62d81579ea..c1ac765190d 100644 --- a/cli/src/types/app-meta.test.ts +++ b/cli/src/types/app-meta.test.ts @@ -9,8 +9,6 @@ function describeResp(): AppDescribeResponse { name: 'Greeter', description: '', mode: 'chat', - author: 'tester', - tags: [], updated_at: undefined, service_api_enabled: false, is_agent: false, diff --git a/cli/test/e2e/setup/global-setup.ts b/cli/test/e2e/setup/global-setup.ts index 35e171ecafb..20b23295acb 100644 --- a/cli/test/e2e/setup/global-setup.ts +++ b/cli/test/e2e/setup/global-setup.ts @@ -519,14 +519,14 @@ async function provisionApps( async function importAppCli(filePath: string, wsId: string): Promise { const result = await run( - ['import', 'app', '--from-file', filePath, '--workspace', wsId], + ['import', 'studio-app', '--from-file', filePath, '--workspace', wsId], { configDir, timeout: 60_000 }, ) if (result.exitCode !== 0) - throw new Error(`import app failed (exit ${result.exitCode}): ${result.stderr}`) + throw new Error(`import studio-app failed (exit ${result.exitCode}): ${result.stderr}`) const match = result.stderr.match(/app ([0-9a-f-]{36})/) if (!match?.[1]) - throw new Error(`import app: could not parse app_id: ${result.stderr}`) + throw new Error(`import studio-app: could not parse app_id: ${result.stderr}`) return match[1] } diff --git a/cli/test/e2e/suites/agent/agent-skill-workflow.e2e.ts b/cli/test/e2e/suites/agent/agent-skill-workflow.e2e.ts index a1dea13e66a..dd83592be36 100644 --- a/cli/test/e2e/suites/agent/agent-skill-workflow.e2e.ts +++ b/cli/test/e2e/suites/agent/agent-skill-workflow.e2e.ts @@ -288,7 +288,7 @@ describe('E2E / agent skill — get app -o json (auth required)', () => { expect(line.trim()).not.toMatch(/\s/) }) - itWithSso('[P0] [SSO] dfoe_ get app → JSON error envelope (insufficient_scope)', async () => { + itWithSso('[P0] [SSO] dfoe_ get app -o json → permitted-apps list envelope', async () => { const tc = await withTempConfig() try { const { mkdir, writeFile } = await import('node:fs/promises') @@ -296,12 +296,21 @@ describe('E2E / agent skill — get app -o json (auth required)', () => { await mkdir(tc.configDir, { recursive: true }) await writeFile( join(tc.configDir, 'hosts.yml'), - `${[`current_host: ${E.host}`, 'token_storage: file', 'tokens:', ` bearer: ${E.ssoToken}`].join('\n')}\n`, + `${[ + `current_host: ${E.host}`, + 'token_storage: file', + 'tokens:', + ` bearer: ${E.ssoToken}`, + 'external_subject:', + ' email: sso@example.com', + ' issuer: https://issuer.example.com', + ].join('\n')}\n`, { mode: 0o600 }, ) const r = await run(['get', 'app', '-o', 'json'], { configDir: tc.configDir }) - expect(r.exitCode).not.toBe(0) - assertErrorEnvelope(r) + assertExitCode(r, 0) + const parsed = assertJson<{ data: unknown[] }>(r) + expect(Array.isArray(parsed.data), 'permitted-apps envelope has a data array').toBe(true) } finally { await tc.cleanup() } }) diff --git a/cli/test/e2e/suites/auth/whoami.e2e.ts b/cli/test/e2e/suites/auth/whoami.e2e.ts index 2caec57dd7e..69404bb9550 100644 --- a/cli/test/e2e/suites/auth/whoami.e2e.ts +++ b/cli/test/e2e/suites/auth/whoami.e2e.ts @@ -57,6 +57,8 @@ describe('E2E / difyctl auth whoami + SSO session', () => { }) } + const itWithSso = optionalIt(Boolean(E.ssoToken)) + // ── auth whoami — internal user ────────────────────────────────────────────── it('[P0] internal user auth whoami outputs email', async () => { @@ -123,12 +125,12 @@ describe('E2E / difyctl auth whoami + SSO session', () => { expect(result.exitCode).not.toBe(0) }) - it('[P0] external user get app returns insufficient_scope error', async () => { - // Spec: external user get app returns insufficient_scope + itWithSso('[P0] external user can list permitted apps via SSO token', async () => { + // External users read apps via the permitted-external surface (no workspace scope). await withSSOAuth() const result = await r(['get', 'app']) - expect(result.exitCode).not.toBe(0) - expect(result.stderr).toMatch(/insufficient|scope|workspace|SSO/i) + assertExitCode(result, 0) + expect(result.stdout).toMatch(/NAME\s+ID\s+MODE/i) }) it('[P0] external user whoami outputs SSO email', async () => { @@ -138,8 +140,6 @@ describe('E2E / difyctl auth whoami + SSO session', () => { expect(result.stdout).toContain('sso-user@example.com') }) - const itWithSso = optionalIt(Boolean(E.ssoToken)) - itWithSso('[P0] external user can execute run app using SSO token', async () => { await injectSsoAuth(configDir, { host: E.host, diff --git a/cli/test/e2e/suites/discovery/describe-app.e2e.ts b/cli/test/e2e/suites/discovery/describe-app.e2e.ts index 75cfe226370..68902a32da6 100644 --- a/cli/test/e2e/suites/discovery/describe-app.e2e.ts +++ b/cli/test/e2e/suites/discovery/describe-app.e2e.ts @@ -67,12 +67,6 @@ describe('E2E / difyctl describe app', () => { expect(result.stdout).toMatch(/Name:/i) }) - it('[P1] describe output contains Tags field', async () => { - const result = await fx.r(['describe', 'app', E.chatAppId]) - assertExitCode(result, 0) - expect(result.stdout).toMatch(/Tags:/i) - }) - // ── Input schema ────────────────────────────────────────────────────────── it('[P0] describe output contains Parameters section', async () => { @@ -172,8 +166,9 @@ describe('E2E / difyctl describe app', () => { // ── External SSO ────────────────────────────────────────────────────────── - itWithSso('[P0] external SSO user describe app returns insufficient_scope (3.86)', async () => { - // Spec 3.86: dfoe_ token → insufficient_scope, exit non-0. + itWithSso('[P0] external SSO user can describe a permitted app', async () => { + // A dfoe_ token resolves `describe app` via the permitted-external surface + // (not the account /apps surface), so a permitted app describes successfully. // Uses DIFY_E2E_SSO_TOKEN; skipped when not configured. const { mkdir, writeFile } = await import('node:fs/promises') const { join } = await import('node:path') @@ -191,8 +186,10 @@ describe('E2E / difyctl describe app', () => { ].join('\n')}\n` await writeFile(join(ssoTmp.configDir, 'hosts.yml'), hostsYml, { mode: 0o600 }) const result = await run(['describe', 'app', E.chatAppId], { configDir: ssoTmp.configDir }) - expect(result.exitCode, 'SSO user describe app should exit non-zero').not.toBe(0) - expect(result.stderr).toMatch(/insufficient_scope|scope|not_logged_in|auth/i) + assertExitCode(result, 0) + expect(result.stdout).toMatch(/ID:/i) + expect(result.stdout).toContain(E.chatAppId) + expect(result.stdout).toMatch(/Mode:/i) } finally { await ssoTmp.cleanup() @@ -225,16 +222,6 @@ describe('E2E / difyctl describe app', () => { expect(result.stdout).toContain('e2e-test') }) - it('[P1] describe output contains Author field (3.67)', async () => { - // Spec 3.67: output includes Author field when app has an author. - const result = await withRetry( - () => fx.r(['describe', 'app', E.chatAppId]), - { attempts: 3, delayMs: 2000 }, - ) - assertExitCode(result, 0) - expect(result.stdout).toMatch(/Author:/i) - }) - it('[P0] Inputs section shows parameter names (3.70)', async () => { // Spec 3.70: Parameters/Inputs section displays variable names. // workflow app has x, num, enum_var, paragraph. diff --git a/cli/test/e2e/suites/discovery/get-app-all-workspaces.e2e.ts b/cli/test/e2e/suites/discovery/get-app-all-workspaces.e2e.ts index 38d9e0a427f..54feaf9a34c 100644 --- a/cli/test/e2e/suites/discovery/get-app-all-workspaces.e2e.ts +++ b/cli/test/e2e/suites/discovery/get-app-all-workspaces.e2e.ts @@ -61,7 +61,7 @@ describe('E2E / difyctl get app -A (all-workspaces)', () => { eeIt('[EE][P0] -o wide output contains WORKSPACE column and JSON has workspace_id (3.92)', async () => { // Spec 3.92: WORKSPACE column (priority:1) appears only in -o wide mode. - // Default table shows priority:0 columns only (NAME/ID/MODE/TAGS/UPDATED). + // Default table shows priority:0 columns only (NAME/ID/MODE/UPDATED). const wideResult = await withRetry( () => fx.r(['get', 'app', '-A', '-o', 'wide']), { attempts: 3, delayMs: 2000 }, @@ -151,15 +151,15 @@ describe('E2E / difyctl get app -A (all-workspaces)', () => { // ── External SSO ────────────────────────────────────────────────────────── - itWithSso('[P0] external SSO user get app -A returns insufficient_scope error (3.103)', async () => { - // Spec 3.103: dfoe_ token on -A → insufficient_scope, exit non-0. - // Merged from two duplicate fake-token cases; now uses real DIFY_E2E_SSO_TOKEN. + itWithSso('[P0] external SSO user get app -A is rejected as an invalid flag', async () => { + // --all-workspaces is meaningless for external SSO users (no workspace + // scope), so the CLI rejects it client-side with usage_invalid_flag (exit 2). + // Uses real DIFY_E2E_SSO_TOKEN; skipped when not configured. const { mkdir, writeFile } = await import('node:fs/promises') const { join } = await import('node:path') const ssoTmp = await withTempConfig() try { await mkdir(ssoTmp.configDir, { recursive: true }) - // Use minimal SSO hosts.yml (no workspace) so CLI hits the scope/auth error path. const hostsYml = `${[ `current_host: ${E.host}`, `token_storage: file`, @@ -171,8 +171,8 @@ describe('E2E / difyctl get app -A (all-workspaces)', () => { ].join('\n')}\n` await writeFile(join(ssoTmp.configDir, 'hosts.yml'), hostsYml, { mode: 0o600 }) const result = await run(['get', 'app', '-A'], { configDir: ssoTmp.configDir }) - expect(result.exitCode, 'SSO user -A should exit non-zero').not.toBe(0) - expect(result.stderr).toMatch(/insufficient_scope|scope|not_logged_in|auth|missing/i) + assertExitCode(result, 2) + expect(result.stderr).toMatch(/--all-workspaces is not available for external logins/) } finally { await ssoTmp.cleanup() diff --git a/cli/test/e2e/suites/discovery/get-app-list.e2e.ts b/cli/test/e2e/suites/discovery/get-app-list.e2e.ts index 781d351826b..2e7623933b0 100644 --- a/cli/test/e2e/suites/discovery/get-app-list.e2e.ts +++ b/cli/test/e2e/suites/discovery/get-app-list.e2e.ts @@ -8,7 +8,6 @@ * DIFY_E2E_WORKFLOW_APP_ID — echo-workflow app */ -import { Buffer } from 'node:buffer' import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest' import { assertErrorEnvelope, @@ -99,8 +98,8 @@ describe('E2E / difyctl get app (list)', () => { it('[P1] -o wide outputs extended fields', async () => { const result = await fx.r(['get', 'app', '-o', 'wide']) assertExitCode(result, 0) - // wide adds AUTHOR and WORKSPACE columns - expect(result.stdout).toMatch(/AUTHOR|WORKSPACE/i) + // wide adds the WORKSPACE column + expect(result.stdout).toMatch(/WORKSPACE/i) }) it('[P1] output is pipe-friendly in JSON mode', async () => { @@ -206,17 +205,15 @@ describe('E2E / difyctl get app (list)', () => { // ── External SSO ────────────────────────────────────────────────────────── - itWithSso('[P0] external SSO user get app returns insufficient_scope error (3.24 / 3.25)', async () => { - // Spec 3.24: dfoe_ token → insufficient_scope; Spec 3.25: exit code is 1. + itWithSso('[P0] external SSO user can list permitted apps', async () => { + // A dfoe_ token lists apps via the permitted-external surface + // (apps:read:permitted-external scope), with no workspace scoping. // Uses DIFY_E2E_SSO_TOKEN (itWithSso skips when not configured). const { mkdir, writeFile } = await import('node:fs/promises') const { join } = await import('node:path') const ssoTmp = await withTempConfig() try { await mkdir(ssoTmp.configDir, { recursive: true }) - // SSO (dfoe_) users have apps:run scope only, not apps:list. - // Inject a minimal hosts.yml without workspace so the CLI reaches the - // scope-check path rather than resolving the workspace successfully. const hostsYml = `${[ `current_host: ${E.host}`, `token_storage: file`, @@ -228,8 +225,8 @@ describe('E2E / difyctl get app (list)', () => { ].join('\n')}\n` await writeFile(join(ssoTmp.configDir, 'hosts.yml'), hostsYml, { mode: 0o600 }) const result = await run(['get', 'app'], { configDir: ssoTmp.configDir }) - expect(result.exitCode, 'SSO user get app should exit non-zero').not.toBe(0) - expect(result.stderr).toMatch(/insufficient_scope|scope|not_logged_in|auth/i) + assertExitCode(result, 0) + expect(result.stdout).toMatch(/NAME\s+ID\s+MODE/i) } finally { await ssoTmp.cleanup() @@ -348,114 +345,4 @@ describe('E2E / difyctl get app (list)', () => { await networkTmp.cleanup() } }) - - it('[P1] --tag filter returns only apps that carry the specified tag (3.20)', async () => { - // Spec 3.20: --tag performs exact tag-name match. - // - // Before asserting: ensure echo-chat app has the 'e2e-test' tag. - // 1. GET /console/api/tags?type=app&keyword=e2e-test → find or confirm tag exists - // 2. POST /console/api/tags → create tag when absent - // 3. GET /console/api/apps/ → check existing bindings - // 4. POST /console/api/tag-bindings → bind when not yet bound - - const base = E.host.replace(/\/$/, '') - - // ── Console login: obtain cookie + CSRF (console API rejects dfoa_ Bearer) ── - const passwordB64 = Buffer.from(E.password, 'utf8').toString('base64') - const loginRes = await fetch(`${base}/console/api/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: E.email, password: passwordB64, remember_me: false }), - }) - expect(loginRes.ok, `console login failed: ${loginRes.status}`).toBe(true) - - // Helper: extract cookie string + csrf from Set-Cookie array - function parseCookies(res: Response): { cookieString: string, csrfToken: string } { - const setCookies = res.headers.getSetCookie?.() ?? [] - const cookieString = setCookies.map(kv => kv.split(';')[0]).join('; ') - const csrfPair = setCookies.map(kv => kv.split(';')[0]).filter((p): p is string => typeof p === 'string' && p.includes('csrf_token='))[0] - const csrfToken = csrfPair !== undefined - ? csrfPair.slice(csrfPair.indexOf('csrf_token=') + 'csrf_token='.length) - : '' - return { cookieString, csrfToken } - } - - let { cookieString, csrfToken } = parseCookies(loginRes) - - // ── Switch to the workspace that contains the test fixtures ────────────── - // E.workspaceId is resolved by global-setup; tag-bindings scope to the active workspace. - const switchRes = await fetch(`${base}/console/api/workspaces/switch`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Cookie': cookieString, 'X-CSRF-Token': csrfToken }, - body: JSON.stringify({ tenant_id: E.workspaceId }), - }) - // After workspace switch the server issues fresh cookies; use them for all subsequent calls. - if (switchRes.ok && switchRes.headers.getSetCookie?.().length) { - const switched = parseCookies(switchRes) - cookieString = switched.cookieString - csrfToken = switched.csrfToken - } - - const headers: Record = { - 'Content-Type': 'application/json', - 'Cookie': cookieString, - 'X-CSRF-Token': csrfToken, - } - - // ── Step 1: find the 'e2e-test' app tag ────────────────────────────────── - const tagsRes = await fetch(`${base}/console/api/tags?type=app&keyword=e2e-test`, { headers }) - expect(tagsRes.ok, `GET /tags failed: ${tagsRes.status}`).toBe(true) - const tagsList = await tagsRes.json() as Array<{ id: string, name: string }> - let tagId = tagsList.find(t => t.name === 'e2e-test')?.id - - // ── Step 2: create the tag if it doesn't exist yet ─────────────────────── - if (!tagId) { - const createRes = await fetch(`${base}/console/api/tags`, { - method: 'POST', - headers, - body: JSON.stringify({ name: 'e2e-test', type: 'app' }), - }) - expect(createRes.ok, `POST /tags failed: ${createRes.status}`).toBe(true) - const created = await createRes.json() as { id: string, name: string } - tagId = created.id - } - - expect(tagId, 'tag id must be resolved').toBeTruthy() - - // ── Step 3 & 4: bind tag idempotently (tag-bindings is idempotent on duplicates) ── - const bindRes = await fetch(`${base}/console/api/tag-bindings`, { - method: 'POST', - headers, - body: JSON.stringify({ - tag_ids: [tagId], - target_id: E.chatAppId, - type: 'app', - }), - }) - // Accept 200 (bound) or 409/4xx if already bound — binding is idempotent - expect( - bindRes.ok || bindRes.status === 409, - `POST /tag-bindings failed unexpectedly: ${bindRes.status}`, - ).toBe(true) - - // ── Assertion: difyctl --tag e2e-test returns echo-chat ────────────────── - const result = await fx.r(['get', 'app', '--tag', 'e2e-test', '-o', 'json']) - assertExitCode(result, 0) - const parsed = assertJson<{ data: Array<{ id: string, name: string, tags: Array<{ name: string }> }> }>(result) - - // echo-chat must appear in the filtered list - const echoChatInResult = parsed.data.find(app => app.id === E.chatAppId) - expect( - echoChatInResult, - `echo-chat (id=${E.chatAppId}) should appear in --tag e2e-test results`, - ).toBeDefined() - - // Every returned app must carry the e2e-test tag - parsed.data.forEach(app => - expect( - app.tags.some(t => t.name === 'e2e-test'), - `app "${app.name}" should carry the e2e-test tag`, - ).toBe(true), - ) - }) }) diff --git a/cli/test/e2e/suites/discovery/get-app-single.e2e.ts b/cli/test/e2e/suites/discovery/get-app-single.e2e.ts index b09ce25d679..b620eb383ef 100644 --- a/cli/test/e2e/suites/discovery/get-app-single.e2e.ts +++ b/cli/test/e2e/suites/discovery/get-app-single.e2e.ts @@ -68,8 +68,9 @@ describe('E2E / difyctl get app (single)', () => { // ── External SSO ────────────────────────────────────────────────────────── - itWithSso('[P0] external SSO user get app returns insufficient_scope error (3.55)', async () => { - // Spec 3.55: dfoe_ token on get app → insufficient_scope, exit 1. + itWithSso('[P0] external SSO user can get a permitted app by id', async () => { + // A dfoe_ token resolves get app via the permitted-external describe + // surface (apps:read:permitted-external scope), so a permitted app is returned. // Uses DIFY_E2E_SSO_TOKEN; skipped when not configured. const { mkdir, writeFile } = await import('node:fs/promises') const { join } = await import('node:path') @@ -87,8 +88,8 @@ describe('E2E / difyctl get app (single)', () => { ].join('\n')}\n` await writeFile(join(ssoTmp.configDir, 'hosts.yml'), hostsYml, { mode: 0o600 }) const result = await run(['get', 'app', E.chatAppId], { configDir: ssoTmp.configDir }) - expect(result.exitCode, 'SSO user get app should exit non-zero').not.toBe(0) - expect(result.stderr).toMatch(/insufficient_scope|scope|not_logged_in|auth/i) + assertExitCode(result, 0) + expect(result.stdout).toContain(E.chatAppId) } finally { await ssoTmp.cleanup() @@ -153,13 +154,13 @@ describe('E2E / difyctl get app (single)', () => { }) it('[P1] get app -o wide outputs extended columns (3.48)', async () => { - // Spec 3.48: -o wide → TAGS/UPDATED/AUTHOR columns, exit 0. + // Spec 3.48: -o wide → UPDATED/WORKSPACE columns, exit 0. const result = await withRetry( () => fx.r(['get', 'app', E.chatAppId, '-o', 'wide']), { attempts: 3, delayMs: 2000 }, ) assertExitCode(result, 0) - expect(result.stdout).toMatch(/AUTHOR|UPDATED|TAGS/i) + expect(result.stdout).toMatch(/UPDATED|WORKSPACE/i) }) it('[P1] get app -o json is pipe-friendly with no ANSI (3.49)', async () => { diff --git a/cli/test/e2e/suites/dsl/export-app.e2e.ts b/cli/test/e2e/suites/dsl/export-studio-app.e2e.ts similarity index 82% rename from cli/test/e2e/suites/dsl/export-app.e2e.ts rename to cli/test/e2e/suites/dsl/export-studio-app.e2e.ts index f96fbd216a4..e158ceaccec 100644 --- a/cli/test/e2e/suites/dsl/export-app.e2e.ts +++ b/cli/test/e2e/suites/dsl/export-studio-app.e2e.ts @@ -1,5 +1,5 @@ /** - * E2E: difyctl export app — DSL export + * E2E: difyctl export studio-app — DSL export * * Prerequisites (DIFY_E2E_* env vars): * DIFY_E2E_WORKFLOW_APP_ID — echo-workflow app (no model provider dependency) @@ -21,7 +21,7 @@ import { resolveEnv } from '../../setup/env.js' const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities const E = resolveEnv(caps) -describe('E2E / difyctl export app', () => { +describe('E2E / difyctl export studio-app', () => { let fx: AuthFixture beforeEach(async () => { @@ -34,37 +34,37 @@ describe('E2E / difyctl export app', () => { // ── Basic export ────────────────────────────────────────────────────────── it('[P0] exported DSL is non-empty YAML printed to stdout', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId]) assertExitCode(result, 0) expect(result.stdout.trim().length).toBeGreaterThan(0) }) it('[P0] exported YAML contains kind: app', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId]) assertExitCode(result, 0) expect(result.stdout).toMatch(/^kind:\s*app/m) }) it('[P0] exported YAML contains version field', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId]) assertExitCode(result, 0) expect(result.stdout).toMatch(/^version:/m) }) it('[P0] exported YAML contains app section with mode', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId]) assertExitCode(result, 0) expect(result.stdout).toMatch(/^\s+mode:/m) }) it('[P1] exported YAML ends with a newline (POSIX pipe convention)', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId]) assertExitCode(result, 0) expect(result.stdout.endsWith('\n')).toBe(true) }) it('[P1] chat app export also succeeds and includes mode', async () => { - const result = await fx.r(['export', 'app', E.chatAppId]) + const result = await fx.r(['export', 'studio-app', E.chatAppId]) assertExitCode(result, 0) expect(result.stdout).toMatch(/^kind:\s*app/m) expect(result.stdout).toMatch(/^\s+mode:/m) @@ -76,7 +76,7 @@ describe('E2E / difyctl export app', () => { const dir = await mkdtemp(join(tmpdir(), 'difyctl-e2e-export-')) const outPath = join(dir, 'exported.yaml') try { - const result = await fx.r(['export', 'app', E.workflowAppId, '--output', outPath]) + const result = await fx.r(['export', 'studio-app', E.workflowAppId, '--output', outPath]) assertExitCode(result, 0) const content = await readFile(outPath, 'utf8') expect(content).toMatch(/^kind:\s*app/m) @@ -92,8 +92,8 @@ describe('E2E / difyctl export app', () => { const outPath = join(dir, 'exported.yaml') try { const [stdoutResult, fileResult] = await Promise.all([ - fx.r(['export', 'app', E.workflowAppId]), - fx.r(['export', 'app', E.workflowAppId, '--output', outPath]).then(async (r) => { + fx.r(['export', 'studio-app', E.workflowAppId]), + fx.r(['export', 'studio-app', E.workflowAppId, '--output', outPath]).then(async (r) => { const content = await readFile(outPath, 'utf8') return { exitCode: r.exitCode, content } }), @@ -113,12 +113,12 @@ describe('E2E / difyctl export app', () => { const dir = await mkdtemp(join(tmpdir(), 'difyctl-e2e-roundtrip-')) const dslPath = join(dir, 'roundtrip.yaml') try { - const exportResult = await fx.r(['export', 'app', E.workflowAppId, '--output', dslPath]) + const exportResult = await fx.r(['export', 'studio-app', E.workflowAppId, '--output', dslPath]) assertExitCode(exportResult, 0) const importResult = await fx.r([ 'import', - 'app', + 'studio-app', '--from-file', dslPath, '--name', @@ -137,7 +137,7 @@ describe('E2E / difyctl export app', () => { // ── Error scenarios ─────────────────────────────────────────────────────── it('[P0] non-existent app returns exit code 1 with error in stderr', async () => { - const result = await fx.r(['export', 'app', 'nonexistent-app-id-export-e2e']) + const result = await fx.r(['export', 'studio-app', 'nonexistent-app-id-export-e2e']) expect(result.exitCode).toBe(1) expect(result.stderr.length).toBeGreaterThan(0) }) @@ -145,7 +145,7 @@ describe('E2E / difyctl export app', () => { it('[P0] unauthenticated export returns auth error (exit code 4)', async () => { const unauthTmp = await withTempConfig() try { - const result = await run(['export', 'app', E.workflowAppId], { + const result = await run(['export', 'studio-app', E.workflowAppId], { configDir: unauthTmp.configDir, }) assertExitCode(result, 4) @@ -156,13 +156,13 @@ describe('E2E / difyctl export app', () => { }) it('[P1] export with missing app id argument exits non-zero', async () => { - const result = await fx.r(['export', 'app']) + const result = await fx.r(['export', 'studio-app']) expect(result.exitCode).not.toBe(0) expect(result.stderr).toMatch(/missing required argument|required|app id/i) }) it('[P1] malformed --workflow-id returns a 4xx, not a 5xx', async () => { - const result = await fx.r(['export', 'app', E.workflowAppId, '--workflow-id', 'not-a-uuid']) + const result = await fx.r(['export', 'studio-app', E.workflowAppId, '--workflow-id', 'not-a-uuid']) expect(result.exitCode).not.toBe(0) expect(result.stderr).toMatch(/http_status:\s*4\d\d/) expect(result.stderr).not.toMatch(/http_status:\s*5\d\d/) @@ -171,7 +171,7 @@ describe('E2E / difyctl export app', () => { it('[P1] non-existent --workflow-id returns 404, not a 5xx', async () => { const result = await fx.r([ 'export', - 'app', + 'studio-app', E.workflowAppId, '--workflow-id', '00000000-0000-0000-0000-000000000000', @@ -184,7 +184,7 @@ describe('E2E / difyctl export app', () => { const dir = await mkdtemp(join(tmpdir(), 'difyctl-e2e-export-nofile-')) const outPath = join(dir, 'should-not-exist.yaml') try { - const result = await fx.r(['export', 'app', 'nonexistent-app-id-nofile-e2e', '--output', outPath]) + const result = await fx.r(['export', 'studio-app', 'nonexistent-app-id-nofile-e2e', '--output', outPath]) expect(result.exitCode).not.toBe(0) const exists = await readFile(outPath, 'utf8').then(() => true).catch(() => false) expect(exists, 'output file must not be created on export failure').toBe(false) diff --git a/cli/test/e2e/suites/error-handling/error-messages.e2e.ts b/cli/test/e2e/suites/error-handling/error-messages.e2e.ts index 932998d9afe..5c4a9f79fa0 100644 --- a/cli/test/e2e/suites/error-handling/error-messages.e2e.ts +++ b/cli/test/e2e/suites/error-handling/error-messages.e2e.ts @@ -82,10 +82,9 @@ describe('E2E / error message standards (spec 5.3)', () => { // ── 5.63 dfoe_ token insufficient_scope ────────────────────────────────── - itWithSso('[P0] 5.63 dfoe_ SSO token with workspace returns insufficient_scope for management commands', async () => { - // Spec 5.63: an external SSO token (dfoe_) must not be able to access - // internal management APIs; the CLI must return an insufficient_scope - // error with exit 1. + itWithSso('[P0] dfoe_ SSO token is denied account-only management commands', async () => { + // A dfoe_ SSO token is rejected with a non-zero exit when it targets an + // account-only management command (`export studio-app`). const { mkdir } = await import('node:fs/promises') const ssoTmp = await withTempConfig() try { @@ -95,16 +94,13 @@ describe('E2E / error message standards (spec 5.3)', () => { `token_storage: file`, `tokens:`, ` bearer: ${E.ssoToken}`, - `workspace:`, - ` id: ${E.workspaceId}`, - ` name: "${E.workspaceName}"`, - ` role: member`, + `external_subject:`, + ` email: sso@example.com`, + ` issuer: https://issuer.example.com`, ].join('\n')}\n` await writeFile(join(ssoTmp.configDir, 'hosts.yml'), hostsYml, { mode: 0o600 }) - const result = await run(['get', 'app'], { configDir: ssoTmp.configDir }) + const result = await run(['export', 'studio-app', E.chatAppId], { configDir: ssoTmp.configDir }) assertNonZeroExit(result) - // In this environment ssoToken may be a dfoa_ token; the server returns - // either insufficient_scope or server_5xx — both are non-zero exits. expect(result.stderr.trim().length, 'stderr must contain an error message').toBeGreaterThan(0) } finally { diff --git a/cli/test/e2e/suites/output/table-output.e2e.ts b/cli/test/e2e/suites/output/table-output.e2e.ts index c3300f8bec0..ecfb0577d96 100644 --- a/cli/test/e2e/suites/output/table-output.e2e.ts +++ b/cli/test/e2e/suites/output/table-output.e2e.ts @@ -41,7 +41,7 @@ import type { AuthFixture } from '../../helpers/cli.js' import { afterEach, beforeEach, describe, expect, it, inject } from 'vitest' import { assertExitCode, assertNoAnsi } from '../../helpers/assert.js' import { withAuthFixture } from '../../helpers/cli.js' -import { loadE2EEnv, resolveEnv } from '../../setup/env.js' +import { resolveEnv } from '../../setup/env.js' // @ts-expect-error — see test/e2e/helpers/vitest-context.ts for explanation const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities @@ -65,19 +65,18 @@ describe('E2E / table output — header and column format (spec 5.1–5.19)', () expect(result.stdout.trim().length).toBeGreaterThan(0) }) - it('[P0] 5.2 header row contains all five expected column names', async () => { - // Spec 5.2: header columns are NAME / ID / MODE / TAGS / UPDATED. + it('[P0] 5.2 header row contains all four expected column names', async () => { + // Spec 5.2: header columns are NAME / ID / MODE / UPDATED. const result = await fx.r(['get', 'app']) assertExitCode(result, 0) const header = result.stdout.split('\n')[0] ?? '' expect(header).toMatch(/NAME/i) expect(header).toMatch(/ID/i) expect(header).toMatch(/MODE/i) - expect(header).toMatch(/TAGS/i) expect(header).toMatch(/UPDATED/i) }) - it('[P0] 5.3 column order is NAME → ID → MODE → TAGS → UPDATED', async () => { + it('[P0] 5.3 column order is NAME → ID → MODE → UPDATED', async () => { // Spec 5.3: columns appear in the defined order (as verified from actual CLI output). const result = await fx.r(['get', 'app']) assertExitCode(result, 0) @@ -85,19 +84,16 @@ describe('E2E / table output — header and column format (spec 5.1–5.19)', () const nameIdx = header.indexOf('NAME') const idIdx = header.indexOf('ID') const modeIdx = header.indexOf('MODE') - const tagsIdx = header.indexOf('TAGS') const updatedIdx = header.indexOf('UPDATED') // All columns must be present expect(nameIdx).toBeGreaterThanOrEqual(0) expect(idIdx).toBeGreaterThanOrEqual(0) expect(modeIdx).toBeGreaterThanOrEqual(0) - expect(tagsIdx).toBeGreaterThanOrEqual(0) expect(updatedIdx).toBeGreaterThanOrEqual(0) // Verify left-to-right order expect(nameIdx).toBeLessThan(idIdx) expect(idIdx).toBeLessThan(modeIdx) - expect(modeIdx).toBeLessThan(tagsIdx) - expect(tagsIdx).toBeLessThan(updatedIdx) + expect(modeIdx).toBeLessThan(updatedIdx) }) it('[P0] 5.5 table displays multiple data rows when more than one app exists', async () => { @@ -153,32 +149,6 @@ describe('E2E / table output — header and column format (spec 5.1–5.19)', () expect(result.stdout).not.toMatch(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/) }) - // ── 5.17 — Empty-field rendering ───────────────────────────────────────── - - it('[P1] 5.17 empty TAGS field is rendered as blank — not as a dash (-)', async () => { - // Spec 5.17: empty fields show blank, not the `-` placeholder. - // Most apps in the fixture workspace have no tags. - const result = await fx.r(['get', 'app']) - assertExitCode(result, 0) - const lines = result.stdout.trim().split('\n') - const header = lines[0] ?? '' - const tagsStart = header.indexOf('TAGS') - const updatedStart = header.indexOf('UPDATED') - // Check at least one data row: the TAGS slice should be blank, not '-' - const dataLines = lines.slice(1).filter(l => l.trim()) - if (dataLines.length > 0 && tagsStart >= 0 && updatedStart > tagsStart) { - const tagsSlice = (dataLines[0] ?? '').substring(tagsStart, updatedStart).trim() - // If there are no tags, the slice should be empty (not contain a lone '-') - if (tagsSlice === '') { - expect(tagsSlice).toBe('') - } - else { - // Tags are present — just verify it's not the placeholder dash - expect(tagsSlice).not.toBe('-') - } - } - }) - // ── 5.25 — Performance ──────────────────────────────────────────────────── it('[P1] 5.25 querying up to 100 apps completes without timeout', async () => { diff --git a/cli/test/fixtures/dify-mock/server.ts b/cli/test/fixtures/dify-mock/server.ts index c9d6fe7a865..4b119286dbc 100644 --- a/cli/test/fixtures/dify-mock/server.ts +++ b/cli/test/fixtures/dify-mock/server.ts @@ -269,8 +269,34 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono { name: app.name, description: app.description, mode: app.mode, - author: app.author ?? '', - tags: app.tags, + updated_at: app.updated_at, + service_api_enabled: app.service_api_enabled ?? false, + is_agent: app.is_agent ?? false, + } + : null, + parameters: wantParams ? (app.parameters ?? null) : null, + input_schema: wantInputSchema ? (app.input_schema ?? null) : null, + }) + }) + + app.get('/openapi/v1/permitted-external-apps/:id/describe', (c) => { + const id = c.req.param('id') + const fieldsRaw = c.req.query('fields') ?? '' + const fields = fieldsRaw === '' ? [] : fieldsRaw.split(',').map(s => s.trim()).filter(s => s !== '') + // External subjects have no workspace scope; the app is reachable across workspaces. + const app = APPS.find(a => a.id === id) + if (app === undefined) + return c.json({ error: { code: 'not_found', message: 'app not found' } }, { status: 404 }) + const wantInfo = fields.length === 0 || fields.includes('info') + const wantParams = fields.length === 0 || fields.includes('parameters') + const wantInputSchema = fields.length === 0 || fields.includes('input_schema') + return c.json({ + info: wantInfo + ? { + id: app.id, + name: app.name, + description: app.description, + mode: app.mode, updated_at: app.updated_at, service_api_enabled: app.service_api_enabled ?? false, is_agent: app.is_agent ?? false, diff --git a/packages/contracts/generated/api/openapi/orpc.gen.ts b/packages/contracts/generated/api/openapi/orpc.gen.ts index bc7cbea340b..47aa1b90d6a 100644 --- a/packages/contracts/generated/api/openapi/orpc.gen.ts +++ b/packages/contracts/generated/api/openapi/orpc.gen.ts @@ -30,6 +30,9 @@ import { zGetHealthResponse, zGetOauthDeviceLookupQuery, zGetOauthDeviceLookupResponse, + zGetPermittedExternalAppsByAppIdDescribePath, + zGetPermittedExternalAppsByAppIdDescribeQuery, + zGetPermittedExternalAppsByAppIdDescribeResponse, zGetPermittedExternalAppsQuery, zGetPermittedExternalAppsResponse, zGetVersionResponse, @@ -450,6 +453,30 @@ export const oauth = { } export const get12 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getPermittedExternalAppsByAppIdDescribe', + path: '/permitted-external-apps/{app_id}/describe', + tags: ['openapi'], + }) + .input( + z.object({ + params: zGetPermittedExternalAppsByAppIdDescribePath, + query: zGetPermittedExternalAppsByAppIdDescribeQuery.optional(), + }), + ) + .output(zGetPermittedExternalAppsByAppIdDescribeResponse) + +export const describe2 = { + get: get12, +} + +export const byAppId2 = { + describe: describe2, +} + +export const get13 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -461,7 +488,8 @@ export const get12 = oc .output(zGetPermittedExternalAppsResponse) export const permittedExternalApps = { - get: get12, + get: get13, + byAppId: byAppId2, } export const post9 = oc @@ -544,7 +572,7 @@ export const byMemberId = { role, } -export const get13 = oc +export const get14 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -578,7 +606,7 @@ export const post11 = oc .output(zPostWorkspacesByWorkspaceIdMembersResponse) export const members = { - get: get13, + get: get14, post: post11, byMemberId, } @@ -598,7 +626,7 @@ export const switch_ = { post: post12, } -export const get14 = oc +export const get15 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -610,13 +638,13 @@ export const get14 = oc .output(zGetWorkspacesByWorkspaceIdResponse) export const byWorkspaceId = { - get: get14, + get: get15, apps: apps2, members, switch: switch_, } -export const get15 = oc +export const get16 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -627,7 +655,7 @@ export const get15 = oc .output(zGetWorkspacesResponse) export const workspaces = { - get: get15, + get: get16, byWorkspaceId, } diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index 14f8ef0a818..185ee37aa6f 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -20,14 +20,12 @@ export type AccountResponse = { } export type AppDescribeInfo = { - author?: string | null description?: string | null id: string is_agent?: boolean mode: string name: string service_api_enabled: boolean - tags?: Array updated_at?: string | null } @@ -66,13 +64,11 @@ export type AppDslImportPayload = { yaml_url?: string | null } -export type AppInfoResponse = { - author?: string | null +export type AppInfo = { description?: string | null id: string mode: string name: string - tags?: Array } export type AppListQuery = { @@ -80,7 +76,6 @@ export type AppListQuery = { mode?: AppMode | null name?: string | null page?: number - tag?: string | null workspace_id: string } @@ -93,12 +88,10 @@ export type AppListResponse = { } export type AppListRow = { - created_by_name?: string | null description?: string | null id: string mode: AppMode name: string - tags?: Array updated_at?: string | null workspace_id?: string | null workspace_name?: string | null @@ -412,10 +405,6 @@ export type SessionRow = { prefix: string } -export type TagItem = { - name: string -} - export type TaskStopResponse = { result: 'success' } @@ -611,7 +600,6 @@ export type GetAppsData = { | 'workflow' name?: string page?: number - tag?: string workspace_id: string } url: '/apps' @@ -947,6 +935,32 @@ export type GetPermittedExternalAppsResponses = { export type GetPermittedExternalAppsResponse = GetPermittedExternalAppsResponses[keyof GetPermittedExternalAppsResponses] +export type GetPermittedExternalAppsByAppIdDescribeData = { + body?: never + path: { + app_id: string + } + query?: { + fields?: string + } + url: '/permitted-external-apps/{app_id}/describe' +} + +export type GetPermittedExternalAppsByAppIdDescribeErrors = { + 422: ErrorBody + default: ErrorBody +} + +export type GetPermittedExternalAppsByAppIdDescribeError + = GetPermittedExternalAppsByAppIdDescribeErrors[keyof GetPermittedExternalAppsByAppIdDescribeErrors] + +export type GetPermittedExternalAppsByAppIdDescribeResponses = { + 200: AppDescribeResponse +} + +export type GetPermittedExternalAppsByAppIdDescribeResponse + = GetPermittedExternalAppsByAppIdDescribeResponses[keyof GetPermittedExternalAppsByAppIdDescribeResponses] + export type GetWorkspacesData = { body?: never path?: never diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index 57d65e62c0e..804c75394f6 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -11,6 +11,19 @@ export const zAccountPayload = z.object({ name: z.string(), }) +/** + * AppDescribeInfo + */ +export const zAppDescribeInfo = z.object({ + description: z.string().nullish(), + id: z.string(), + is_agent: z.boolean().optional().default(false), + mode: z.string(), + name: z.string(), + service_api_enabled: z.boolean(), + updated_at: z.string().nullish(), +}) + /** * AppDescribeQuery * @@ -22,6 +35,15 @@ export const zAppDescribeQuery = z.object({ fields: z.string().optional(), }) +/** + * AppDescribeResponse + */ +export const zAppDescribeResponse = z.object({ + info: zAppDescribeInfo.nullish(), + input_schema: z.record(z.string(), z.unknown()).nullish(), + parameters: z.record(z.string(), z.unknown()).nullish(), +}) + /** * AppDslExportQuery * @@ -58,6 +80,16 @@ export const zAppDslImportPayload = z.object({ yaml_url: z.string().nullish(), }) +/** + * AppInfo + */ +export const zAppInfo = z.object({ + description: z.string().nullish(), + id: z.string(), + mode: z.string(), + name: z.string(), +}) + /** * AppMode */ @@ -82,10 +114,33 @@ export const zAppListQuery = z.object({ mode: zAppMode.nullish(), name: z.string().max(200).nullish(), page: z.int().gte(1).optional().default(1), - tag: z.string().max(100).nullish(), workspace_id: z.string(), }) +/** + * AppListRow + */ +export const zAppListRow = z.object({ + description: z.string().nullish(), + id: z.string(), + mode: zAppMode, + name: z.string(), + updated_at: z.string().nullish(), + workspace_id: z.string().nullish(), + workspace_name: z.string().nullish(), +}) + +/** + * AppListResponse + */ +export const zAppListResponse = z.object({ + data: z.array(zAppListRow), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * AppRunRequest */ @@ -409,6 +464,17 @@ export const zPermittedExternalAppsListQuery = z.object({ page: z.int().gte(1).optional().default(1), }) +/** + * PermittedExternalAppsListResponse + */ +export const zPermittedExternalAppsListResponse = z.object({ + data: z.array(zAppListRow), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * RevokeResponse */ @@ -460,86 +526,6 @@ export const zSessionListResponse = z.object({ total: z.int(), }) -/** - * TagItem - */ -export const zTagItem = z.object({ - name: z.string(), -}) - -/** - * AppDescribeInfo - */ -export const zAppDescribeInfo = z.object({ - author: z.string().nullish(), - description: z.string().nullish(), - id: z.string(), - is_agent: z.boolean().optional().default(false), - mode: z.string(), - name: z.string(), - service_api_enabled: z.boolean(), - tags: z.array(zTagItem).optional().default([]), - updated_at: z.string().nullish(), -}) - -/** - * AppDescribeResponse - */ -export const zAppDescribeResponse = z.object({ - info: zAppDescribeInfo.nullish(), - input_schema: z.record(z.string(), z.unknown()).nullish(), - parameters: z.record(z.string(), z.unknown()).nullish(), -}) - -/** - * AppInfoResponse - */ -export const zAppInfoResponse = z.object({ - author: z.string().nullish(), - description: z.string().nullish(), - id: z.string(), - mode: z.string(), - name: z.string(), - tags: z.array(zTagItem).optional().default([]), -}) - -/** - * AppListRow - */ -export const zAppListRow = z.object({ - created_by_name: z.string().nullish(), - description: z.string().nullish(), - id: z.string(), - mode: zAppMode, - name: z.string(), - tags: z.array(zTagItem).optional().default([]), - updated_at: z.string().nullish(), - workspace_id: z.string().nullish(), - workspace_name: z.string().nullish(), -}) - -/** - * AppListResponse - */ -export const zAppListResponse = z.object({ - data: z.array(zAppListRow), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - -/** - * PermittedExternalAppsListResponse - */ -export const zPermittedExternalAppsListResponse = z.object({ - data: z.array(zAppListRow), - has_more: z.boolean(), - limit: z.int(), - page: z.int(), - total: z.int(), -}) - /** * TaskStopResponse * @@ -726,7 +712,6 @@ export const zGetAppsQuery = z.object({ .optional(), name: z.string().max(200).optional(), page: z.int().gte(1).optional().default(1), - tag: z.string().max(100).optional(), workspace_id: z.string(), }) @@ -898,6 +883,19 @@ export const zGetPermittedExternalAppsQuery = z.object({ */ export const zGetPermittedExternalAppsResponse = zPermittedExternalAppsListResponse +export const zGetPermittedExternalAppsByAppIdDescribePath = z.object({ + app_id: z.string(), +}) + +export const zGetPermittedExternalAppsByAppIdDescribeQuery = z.object({ + fields: z.string().optional(), +}) + +/** + * Permitted external app description + */ +export const zGetPermittedExternalAppsByAppIdDescribeResponse = zAppDescribeResponse + /** * Workspace list */ From 25b90229bce3a48ec7f8093615cd7666e3268be4 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Mon, 22 Jun 2026 16:14:08 +0800 Subject: [PATCH 17/35] fix: disable deployment DSL imports (#37745) --- .../state/__tests__/index.spec.ts | 73 +++++++++++++++++++ .../deployments/create-guide/state/index.ts | 28 ++++--- .../ui/__tests__/source-step.spec.tsx | 49 +++++++++++++ .../create-guide/ui/release-step.tsx | 4 +- .../create-guide/ui/source-step.tsx | 21 +++--- .../ui/__tests__/source-app-picker.spec.tsx | 34 +++++++++ .../use-release-content-check.spec.ts | 36 +++++++++ .../deployments/create-release/ui/dialog.tsx | 11 ++- .../create-release/ui/source-app-picker.tsx | 30 ++++++-- .../create-release/ui/source-section.tsx | 63 ++++++++-------- .../ui/use-create-release-submission.ts | 2 +- .../ui/use-release-content-check.ts | 12 ++- .../shared/domain/feature-flags.ts | 2 + 13 files changed, 301 insertions(+), 64 deletions(-) create mode 100644 web/features/deployments/create-guide/state/__tests__/index.spec.ts create mode 100644 web/features/deployments/create-guide/ui/__tests__/source-step.spec.tsx create mode 100644 web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx create mode 100644 web/features/deployments/create-release/ui/__tests__/use-release-content-check.spec.ts create mode 100644 web/features/deployments/shared/domain/feature-flags.ts diff --git a/web/features/deployments/create-guide/state/__tests__/index.spec.ts b/web/features/deployments/create-guide/state/__tests__/index.spec.ts new file mode 100644 index 00000000000..910a7b84bb2 --- /dev/null +++ b/web/features/deployments/create-guide/state/__tests__/index.spec.ts @@ -0,0 +1,73 @@ +import type { Getter } from 'jotai' +import { atom, createStore } from 'jotai' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('jotai-tanstack-query', () => ({ + atomWithInfiniteQuery: (createOptions: (get: Getter) => Record) => atom((get) => { + const options = createOptions(get) + + return { + ...options, + data: { pages: [{ data: [] }] }, + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + isLoading: false, + isPlaceholderData: false, + fetchNextPage: vi.fn(), + } + }), + atomWithMutation: () => atom(() => ({ + isPending: false, + mutateAsync: vi.fn(), + })), + atomWithQuery: (createOptions: (get: Getter) => Record) => atom(get => ({ + ...createOptions(get), + data: undefined, + isError: false, + isFetching: false, + isLoading: false, + isSuccess: false, + })), +})) + +vi.mock('@/service/client', () => ({ + consoleQuery: { + apps: { + list: { + infiniteOptions: (options: Record) => ({ + ...options, + queryKey: ['apps', 'list'], + }), + }, + }, + }, +})) + +async function loadState() { + return await import('../index') +} + +describe('create deployment guide state', () => { + it('should keep the guide on source app mode when DSL import is disabled', async () => { + const state = await loadState() + const store = createStore() + + store.set(state.selectMethodAtom, 'importDsl') + + expect(store.get(state.methodAtom)).toBe('bindApp') + expect(store.get(state.effectiveMethodAtom)).toBe('bindApp') + }) + + it('should keep source app loading enabled if stale state points to DSL import', async () => { + const state = await loadState() + const store = createStore() + + store.set(state.methodAtom, 'importDsl') + + const sourceAppsQuery = store.get(state.sourceAppsQueryAtom) as unknown as { enabled?: boolean } + + expect(store.get(state.effectiveMethodAtom)).toBe('bindApp') + expect(sourceAppsQuery.enabled).toBe(true) + }) +}) diff --git a/web/features/deployments/create-guide/state/index.ts b/web/features/deployments/create-guide/state/index.ts index 06dab290172..8108780bff8 100644 --- a/web/features/deployments/create-guide/state/index.ts +++ b/web/features/deployments/create-guide/state/index.ts @@ -27,6 +27,7 @@ import { isWorkflowDsl, } from '@/features/deployments/shared/domain/dsl' import { unsupportedDslNodeError } from '@/features/deployments/shared/domain/error' +import { isDeploymentDslImportEnabled } from '@/features/deployments/shared/domain/feature-flags' import { createDeploymentIdempotencyKey } from '@/features/deployments/shared/domain/idempotency' import { DEPLOYMENT_PAGE_SIZE, @@ -41,6 +42,12 @@ export type GuideMethod = 'bindApp' | 'importDsl' export type GuideStep = 'source' | 'release' | 'target' export type WorkflowSourceApp = App & { mode: Extract } +function deploymentGuideMethod(method: GuideMethod): GuideMethod { + return method === 'importDsl' && !isDeploymentDslImportEnabled + ? 'bindApp' + : method +} + const RANDOM_SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz' const RANDOM_SUFFIX_LENGTH = 4 const RANDOM_SUFFIX_FALLBACK_LENGTH = 6 @@ -124,6 +131,7 @@ function envVarInput(slot: EnvVarBindingSlot, selection: EnvVarValueSelection | // Workflow primitives export const stepAtom = atom('source') export const methodAtom = atom('bindApp') +export const effectiveMethodAtom = atom(get => deploymentGuideMethod(get(methodAtom))) // Source primitives export const sourceSearchTextAtom = atom('') @@ -145,7 +153,7 @@ export const dslDefaultAppNameAtom = atom((get) => { export const dslUnsupportedModeAtom = atom((get) => { const dslContent = get(dslContentAtom) - return get(methodAtom) === 'importDsl' + return get(effectiveMethodAtom) === 'importDsl' && Boolean(dslContent.trim()) && !get(isReadingDslAtom) && !get(dslReadErrorAtom) @@ -199,7 +207,7 @@ export const sourceAppsQueryAtom = atomWithInfiniteQuery((get) => { initialPageParam: 1, placeholderData: keepPreviousData, }), - enabled: get(methodAtom) === 'bindApp', + enabled: get(effectiveMethodAtom) === 'bindApp', } }) @@ -218,7 +226,7 @@ export const effectiveSelectedAppAtom = atom((get) => { }) function sourceReady(get: Getter) { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) return method === 'importDsl' ? get(importDslReadyAtom) @@ -269,7 +277,7 @@ export const deployableEnvironmentsQueryAtom = atomWithQuery((get) => { }) const precheckReleaseQueryAtom = atomWithQuery((get) => { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const effectiveSelectedApp = get(effectiveSelectedAppAtom) const dslContent = get(dslContentAtom) const enabled = sourceReady(get) @@ -310,7 +318,7 @@ function precheckReleaseReady(get: Getter) { } export const deploymentOptionsQueryAtom = atomWithQuery((get) => { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const effectiveSelectedApp = get(effectiveSelectedAppAtom) const dslContent = get(dslContentAtom) const enabled = precheckReleaseReady(get) @@ -378,7 +386,7 @@ const deploymentOptionsContentCheckedAtom = atom((get) => { }) export const sourceCanGoNextAtom = atom((get) => { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const effectiveSelectedApp = get(effectiveSelectedAppAtom) const importDslReady = method === 'importDsl' && get(importDslReadyAtom) const bindAppReady = method === 'bindApp' && Boolean(effectiveSelectedApp?.id) @@ -416,7 +424,7 @@ export const continueFromSourceAtom = atom(null, (get, set, { if (!get(sourceCanGoNextAtom)) return - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const effectiveSelectedApp = get(effectiveSelectedAppAtom) if (method === 'bindApp' && effectiveSelectedApp) set(selectSourceAppAtom, effectiveSelectedApp) @@ -606,7 +614,7 @@ const requiredBindingsReadyAtom = atom((get) => { }) export const deploymentTargetEnvVarSlotsAtom = atom((get) => { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const deploymentOptionsQuery = get(deploymentOptionsQueryAtom) const slots = sourceReady(get) ? deploymentOptionsQuery.data?.options?.envVarSlots : undefined const dslContent = get(dslContentAtom) @@ -702,7 +710,7 @@ export const setEnvVarAtom = atom(null, (get, set, key: string, value: EnvVarVal // Workflow actions export const selectMethodAtom = atom(null, (_get, set, method: GuideMethod) => { - set(methodAtom, method) + set(methodAtom, deploymentGuideMethod(method)) set(selectedEnvironmentIdAtom, '') set(manualBindingSelectionsAtom, {}) set(envVarValuesAtom, {}) @@ -738,7 +746,7 @@ export const createDeploymentGuideSubmissionAtom = atom(null, async (get, set, { }: { deployToEnvironment: boolean }) => { - const method = get(methodAtom) + const method = get(effectiveMethodAtom) const dslContent = get(dslContentAtom) const submittedInstanceName = get(instanceNameAtom).trim() const submittedReleaseName = get(releaseNameAtom).trim() diff --git a/web/features/deployments/create-guide/ui/__tests__/source-step.spec.tsx b/web/features/deployments/create-guide/ui/__tests__/source-step.spec.tsx new file mode 100644 index 00000000000..3a99e1418a7 --- /dev/null +++ b/web/features/deployments/create-guide/ui/__tests__/source-step.spec.tsx @@ -0,0 +1,49 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { SourceStepContent } from '../source-step' + +vi.mock('@/features/deployments/create-guide/state', async () => { + const { atom } = await import('jotai') + const methodAtom = atom<'bindApp' | 'importDsl'>('bindApp') + const emptyActionAtom = atom(null, () => undefined) + + return { + continueFromSourceAtom: emptyActionAtom, + dslFileAtom: atom(undefined), + dslReadErrorAtom: atom(false), + dslUnsupportedModeAtom: atom(false), + effectiveMethodAtom: atom(get => get(methodAtom)), + effectiveSelectedAppAtom: atom(undefined), + isReadingDslAtom: atom(false), + methodAtom, + selectDslFileAtom: emptyActionAtom, + selectMethodAtom: atom(null, (_get, set, value: 'bindApp' | 'importDsl') => { + set(methodAtom, value) + }), + selectSourceAppAtom: emptyActionAtom, + setSourceSearchTextAtom: emptyActionAtom, + sourceAppsQueryAtom: atom({ + data: { pages: [{ data: [] }] }, + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + isLoading: false, + isPlaceholderData: false, + fetchNextPage: vi.fn(), + }), + sourceCanGoNextAtom: atom(false), + sourceSearchTextAtom: atom(''), + unsupportedDslNodesAtom: atom([]), + } +}) + +describe('SourceStepContent', () => { + it('should hide the import DSL option when deployment DSL import is disabled', () => { + render() + + expect(screen.getByText(/createGuide\.methods\.bindApp\.title/)).toBeInTheDocument() + expect(screen.queryByText(/createGuide\.methods\.importDsl\.title/)).not.toBeInTheDocument() + expect(screen.queryByText(/createGuide\.methods\.importDsl\.description/)).not.toBeInTheDocument() + expect(screen.getByRole('textbox', { name: /createGuide\.source\.sourceApp/ })).toBeInTheDocument() + }) +}) diff --git a/web/features/deployments/create-guide/ui/release-step.tsx b/web/features/deployments/create-guide/ui/release-step.tsx index 192351003ab..19494772935 100644 --- a/web/features/deployments/create-guide/ui/release-step.tsx +++ b/web/features/deployments/create-guide/ui/release-step.tsx @@ -7,10 +7,10 @@ import { useTranslation } from 'react-i18next' import { continueFromReleaseAtom, dslDefaultAppNameAtom, + effectiveMethodAtom, hasInstanceNameConflictAtom, instanceDescriptionAtom, instanceNameAtom, - methodAtom, releaseCanGoNextAtom, releaseDescriptionAtom, releaseNameAtom, @@ -74,7 +74,7 @@ function InstanceNameField() { const { t } = useTranslation('deployments') const instanceName = useAtomValue(instanceNameAtom) const setInstanceName = useSetAtom(setInstanceNameAtom) - const method = useAtomValue(methodAtom) + const method = useAtomValue(effectiveMethodAtom) const selectedApp = useAtomValue(selectedAppAtom) const dslDefaultAppName = useAtomValue(dslDefaultAppNameAtom) const instanceNamePlaceholder = method === 'importDsl' diff --git a/web/features/deployments/create-guide/ui/source-step.tsx b/web/features/deployments/create-guide/ui/source-step.tsx index 832219f1b18..d75b9965773 100644 --- a/web/features/deployments/create-guide/ui/source-step.tsx +++ b/web/features/deployments/create-guide/ui/source-step.tsx @@ -19,9 +19,9 @@ import { dslFileAtom, dslReadErrorAtom, dslUnsupportedModeAtom, + effectiveMethodAtom, effectiveSelectedAppAtom, isReadingDslAtom, - methodAtom, selectDslFileAtom, selectMethodAtom, selectSourceAppAtom, @@ -31,12 +31,13 @@ import { sourceSearchTextAtom, unsupportedDslNodesAtom, } from '@/features/deployments/create-guide/state' +import { isDeploymentDslImportEnabled } from '@/features/deployments/shared/domain/feature-flags' import { StepShell } from './layout' const sourceAppSkeletonKeys = ['first-source-app', 'second-source-app', 'third-source-app'] export function SourceStepContent() { - const method = useAtomValue(methodAtom) + const method = useAtomValue(effectiveMethodAtom) const unsupportedDslNodes = useAtomValue(unsupportedDslNodesAtom) return ( @@ -55,7 +56,7 @@ export function SourceStepContent() { function SourceMethodSection() { const { t } = useTranslation('deployments') - const method = useAtomValue(methodAtom) + const method = useAtomValue(effectiveMethodAtom) const selectMethod = useSetAtom(selectMethodAtom) return ( @@ -76,12 +77,14 @@ function SourceMethodSection() { title={t('createGuide.methods.bindApp.title')} description={t('createGuide.methods.bindApp.description')} /> - + {isDeploymentDslImportEnabled && ( + + )} ) diff --git a/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx b/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx new file mode 100644 index 00000000000..5876a3c5d4f --- /dev/null +++ b/web/features/deployments/create-release/ui/__tests__/source-app-picker.spec.tsx @@ -0,0 +1,34 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { SourceAppPicker } from '../source-app-picker' + +function renderSourceAppPicker(disabled: boolean) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + return render( + + undefined} + ariaLabel="Source app" + disabled={disabled} + /> + , + ) +} + +describe('SourceAppPicker', () => { + it('should disable the switch control when disabled', () => { + renderSourceAppPicker(true) + + expect(screen.getByText('Workflow 1')).toBeInTheDocument() + expect(screen.getByRole('combobox', { name: 'Source app' })).toBeDisabled() + }) +}) diff --git a/web/features/deployments/create-release/ui/__tests__/use-release-content-check.spec.ts b/web/features/deployments/create-release/ui/__tests__/use-release-content-check.spec.ts new file mode 100644 index 00000000000..e9ca69fc366 --- /dev/null +++ b/web/features/deployments/create-release/ui/__tests__/use-release-content-check.spec.ts @@ -0,0 +1,36 @@ +import type { CreateReleaseSourceSelection } from '../use-release-content-check' +import { describe, expect, it } from 'vitest' +import { canCheckReleaseSourceContent } from '../use-release-content-check' + +function releaseSource(overrides: Partial = {}): CreateReleaseSourceSelection { + return { + dslContent: '', + dslReadError: false, + encodedDslContent: '', + hasDslContent: false, + hasUnsupportedDslMode: false, + isReadingDsl: false, + isWorkflowDslContent: false, + releaseSourceMode: 'sourceApp', + selectedSourceAppId: undefined, + ...overrides, + } +} + +describe('canCheckReleaseSourceContent', () => { + it('should allow source app releases when a source app is selected', () => { + expect(canCheckReleaseSourceContent(releaseSource({ + selectedSourceAppId: 'app-1', + }))).toBe(true) + }) + + it('should block DSL release content checks when deployment DSL import is disabled', () => { + expect(canCheckReleaseSourceContent(releaseSource({ + dslContent: 'app:\n mode: workflow', + encodedDslContent: 'encoded-dsl', + hasDslContent: true, + isWorkflowDslContent: true, + releaseSourceMode: 'dsl', + }))).toBe(false) + }) +}) diff --git a/web/features/deployments/create-release/ui/dialog.tsx b/web/features/deployments/create-release/ui/dialog.tsx index 501600e73ac..f62d218cf99 100644 --- a/web/features/deployments/create-release/ui/dialog.tsx +++ b/web/features/deployments/create-release/ui/dialog.tsx @@ -8,6 +8,7 @@ import { ScopeProvider } from 'jotai-scope' import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { consoleQuery } from '@/service/client' +import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags' import { closeCreateReleaseDialogAtom, createReleaseDialogOpenAtom, @@ -87,13 +88,19 @@ function CreateReleaseDefaultSourceApp({ formValues }: { const defaultSourceApp = latestSourceAppId ? workflowSourceAppPickerValue(defaultSourceAppQuery.data, latestSourceAppId) : undefined + const sourceAppLocked = !isDeploymentDslImportEnabled + const releaseSourceMode = formValues.releaseSourceMode === 'dsl' && !isDeploymentDslImportEnabled + ? 'sourceApp' + : formValues.releaseSourceMode useEffect(() => { - if (!isDialogOpen || formValues.releaseSourceMode !== 'sourceApp' || formValues.sourceApp || !defaultSourceApp) + if (!isDialogOpen || releaseSourceMode !== 'sourceApp' || !defaultSourceApp) + return + if (formValues.sourceApp && (!sourceAppLocked || formValues.sourceApp.id === defaultSourceApp.id)) return form.setFieldValue('sourceApp', defaultSourceApp) - }, [defaultSourceApp, form, formValues.releaseSourceMode, formValues.sourceApp, isDialogOpen]) + }, [defaultSourceApp, form, formValues.sourceApp, isDialogOpen, releaseSourceMode, sourceAppLocked]) return null } diff --git a/web/features/deployments/create-release/ui/source-app-picker.tsx b/web/features/deployments/create-release/ui/source-app-picker.tsx index 93b99e7fac2..1d37fe749e7 100644 --- a/web/features/deployments/create-release/ui/source-app-picker.tsx +++ b/web/features/deployments/create-release/ui/source-app-picker.tsx @@ -31,16 +31,20 @@ function sourceAppSearchText(app: App) { return `${app.name} ${app.id}`.toLowerCase() } -function SourceAppTrigger({ open, app }: { +function SourceAppTrigger({ open, app, disabled }: { open: boolean app?: SourceAppPickerValue + disabled: boolean }) { const { t } = useTranslation('deployments') return (
- {modeField.state.value === 'sourceApp' + {modeField.state.value === 'sourceApp' || !isDeploymentDslImportEnabled ? : }
@@ -78,6 +81,7 @@ function SourceAppField() { const { t } = useTranslation('deployments') const form = useCreateReleaseFormApi() const clearSubmissionError = useSetAtom(clearCreateReleaseSubmissionErrorAtom) + const sourceAppLocked = !isDeploymentDslImportEnabled return ( @@ -90,6 +94,7 @@ function SourceAppField() { clearSubmissionError() }} ariaLabel={t('versions.sourceAppOption')} + disabled={sourceAppLocked} />
)} diff --git a/web/features/deployments/create-release/ui/use-create-release-submission.ts b/web/features/deployments/create-release/ui/use-create-release-submission.ts index 7c220255ef2..0d2f9b57fa2 100644 --- a/web/features/deployments/create-release/ui/use-create-release-submission.ts +++ b/web/features/deployments/create-release/ui/use-create-release-submission.ts @@ -65,7 +65,7 @@ export function useCreateReleaseSubmission(formValues: CreateReleaseFormValues) if (!canCheckReleaseSourceContent(sourceSelection) || !releaseContent.releaseContentReady) return - if (value.releaseSourceMode === 'dsl') { + if (sourceSelection.releaseSourceMode === 'dsl') { if (!sourceSelection.isWorkflowDslContent) { toast.error(t('versions.dslUnsupportedMode')) return diff --git a/web/features/deployments/create-release/ui/use-release-content-check.ts b/web/features/deployments/create-release/ui/use-release-content-check.ts index a903daae143..f462680cdd2 100644 --- a/web/features/deployments/create-release/ui/use-release-content-check.ts +++ b/web/features/deployments/create-release/ui/use-release-content-check.ts @@ -5,6 +5,7 @@ import type { CreateReleaseFormValues, ReleaseSourceMode } from '../state/types' import { skipToken, useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { consoleQuery } from '@/service/client' +import { isDeploymentDslImportEnabled } from '../../shared/domain/feature-flags' import { createReleaseDialogOpenAtom, createReleaseDslStateAtom, @@ -21,17 +22,20 @@ function createReleaseSourceSelection( formValues: CreateReleaseFormValues, dslState: CreateReleaseDslState, ): CreateReleaseSourceSelection { - const hasUnsupportedDslMode = formValues.releaseSourceMode === 'dsl' + const releaseSourceMode = formValues.releaseSourceMode === 'dsl' && !isDeploymentDslImportEnabled + ? 'sourceApp' + : formValues.releaseSourceMode + const hasUnsupportedDslMode = releaseSourceMode === 'dsl' && dslState.hasDslContent && !dslState.isReadingDsl && !dslState.dslReadError && !dslState.isWorkflowDslContent - const selectedSourceAppId = formValues.releaseSourceMode === 'sourceApp' ? formValues.sourceApp?.id : undefined + const selectedSourceAppId = releaseSourceMode === 'sourceApp' ? formValues.sourceApp?.id : undefined return { ...dslState, hasUnsupportedDslMode, - releaseSourceMode: formValues.releaseSourceMode, + releaseSourceMode, selectedSourceAppId, } } @@ -39,6 +43,8 @@ function createReleaseSourceSelection( export function canCheckReleaseSourceContent(releaseSource: CreateReleaseSourceSelection) { if (releaseSource.releaseSourceMode === 'sourceApp') return Boolean(releaseSource.selectedSourceAppId) + if (!isDeploymentDslImportEnabled) + return false return Boolean( releaseSource.hasDslContent diff --git a/web/features/deployments/shared/domain/feature-flags.ts b/web/features/deployments/shared/domain/feature-flags.ts new file mode 100644 index 00000000000..791ca6cdd73 --- /dev/null +++ b/web/features/deployments/shared/domain/feature-flags.ts @@ -0,0 +1,2 @@ +// Temporary kill switch for deployment DSL import flows. Flip to true to restore them. +export const isDeploymentDslImportEnabled = false From 7c20ffe6c47a0859e2fc3ce1dca7b0e0e87908c0 Mon Sep 17 00:00:00 2001 From: Blackoutta <37723456+Blackoutta@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:25:15 +0800 Subject: [PATCH 18/35] fix: prevent legacy stop from interrupting GraphEngine runs (#37129) Co-authored-by: Crazywoola <100913391+crazywoola@users.noreply.github.com> --- .../apps/message_based_app_queue_manager.py | 3 + .../app/apps/workflow/app_queue_manager.py | 4 -- .../test_legacy_stop_graphengine_lifecycle.py | 59 +++++++++++++++++++ .../apps/workflow/test_app_queue_manager.py | 12 ++-- 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 api/tests/unit_tests/core/app/apps/test_legacy_stop_graphengine_lifecycle.py diff --git a/api/core/app/apps/message_based_app_queue_manager.py b/api/core/app/apps/message_based_app_queue_manager.py index 0b97809bf3a..3c7102971f1 100644 --- a/api/core/app/apps/message_based_app_queue_manager.py +++ b/api/core/app/apps/message_based_app_queue_manager.py @@ -11,6 +11,7 @@ from core.app.entities.queue_entities import ( QueueMessageEndEvent, QueueStopEvent, ) +from models.model import AppMode class MessageBasedAppQueueManager(AppQueueManager): @@ -47,4 +48,6 @@ class MessageBasedAppQueueManager(AppQueueManager): self.stop_listen() if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped(): + if self._app_mode == AppMode.ADVANCED_CHAT.value: + return raise GenerateTaskStoppedError() diff --git a/api/core/app/apps/workflow/app_queue_manager.py b/api/core/app/apps/workflow/app_queue_manager.py index fcdd1465d4f..7824d33b875 100644 --- a/api/core/app/apps/workflow/app_queue_manager.py +++ b/api/core/app/apps/workflow/app_queue_manager.py @@ -1,7 +1,6 @@ from typing import override from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom -from core.app.apps.exc import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import ( AppQueueEvent, @@ -43,6 +42,3 @@ class WorkflowAppQueueManager(AppQueueManager): | QueueWorkflowPartialSuccessEvent, ): self.stop_listen() - - if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped(): - raise GenerateTaskStoppedError() diff --git a/api/tests/unit_tests/core/app/apps/test_legacy_stop_graphengine_lifecycle.py b/api/tests/unit_tests/core/app/apps/test_legacy_stop_graphengine_lifecycle.py new file mode 100644 index 00000000000..a9aafd5949d --- /dev/null +++ b/api/tests/unit_tests/core/app/apps/test_legacy_stop_graphengine_lifecycle.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +import pytest + +from core.app.apps.base_app_queue_manager import PublishFrom +from core.app.apps.exc import GenerateTaskStoppedError +from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager +from core.app.apps.workflow.app_queue_manager import WorkflowAppQueueManager +from core.app.entities.app_invoke_entities import InvokeFrom +from core.app.entities.queue_entities import QueueTextChunkEvent +from models.model import AppMode + + +def _message_queue_manager(app_mode: str) -> MessageBasedAppQueueManager: + with patch("core.app.apps.base_app_queue_manager.redis_client") as mock_redis: + mock_redis.setex.return_value = True + return MessageBasedAppQueueManager( + task_id="task-1", + user_id="user-1", + invoke_from=InvokeFrom.DEBUGGER, + conversation_id="conversation-1", + app_mode=app_mode, + message_id="message-1", + ) + + +def _workflow_queue_manager(app_mode: str) -> WorkflowAppQueueManager: + with patch("core.app.apps.base_app_queue_manager.redis_client") as mock_redis: + mock_redis.setex.return_value = True + return WorkflowAppQueueManager( + task_id="task-1", + user_id="user-1", + invoke_from=InvokeFrom.DEBUGGER, + app_mode=app_mode, + ) + + +def test_message_queue_does_not_raise_legacy_stop_for_advanced_chat() -> None: + manager = _message_queue_manager(AppMode.ADVANCED_CHAT.value) + + with patch.object(manager, "_is_stopped", return_value=True): + manager.publish(QueueTextChunkEvent(text="chunk"), PublishFrom.APPLICATION_MANAGER) + + +def test_workflow_queue_does_not_read_legacy_stop_flag() -> None: + manager = _workflow_queue_manager(AppMode.WORKFLOW.value) + + with patch.object(manager, "_is_stopped", return_value=True) as is_stopped: + manager.publish(QueueTextChunkEvent(text="chunk"), PublishFrom.APPLICATION_MANAGER) + + is_stopped.assert_not_called() + + +def test_message_queue_keeps_legacy_stop_for_non_graphengine_chat() -> None: + manager = _message_queue_manager(AppMode.CHAT.value) + + with patch.object(manager, "_is_stopped", return_value=True): + with pytest.raises(GenerateTaskStoppedError): + manager.publish(QueueTextChunkEvent(text="chunk"), PublishFrom.APPLICATION_MANAGER) diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_app_queue_manager.py b/api/tests/unit_tests/core/app/apps/workflow/test_app_queue_manager.py index 6133be98676..e3b86530098 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_app_queue_manager.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_app_queue_manager.py @@ -1,9 +1,8 @@ from __future__ import annotations -import pytest +from unittest.mock import patch from core.app.apps.base_app_queue_manager import PublishFrom -from core.app.apps.exc import GenerateTaskStoppedError from core.app.apps.workflow.app_queue_manager import WorkflowAppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueMessageEndEvent, QueuePingEvent @@ -17,11 +16,16 @@ class TestWorkflowAppQueueManager: invoke_from=InvokeFrom.DEBUGGER, app_mode="workflow", ) - manager._is_stopped = lambda: True - with pytest.raises(GenerateTaskStoppedError): + with ( + patch.object(manager, "_is_stopped", return_value=True) as is_stopped, + patch.object(manager, "stop_listen") as stop_listen, + ): manager._publish(QueueMessageEndEvent(llm_result=None), PublishFrom.APPLICATION_MANAGER) + stop_listen.assert_called_once() + is_stopped.assert_not_called() + def test_publish_non_stop_event_does_not_raise(self): manager = WorkflowAppQueueManager( task_id="task", From 4065f63dce3fb3b3129f3cd74e29338c9a5cf747 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 22 Jun 2026 17:21:09 +0800 Subject: [PATCH 19/35] fix(agent): add stable debug conversation (#37744) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 4 +++ ...6b2d3e1_add_agent_debug_conversation_id.py | 30 ++++++++++++++++ api/models/agent.py | 2 ++ api/openapi/markdown/console-openapi.md | 2 ++ api/services/agent/roster_service.py | 35 +++++++++++++++++-- .../console/agent/test_agent_controllers.py | 30 +++++++++++++--- .../services/agent/test_agent_services.py | 12 ++++++- .../generated/api/console/agent/types.gen.ts | 4 +++ .../generated/api/console/agent/zod.gen.ts | 4 +++ 9 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 api/migrations/versions/2026_06_22_1000-c8f4a6b2d3e1_add_agent_debug_conversation_id.py diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index eff4f910dae..14d5da7f635 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -186,6 +186,7 @@ class AgentStatisticsQuery(BaseModel): class AgentAppPartial(GenericAppPartial): app_id: str | None = None + debug_conversation_id: str | None = None role: str | None = None active_config_is_published: bool = False published_reference_count: int = 0 @@ -194,6 +195,7 @@ class AgentAppPartial(GenericAppPartial): class AgentAppDetailWithSite(GenericAppDetailWithSite): app_id: str | None = None + debug_conversation_id: str | None = None role: str | None = None active_config_is_published: bool = False @@ -262,6 +264,7 @@ def _serialize_agent_app_detail(app_model) -> dict: payload.pop("bound_agent_id", None) payload["app_id"] = str(app_model.id) payload["id"] = agent.id + payload["debug_conversation_id"] = agent.debug_conversation_id payload["role"] = agent.role or "" payload["active_config_is_published"] = roster_service.active_config_is_published( tenant_id=app_model.tenant_id, @@ -301,6 +304,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: if agent: item["app_id"] = app_id item["id"] = agent.id + item["debug_conversation_id"] = agent.debug_conversation_id item["role"] = agent.role or "" item["active_config_is_published"] = active_config_is_published_by_agent_id.get(agent.id, False) published_references = published_references_by_agent_id.get(agent.id, []) diff --git a/api/migrations/versions/2026_06_22_1000-c8f4a6b2d3e1_add_agent_debug_conversation_id.py b/api/migrations/versions/2026_06_22_1000-c8f4a6b2d3e1_add_agent_debug_conversation_id.py new file mode 100644 index 00000000000..8e0ea3284c9 --- /dev/null +++ b/api/migrations/versions/2026_06_22_1000-c8f4a6b2d3e1_add_agent_debug_conversation_id.py @@ -0,0 +1,30 @@ +"""add agent debug conversation id + +Revision ID: c8f4a6b2d3e1 +Revises: b2515f9d4c2a +Create Date: 2026-06-22 10:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +import models + +# revision identifiers, used by Alembic. +revision = "c8f4a6b2d3e1" +down_revision = "b2515f9d4c2a" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.add_column(sa.Column("debug_conversation_id", models.types.StringUUID(), nullable=True)) + batch_op.create_index("agent_debug_conversation_id_idx", ["debug_conversation_id"], unique=False) + + +def downgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.drop_index("agent_debug_conversation_id_idx") + batch_op.drop_column("debug_conversation_id") diff --git a/api/models/agent.py b/api/models/agent.py index 1905377359f..6500eecafea 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -135,6 +135,7 @@ class Agent(DefaultFieldsMixin, Base): Index("agent_tenant_workflow_id_idx", "tenant_id", "workflow_id"), Index("agent_tenant_app_id_idx", "tenant_id", "app_id"), Index("agent_active_config_snapshot_id_idx", "active_config_snapshot_id"), + Index("agent_debug_conversation_id_idx", "debug_conversation_id"), Index( "agent_tenant_invitable_idx", "tenant_id", @@ -162,6 +163,7 @@ class Agent(DefaultFieldsMixin, Base): scope: Mapped[AgentScope] = mapped_column(EnumText(AgentScope, length=32), nullable=False) source: Mapped[AgentSource] = mapped_column(EnumText(AgentSource, length=32), nullable=False) app_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) + debug_conversation_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) workflow_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True) active_config_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 5d3407f4159..6f507b889b6 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -12066,6 +12066,7 @@ Default namespace | bound_agent_id | string | | No | | created_at | integer | | No | | created_by | string | | No | +| debug_conversation_id | string | | No | | deleted_tools | [ [DeletedTool](#deletedtool) ] | | No | | description | string | | No | | enable_api | boolean | | Yes | @@ -12129,6 +12130,7 @@ default (the config form sends the full desired feature state on save). | create_user_name | string | | No | | created_at | integer | | No | | created_by | string | | No | +| debug_conversation_id | string | | No | | description | string | | No | | has_draft_trigger | boolean | | No | | icon | string | | No | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index 00ea86859d8..1ec4f29e6bb 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -3,6 +3,7 @@ from typing import Any, TypedDict from sqlalchemy import and_, func, or_, select from sqlalchemy.exc import IntegrityError +from core.app.entities.app_invoke_entities import InvokeFrom from libs.datetime_utils import naive_utc_now from libs.helper import to_timestamp from models.agent import ( @@ -18,8 +19,8 @@ from models.agent import ( WorkflowAgentNodeBinding, ) from models.agent_config_entities import AgentSoulConfig -from models.enums import AppStatus -from models.model import App, AppMode, IconType +from models.enums import AppStatus, ConversationFromSource, ConversationStatus +from models.model import App, AppMode, Conversation, IconType from models.workflow import Workflow from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_validator import ComposerConfigValidator @@ -96,6 +97,7 @@ class AgentRosterService: "scope": agent.scope.value, "source": agent.source.value, "app_id": agent.app_id, + "debug_conversation_id": getattr(agent, "debug_conversation_id", None), "workflow_id": agent.workflow_id, "workflow_node_id": agent.workflow_node_id, "active_config_snapshot_id": agent.active_config_snapshot_id, @@ -391,9 +393,38 @@ class AgentRosterService: self._session.add(revision) agent.active_config_snapshot_id = version.id agent.active_config_has_model = agent_soul_has_model(AgentSoulConfig()) + agent.debug_conversation_id = self._create_agent_app_debug_conversation( + app_id=app_id, + account_id=account_id, + ) self._session.flush() return agent + def _create_agent_app_debug_conversation(self, *, app_id: str, account_id: str) -> str: + """Create the stable console conversation used by Agent App debug mode.""" + + conversation = Conversation( + app_id=app_id, + app_model_config_id=None, + model_provider=None, + model_id="", + override_model_configs=None, + mode=AppMode.AGENT, + name="Agent Debugging Conversation", + inputs={}, + introduction="", + system_instruction="", + system_instruction_tokens=0, + status=ConversationStatus.NORMAL, + invoke_from=InvokeFrom.DEBUGGER, + from_source=ConversationFromSource.CONSOLE, + from_end_user_id=None, + from_account_id=account_id, + ) + self._session.add(conversation) + self._session.flush() + return conversation.id + def load_app_backing_agents_by_app_id(self, *, tenant_id: str, app_ids: list[str]) -> dict[str, Agent]: """Return active app-backed Agents keyed by Agent App id.""" if not app_ids: diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 36f98047738..cf02dd3bcc5 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -219,14 +219,22 @@ def test_agent_app_list_and_create_use_agent_route( roster_controller.AgentRosterService, "load_app_backing_agents_by_app_id", lambda _self, **kwargs: { - "app-list": SimpleNamespace(id="agent-list", role="List role", active_config_snapshot_id=None) + "app-list": SimpleNamespace( + id="agent-list", + role="List role", + debug_conversation_id="debug-conversation-list", + active_config_snapshot_id=None, + ) }, ) monkeypatch.setattr( roster_controller.AgentRosterService, "get_app_backing_agent", lambda _self, **kwargs: SimpleNamespace( - id="agent-created", role="Created role", active_config_snapshot_id=None + id="agent-created", + role="Created role", + debug_conversation_id="debug-conversation-created", + active_config_snapshot_id=None, ), ) monkeypatch.setattr( @@ -263,6 +271,7 @@ def test_agent_app_list_and_create_use_agent_route( assert listed["total"] == 1 assert listed["data"][0]["id"] == "agent-list" assert listed["data"][0]["app_id"] == "app-list" + assert listed["data"][0]["debug_conversation_id"] == "debug-conversation-list" assert listed["data"][0]["role"] == "List role" assert listed["data"][0]["active_config_is_published"] is False assert listed["data"][0]["published_reference_count"] == 1 @@ -296,6 +305,7 @@ def test_agent_app_list_and_create_use_agent_route( assert status == 201 assert created["id"] == "agent-created" assert created["app_id"] == "app-created" + assert created["debug_conversation_id"] == "debug-conversation-created" assert created["role"] == "Created role" assert created["active_config_is_published"] is False assert "bound_agent_id" not in created @@ -336,7 +346,12 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( monkeypatch.setattr( roster_controller.AgentRosterService, "get_app_backing_agent", - lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role", active_config_snapshot_id=None), + lambda _self, **kwargs: SimpleNamespace( + id=agent_id, + role="Resolved role", + debug_conversation_id="debug-conversation-detail", + active_config_snapshot_id=None, + ), ) monkeypatch.setattr( roster_controller.FeatureService, @@ -361,6 +376,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id) assert detail["id"] == agent_id assert detail["app_id"] == "app-1" + assert detail["debug_conversation_id"] == "debug-conversation-detail" assert detail["role"] == "Resolved role" assert detail["active_config_is_published"] is False assert "bound_agent_id" not in detail @@ -374,6 +390,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert updated["name"] == "Renamed" assert updated["id"] == agent_id assert updated["app_id"] == "app-1" + assert updated["debug_conversation_id"] == "debug-conversation-detail" assert updated["role"] == "Resolved role" assert updated["active_config_is_published"] is False assert "bound_agent_id" not in updated @@ -445,7 +462,12 @@ def test_agent_app_update_rejects_empty_role(app: Flask, monkeypatch: pytest.Mon monkeypatch.setattr( roster_controller.AgentRosterService, "get_app_backing_agent", - lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="", active_config_snapshot_id=None), + lambda _self, **kwargs: SimpleNamespace( + id=agent_id, + role="", + debug_conversation_id="debug-conversation-detail", + active_config_snapshot_id=None, + ), ) monkeypatch.setattr( roster_controller.FeatureService, diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index e5acc43c52b..e6ca3bb7ca0 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -23,7 +23,8 @@ from models.agent_config_entities import ( DeclaredOutputType, WorkflowNodeJobConfig, ) -from models.model import IconType +from models.enums import ConversationFromSource, ConversationStatus +from models.model import Conversation, IconType from models.workflow import Workflow from services.agent import composer_service, roster_service from services.agent.agent_soul_state import agent_soul_has_model @@ -1353,6 +1354,7 @@ class TestAgentAppBackingAgent: assert agent.agent_kind == AgentKind.DIFY_AGENT assert agent.name == "Iris" assert agent.role == "research assistant" + assert agent.debug_conversation_id is not None # A v1 snapshot + revision are seeded and wired as the active version. snapshots = [a for a in session.added if isinstance(a, AgentConfigSnapshot)] assert len(snapshots) == 1 @@ -1362,6 +1364,14 @@ class TestAgentAppBackingAgent: a for a in session.added if getattr(a, "operation", None) == AgentConfigRevisionOperation.CREATE_VERSION ] assert len(revisions) == 1 + conversations = [a for a in session.added if isinstance(a, Conversation)] + assert len(conversations) == 1 + assert agent.debug_conversation_id == conversations[0].id + assert conversations[0].app_id == "app-1" + assert conversations[0].mode == "agent" + assert conversations[0].status == ConversationStatus.NORMAL + assert conversations[0].from_source == ConversationFromSource.CONSOLE + assert conversations[0].from_account_id == "account-1" # Caller (AppService.create_app) owns the commit — helper must not commit. assert session.commits == 0 diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 7b82989af89..74acc33ed43 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -29,6 +29,7 @@ export type AgentAppDetailWithSite = { bound_agent_id?: string | null created_at?: number | null created_by?: string | null + debug_conversation_id?: string | null deleted_tools?: Array description?: string | null enable_api: boolean @@ -329,6 +330,7 @@ export type AgentAppPartial = { create_user_name?: string | null created_at?: number | null created_by?: string | null + debug_conversation_id?: string | null description?: string | null has_draft_trigger?: boolean | null icon?: string | null @@ -1501,6 +1503,7 @@ export type AgentAppDetailWithSiteWritable = { bound_agent_id?: string | null created_at?: number | null created_by?: string | null + debug_conversation_id?: string | null deleted_tools?: Array description?: string | null enable_api: boolean @@ -1534,6 +1537,7 @@ export type AgentAppPartialWritable = { create_user_name?: string | null created_at?: number | null created_by?: string | null + debug_conversation_id?: string | null description?: string | null has_draft_trigger?: boolean | null icon?: string | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index ec9f5b0107b..b35427c8bd0 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -684,6 +684,7 @@ export const zAgentAppPartial = z.object({ create_user_name: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), + debug_conversation_id: z.string().nullish(), description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), icon: z.string().nullish(), @@ -747,6 +748,7 @@ export const zAgentAppDetailWithSite = z.object({ bound_agent_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), + debug_conversation_id: z.string().nullish(), deleted_tools: z.array(zDeletedTool).optional(), description: z.string().nullish(), enable_api: z.boolean(), @@ -2086,6 +2088,7 @@ export const zAgentAppPartialWritable = z.object({ create_user_name: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), + debug_conversation_id: z.string().nullish(), description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), icon: z.string().nullish(), @@ -2150,6 +2153,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({ bound_agent_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), + debug_conversation_id: z.string().nullish(), deleted_tools: z.array(zDeletedTool).optional(), description: z.string().nullish(), enable_api: z.boolean(), From 7cca8b6bb04a71138af6d369f23143604fb62d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E7=8E=AE=20=28Jade=20Lin=29?= Date: Mon, 22 Jun 2026 17:26:47 +0800 Subject: [PATCH 20/35] fix: Add tenant-level Redis lock for credit pool deduction (#37753) --- api/services/credit_pool_service.py | 52 ++- .../services/test_credit_pool_service.py | 304 ++++++++++++++++++ 2 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 api/tests/unit_tests/services/test_credit_pool_service.py diff --git a/api/services/credit_pool_service.py b/api/services/credit_pool_service.py index 2e103dec153..94515309e79 100644 --- a/api/services/credit_pool_service.py +++ b/api/services/credit_pool_service.py @@ -1,4 +1,12 @@ +"""Tenant credit pool accounting. + +Credit deductions are guarded by a tenant-level Redis lock before the database +row lock is acquired. This keeps concurrent usage accounting for one tenant +from piling up database transactions while preserving cross-tenant concurrency. +""" + import logging +from collections.abc import Callable from sqlalchemy import select from sqlalchemy.orm import Session @@ -7,13 +15,44 @@ from configs import dify_config from core.db.session_factory import session_factory from core.errors.error import QuotaExceededError from extensions.ext_database import db +from extensions.ext_redis import redis_client from models import TenantCreditPool from models.enums import ProviderQuotaType logger = logging.getLogger(__name__) +CREDIT_POOL_TENANT_LOCK_TIMEOUT_SECONDS = 10 +CREDIT_POOL_TENANT_LOCK_BLOCKING_TIMEOUT_SECONDS = 5 + class CreditPoolService: + @staticmethod + def _get_tenant_lock_key(tenant_id: str) -> str: + return f"credit_pool:tenant:{tenant_id}:deduct_lock" + + @classmethod + def _deduct_with_tenant_lock(cls, tenant_id: str, deduct: Callable[[], int]) -> int: + lock_key = cls._get_tenant_lock_key(tenant_id) + lock = redis_client.lock( + lock_key, + timeout=CREDIT_POOL_TENANT_LOCK_TIMEOUT_SECONDS, + blocking_timeout=CREDIT_POOL_TENANT_LOCK_BLOCKING_TIMEOUT_SECONDS, + ) + lock_acquired = False + + try: + lock_acquired = lock.acquire(blocking=True) + if not lock_acquired: + raise QuotaExceededError("Failed to acquire credit pool lock") + + return deduct() + finally: + if lock_acquired: + try: + lock.release() + except Exception: + logger.warning("Failed to release credit pool lock, tenant_id=%s", tenant_id, exc_info=True) + @staticmethod def _get_locked_pool(session: Session, tenant_id: str, pool_type: str) -> TenantCreditPool | None: return session.scalar( @@ -76,7 +115,7 @@ class CreditPoolService: if credits_required <= 0: return 0 - try: + def deduct() -> int: with session_factory.get_session_maker().begin() as session: pool = cls._get_locked_pool(session=session, tenant_id=tenant_id, pool_type=pool_type) if not pool: @@ -89,14 +128,16 @@ class CreditPoolService: raise QuotaExceededError("Insufficient credits remaining") pool.quota_used += credits_required + return credits_required + + try: + return cls._deduct_with_tenant_lock(tenant_id, deduct) except QuotaExceededError: raise except Exception: logger.exception("Failed to deduct credits for tenant %s", tenant_id) raise QuotaExceededError("Failed to deduct credits") - return credits_required - @classmethod def deduct_credits_capped( cls, @@ -108,7 +149,7 @@ class CreditPoolService: if credits_required <= 0: return 0 - try: + def deduct() -> int: with session_factory.get_session_maker().begin() as session: pool = cls._get_locked_pool(session=session, tenant_id=tenant_id, pool_type=pool_type) if not pool: @@ -121,6 +162,9 @@ class CreditPoolService: pool.quota_used += deducted_credits return deducted_credits + + try: + return cls._deduct_with_tenant_lock(tenant_id, deduct) except QuotaExceededError: raise except Exception: diff --git a/api/tests/unit_tests/services/test_credit_pool_service.py b/api/tests/unit_tests/services/test_credit_pool_service.py new file mode 100644 index 00000000000..5e589804c3d --- /dev/null +++ b/api/tests/unit_tests/services/test_credit_pool_service.py @@ -0,0 +1,304 @@ +from collections.abc import Generator +from contextlib import contextmanager +from types import SimpleNamespace +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +import pytest +from sqlalchemy import create_engine, select +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker + +from core.errors.error import QuotaExceededError +from models import TenantCreditPool +from models.enums import ProviderQuotaType +from services.credit_pool_service import ( + CREDIT_POOL_TENANT_LOCK_BLOCKING_TIMEOUT_SECONDS, + CREDIT_POOL_TENANT_LOCK_TIMEOUT_SECONDS, + CreditPoolService, +) + + +def _create_engine_with_pool(*, quota_limit: int, quota_used: int) -> tuple[Engine, str, str]: + engine = create_engine("sqlite:///:memory:") + TenantCreditPool.__table__.create(engine) + tenant_id = str(uuid4()) + pool_id = str(uuid4()) + with engine.begin() as connection: + connection.execute( + TenantCreditPool.__table__.insert(), + { + "id": pool_id, + "tenant_id": tenant_id, + "pool_type": ProviderQuotaType.TRIAL, + "quota_limit": quota_limit, + "quota_used": quota_used, + }, + ) + return engine, tenant_id, pool_id + + +@contextmanager +def _patched_session_factory(engine: Engine) -> Generator[None, None, None]: + session_maker = sessionmaker(bind=engine, expire_on_commit=False) + with patch("services.credit_pool_service.session_factory.get_session_maker", return_value=session_maker): + yield + + +def _get_quota_used(*, engine: Engine, pool_id: str) -> int | None: + with engine.connect() as connection: + return connection.scalar(select(TenantCreditPool.quota_used).where(TenantCreditPool.id == pool_id)) + + +def _make_session_maker(session: MagicMock) -> MagicMock: + session_maker = MagicMock() + transaction = session_maker.begin.return_value + transaction.__enter__.return_value = session + transaction.__exit__.return_value = None + return session_maker + + +def _make_redis_lock() -> MagicMock: + lock = MagicMock() + lock.acquire.return_value = True + return lock + + +def test_get_pool_uses_configured_session_factory_without_flask_app_context() -> None: + engine, tenant_id, _ = _create_engine_with_pool(quota_limit=10, quota_used=2) + + with _patched_session_factory(engine): + pool = CreditPoolService.get_pool(tenant_id=tenant_id, pool_type=ProviderQuotaType.TRIAL) + + assert pool is not None + assert pool.tenant_id == tenant_id + assert pool.quota_used == 2 + + +def test_check_and_deduct_credits_deducts_exact_amount_when_sufficient() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=2) + + with _patched_session_factory(engine): + deducted_credits = CreditPoolService.check_and_deduct_credits(tenant_id=tenant_id, credits_required=3) + + assert deducted_credits == 3 + assert _get_quota_used(engine=engine, pool_id=pool_id) == 5 + + +def test_check_and_deduct_credits_returns_zero_for_non_positive_request() -> None: + assert CreditPoolService.check_and_deduct_credits(tenant_id=str(uuid4()), credits_required=0) == 0 + + +def test_check_and_deduct_credits_raises_when_pool_is_missing() -> None: + engine = create_engine("sqlite:///:memory:") + TenantCreditPool.__table__.create(engine) + + with ( + _patched_session_factory(engine), + pytest.raises(QuotaExceededError, match="Credit pool not found"), + ): + CreditPoolService.check_and_deduct_credits(tenant_id=str(uuid4()), credits_required=1) + + +def test_check_and_deduct_credits_raises_when_pool_is_empty() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=10) + + with ( + _patched_session_factory(engine), + pytest.raises(QuotaExceededError, match="No credits remaining"), + ): + CreditPoolService.check_and_deduct_credits(tenant_id=tenant_id, credits_required=1) + + assert _get_quota_used(engine=engine, pool_id=pool_id) == 10 + + +def test_check_and_deduct_credits_raises_without_partial_deduction_when_insufficient() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=9) + + with ( + _patched_session_factory(engine), + pytest.raises(QuotaExceededError, match="Insufficient credits remaining"), + ): + CreditPoolService.check_and_deduct_credits(tenant_id=tenant_id, credits_required=3) + + assert _get_quota_used(engine=engine, pool_id=pool_id) == 9 + + +def test_check_and_deduct_credits_wraps_unexpected_deduction_errors() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=2) + + with ( + _patched_session_factory(engine), + patch.object(CreditPoolService, "_get_locked_pool", side_effect=RuntimeError("database unavailable")), + pytest.raises(QuotaExceededError, match="Failed to deduct credits"), + ): + CreditPoolService.check_and_deduct_credits(tenant_id=tenant_id, credits_required=1) + + assert _get_quota_used(engine=engine, pool_id=pool_id) == 2 + + +def test_deduct_credits_capped_returns_zero_for_non_positive_request() -> None: + assert CreditPoolService.deduct_credits_capped(tenant_id=str(uuid4()), credits_required=0) == 0 + + +def test_deduct_credits_capped_returns_zero_when_pool_is_missing() -> None: + engine = create_engine("sqlite:///:memory:") + TenantCreditPool.__table__.create(engine) + + with _patched_session_factory(engine): + deducted_credits = CreditPoolService.deduct_credits_capped(tenant_id=str(uuid4()), credits_required=1) + + assert deducted_credits == 0 + + +def test_deduct_credits_capped_returns_zero_when_pool_is_empty() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=10) + + with _patched_session_factory(engine): + deducted_credits = CreditPoolService.deduct_credits_capped(tenant_id=tenant_id, credits_required=1) + + assert deducted_credits == 0 + assert _get_quota_used(engine=engine, pool_id=pool_id) == 10 + + +def test_deduct_credits_capped_deducts_only_remaining_balance_when_insufficient() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=9) + + with _patched_session_factory(engine): + deducted_credits = CreditPoolService.deduct_credits_capped(tenant_id=tenant_id, credits_required=3) + + assert deducted_credits == 1 + assert _get_quota_used(engine=engine, pool_id=pool_id) == 10 + + +def test_deduct_credits_capped_wraps_unexpected_deduction_errors() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=2) + + with ( + _patched_session_factory(engine), + patch.object(CreditPoolService, "_get_locked_pool", side_effect=RuntimeError("database unavailable")), + pytest.raises(QuotaExceededError, match="Failed to deduct credits"), + ): + CreditPoolService.deduct_credits_capped(tenant_id=tenant_id, credits_required=1) + + assert _get_quota_used(engine=engine, pool_id=pool_id) == 2 + + +def test_deduct_credits_capped_reraises_quota_exceeded_errors() -> None: + engine, tenant_id, pool_id = _create_engine_with_pool(quota_limit=10, quota_used=2) + + with ( + _patched_session_factory(engine), + patch.object(CreditPoolService, "_get_locked_pool", side_effect=QuotaExceededError("quota unavailable")), + pytest.raises(QuotaExceededError, match="quota unavailable"), + ): + CreditPoolService.deduct_credits_capped(tenant_id=tenant_id, credits_required=1) + + assert _get_quota_used(engine=engine, pool_id=pool_id) == 2 + + +def test_check_and_deduct_credits_uses_tenant_redis_lock_before_db_deduction() -> None: + tenant_id = "tenant-1" + session = MagicMock() + session_maker = _make_session_maker(session) + pool = SimpleNamespace(remaining_credits=10, quota_used=2) + redis_lock = _make_redis_lock() + + with ( + patch("services.credit_pool_service.redis_client.lock", return_value=redis_lock) as lock, + patch("services.credit_pool_service.session_factory.get_session_maker", return_value=session_maker), + patch.object(CreditPoolService, "_get_locked_pool", return_value=pool) as get_locked_pool, + ): + result = CreditPoolService.check_and_deduct_credits( + tenant_id=tenant_id, + credits_required=3, + pool_type=ProviderQuotaType.TRIAL, + ) + + assert result == 3 + assert pool.quota_used == 5 + lock.assert_called_once_with( + "credit_pool:tenant:tenant-1:deduct_lock", + timeout=CREDIT_POOL_TENANT_LOCK_TIMEOUT_SECONDS, + blocking_timeout=CREDIT_POOL_TENANT_LOCK_BLOCKING_TIMEOUT_SECONDS, + ) + redis_lock.acquire.assert_called_once_with(blocking=True) + redis_lock.release.assert_called_once_with() + get_locked_pool.assert_called_once_with(session=session, tenant_id=tenant_id, pool_type=ProviderQuotaType.TRIAL) + + +def test_deduct_credits_capped_uses_tenant_redis_lock_before_db_deduction() -> None: + tenant_id = "tenant-1" + session = MagicMock() + session_maker = _make_session_maker(session) + pool = SimpleNamespace(remaining_credits=2, quota_used=8) + redis_lock = _make_redis_lock() + + with ( + patch("services.credit_pool_service.redis_client.lock", return_value=redis_lock) as lock, + patch("services.credit_pool_service.session_factory.get_session_maker", return_value=session_maker), + patch.object(CreditPoolService, "_get_locked_pool", return_value=pool) as get_locked_pool, + ): + result = CreditPoolService.deduct_credits_capped( + tenant_id=tenant_id, + credits_required=5, + pool_type=ProviderQuotaType.PAID, + ) + + assert result == 2 + assert pool.quota_used == 10 + lock.assert_called_once_with( + "credit_pool:tenant:tenant-1:deduct_lock", + timeout=CREDIT_POOL_TENANT_LOCK_TIMEOUT_SECONDS, + blocking_timeout=CREDIT_POOL_TENANT_LOCK_BLOCKING_TIMEOUT_SECONDS, + ) + redis_lock.acquire.assert_called_once_with(blocking=True) + redis_lock.release.assert_called_once_with() + get_locked_pool.assert_called_once_with(session=session, tenant_id=tenant_id, pool_type=ProviderQuotaType.PAID) + + +@pytest.mark.parametrize( + "deduct_method", + [ + CreditPoolService.check_and_deduct_credits, + CreditPoolService.deduct_credits_capped, + ], +) +def test_non_positive_credit_request_skips_tenant_redis_lock(deduct_method) -> None: + with patch("services.credit_pool_service.redis_client.lock") as lock: + result = deduct_method(tenant_id="tenant-1", credits_required=0) + + assert result == 0 + lock.assert_not_called() + + +def test_check_and_deduct_credits_wraps_redis_lock_errors_without_querying_db() -> None: + session_maker = MagicMock() + + with ( + patch("services.credit_pool_service.redis_client.lock", side_effect=RuntimeError("redis unavailable")), + patch("services.credit_pool_service.session_factory.get_session_maker", return_value=session_maker), + pytest.raises(QuotaExceededError, match="Failed to deduct credits"), + ): + CreditPoolService.check_and_deduct_credits(tenant_id="tenant-1", credits_required=1) + + session_maker.begin.assert_not_called() + + +def test_deduct_credits_capped_ignores_release_errors_after_successful_deduction() -> None: + session = MagicMock() + session_maker = _make_session_maker(session) + pool = SimpleNamespace(remaining_credits=3, quota_used=7) + redis_lock = _make_redis_lock() + redis_lock.release.side_effect = RuntimeError("release failed") + + with ( + patch("services.credit_pool_service.redis_client.lock", return_value=redis_lock), + patch("services.credit_pool_service.session_factory.get_session_maker", return_value=session_maker), + patch.object(CreditPoolService, "_get_locked_pool", return_value=pool), + ): + result = CreditPoolService.deduct_credits_capped(tenant_id="tenant-1", credits_required=2) + + assert result == 2 + assert pool.quota_used == 9 + redis_lock.release.assert_called_once_with() From 0d7ca17cd1762457d8443ea17be9f1400cc6f185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Mon, 22 Jun 2026 17:30:07 +0800 Subject: [PATCH 21/35] fix: add the outlined button of notification (#37741) --- .../app/in-site-message/__tests__/index.spec.tsx | 3 +++ .../in-site-message/__tests__/notification.spec.tsx | 2 ++ web/app/components/app/in-site-message/index.tsx | 12 ++++++++++-- .../components/app/in-site-message/notification.tsx | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/web/app/components/app/in-site-message/__tests__/index.spec.tsx b/web/app/components/app/in-site-message/__tests__/index.spec.tsx index e4b54e82159..2c13ea9cbf5 100644 --- a/web/app/components/app/in-site-message/__tests__/index.spec.tsx +++ b/web/app/components/app/in-site-message/__tests__/index.spec.tsx @@ -42,12 +42,14 @@ describe('InSiteMessage', () => { it('should render title, subtitle, markdown content, and action buttons', () => { const actions: InSiteMessageActionItem[] = [ { action: 'close', action_name: 'dismiss', text: 'Close', type: 'default' }, + { action: 'close', action_name: 'outline', text: 'Outline', type: 'outline' }, { action: 'link', action_name: 'learn_more', text: 'Learn more', type: 'primary', data: 'https://example.com' }, ] renderComponent(actions, { className: 'custom-message' }) const closeButton = screen.getByRole('button', { name: 'Close' }) + const outlineButton = screen.getByRole('button', { name: 'Outline' }) const learnMoreButton = screen.getByRole('button', { name: 'Learn more' }) const panel = closeButton.closest('div.fixed') const titleElement = panel?.querySelector('.title-3xl-bold') @@ -59,6 +61,7 @@ describe('InSiteMessage', () => { expect(subtitleElement?.textContent).not.toContain('\\n') expect(screen.getByText('Main content')).toBeInTheDocument() expect(closeButton).toBeInTheDocument() + expect(outlineButton).toHaveClass('bg-components-button-secondary-bg') expect(learnMoreButton).toBeInTheDocument() }) diff --git a/web/app/components/app/in-site-message/__tests__/notification.spec.tsx b/web/app/components/app/in-site-message/__tests__/notification.spec.tsx index f5171a57c5b..0af00256789 100644 --- a/web/app/components/app/in-site-message/__tests__/notification.spec.tsx +++ b/web/app/components/app/in-site-message/__tests__/notification.spec.tsx @@ -116,6 +116,7 @@ describe('InSiteMessageNotification', () => { main: 'Parsed body main', actions: [ { action: 'link', data: 'https://example.com/docs', text: 'Visit docs', type: 'primary' }, + { action: 'close', text: 'Outline close', type: 'outline' }, { action: 'close', text: 'Dismiss now', type: 'default' }, { action: 'link', data: 'https://example.com/invalid', text: 100, type: 'primary' }, ], @@ -132,6 +133,7 @@ describe('InSiteMessageNotification', () => { expect(screen.getByText('Parsed body main')).toBeInTheDocument() }) expect(screen.getByRole('button', { name: 'Visit docs' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Outline close' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Dismiss now' })).toBeInTheDocument() expect(screen.queryByRole('button', { name: 'Invalid' })).not.toBeInTheDocument() diff --git a/web/app/components/app/in-site-message/index.tsx b/web/app/components/app/in-site-message/index.tsx index 4038fb375d9..9452218cf78 100644 --- a/web/app/components/app/in-site-message/index.tsx +++ b/web/app/components/app/in-site-message/index.tsx @@ -7,7 +7,7 @@ import { trackEvent } from '@/app/components/base/amplitude' import { MarkdownWithDirective } from '@/app/components/base/markdown-with-directive' type InSiteMessageAction = 'link' | 'close' -type InSiteMessageButtonType = 'primary' | 'default' +type InSiteMessageButtonType = 'primary' | 'default' | 'outline' export type InSiteMessageActionItem = { action: InSiteMessageAction @@ -54,6 +54,14 @@ function normalizeLinkData(data: unknown): { href: string, rel?: string, target? const DEFAULT_HEADER_BG_URL = '/in-site-message/header-bg.svg' +function resolveButtonVariant(type: InSiteMessageButtonType) { + if (type === 'primary') + return 'primary' + if (type === 'outline') + return 'secondary' + return 'ghost' +} + function InSiteMessage({ notificationId, actions, @@ -132,7 +140,7 @@ function InSiteMessage({ {actions.map(item => (
+ + ) + + if (isPreviewOnly) { + return ( +
+ {cardContent} +
+ ) + } + + return ( + + {cardContent} ) } diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx index 5ce1c8f72c1..0318645ea9f 100644 --- a/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx @@ -11,7 +11,7 @@ import { useInvalidDataSourceList } from '@/service/use-pipeline' import Card from '../card' import { useDataSourceAuthUpdate } from '../hooks' -let mockWorkspacePermissionKeys: string[] = ['credential.manage', 'credential.use'] +let mockWorkspacePermissionKeys: string[] = ['credential.use', 'credential.create', 'credential.manage'] vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({ @@ -126,7 +126,7 @@ describe('Card Component', () => { beforeEach(() => { vi.clearAllMocks() - mockWorkspacePermissionKeys = ['credential.manage', 'credential.use'] + mockWorkspacePermissionKeys = ['credential.use', 'credential.create', 'credential.manage'] mockPluginAuthActionReturn = createMockPluginAuthActionReturn() vi.mocked(useDataSourceAuthUpdate).mockReturnValue({ handleAuthUpdate: mockHandleAuthUpdate }) @@ -451,7 +451,7 @@ describe('Card Component', () => { expectAuthUpdated() }) - it('should disable configure credential actions when user lacks credential.manage', () => { + it('should disable configure credential actions when user lacks credential.create', () => { // Arrange mockWorkspacePermissionKeys = ['credential.use'] const configurableItem: DataSourceAuth = { diff --git a/web/app/components/header/account-setting/data-source-page-new/card.tsx b/web/app/components/header/account-setting/data-source-page-new/card.tsx index 0eddcb5d394..a972893e7ec 100644 --- a/web/app/components/header/account-setting/data-source-page-new/card.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/card.tsx @@ -50,7 +50,7 @@ const Card = ({ onPluginUpdate, }: CardProps) => { const { t } = useTranslation() - const { canUseCredential, canManageCredential } = useCredentialPermissions() + const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() const renderI18nObject = useRenderI18nObject() const { icon, @@ -178,7 +178,7 @@ const Card = ({ pluginPayload={pluginPayload} item={item} onUpdate={handleAuthUpdate} - disabled={disabled || !canManageCredential} + disabled={disabled || !canCreateCredential} />
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx index 16b3dc3fb54..c8ec3a1f2a2 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx @@ -3,9 +3,13 @@ import { fireEvent, render, screen } from '@testing-library/react' import { ConfigurationMethodEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import AddCredentialInLoadBalancing from '../add-credential-in-load-balancing' +const mockWorkspacePermissionKeys = vi.hoisted(() => ({ + value: ['credential.use', 'credential.create', 'credential.manage'], +})) + vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({ - workspacePermissionKeys: ['credential.use', 'credential.manage'], + workspacePermissionKeys: mockWorkspacePermissionKeys.value, }), })) @@ -15,13 +19,21 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth' authParams, items, onItemClick, + hideAddAction, + triggerOnlyOpenModal, }: { renderTrigger: (open?: boolean) => React.ReactNode authParams?: { onUpdate?: (payload?: unknown, formValues?: Record) => void } items: Array<{ credentials: Array<{ credential_id: string, credential_name: string }> }> onItemClick?: (credential: { credential_id: string, credential_name: string }) => void + hideAddAction?: boolean + triggerOnlyOpenModal?: boolean }) => ( -
+
{renderTrigger(false)} @@ -50,6 +62,7 @@ describe('AddCredentialInLoadBalancing', () => { beforeEach(() => { vi.clearAllMocks() + mockWorkspacePermissionKeys.value = ['credential.use', 'credential.create', 'credential.manage'] }) it('should render add credential label', () => { @@ -103,6 +116,45 @@ describe('AddCredentialInLoadBalancing', () => { expect(onSelectCredential).toHaveBeenCalledWith(modelCredential.available_credentials[0]) }) + it('should render credential menu for manage-only users with existing credentials', () => { + mockWorkspacePermissionKeys.value = ['credential.manage'] + + render( + , + ) + + expect(screen.getByText(/modelProvider.auth.addCredential/i))!.toBeInTheDocument() + expect(screen.getByTestId('authorized-mock')).toHaveAttribute('data-hide-add-action', 'true') + expect(screen.getByTestId('authorized-mock')).toHaveAttribute('data-trigger-only-open-modal', 'false') + }) + + it('should render nothing for manage-only users without existing credentials', () => { + mockWorkspacePermissionKeys.value = ['credential.manage'] + + const emptyModelCredential = { + ...modelCredential, + available_credentials: [], + } as ModelCredential + + const { container } = render( + , + ) + + expect(container).toBeEmptyDOMElement() + }) + // renderTrigger with open=true: bg-state-base-hover style applied it('should apply hover background when trigger is rendered with open=true', async () => { vi.doMock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({ diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx index 3bfa1029474..7230be997f4 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx @@ -24,9 +24,13 @@ vi.mock('../hooks/use-custom-models', () => ({ useCanAddedModels: () => mockCanAddedModels, })) +const mockWorkspacePermissionKeys = vi.hoisted(() => ({ + value: ['credential.use', 'credential.create', 'credential.manage'], +})) + vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({ - workspacePermissionKeys: ['credential.manage', 'credential.use'], + workspacePermissionKeys: mockWorkspacePermissionKeys.value, }), })) @@ -60,6 +64,7 @@ describe('AddCustomModel', () => { beforeEach(() => { vi.clearAllMocks() + mockWorkspacePermissionKeys.value = ['credential.use', 'credential.create', 'credential.manage'] mockCanAddedModels = [] }) @@ -120,6 +125,31 @@ describe('AddCustomModel', () => { expect(mockHandleOpenModalForAddCustomModelToModelList).toHaveBeenCalledWith(undefined, model) }) + it('should show existing model rows as disabled for create-only users', () => { + const model = { model: 'gpt-4', model_type: 'llm' } + mockWorkspacePermissionKeys.value = ['credential.create'] + mockCanAddedModels = [model] + + render( + , + ) + + fireEvent.click(screen.getByTestId('popover-trigger')) + + const modelRow = screen.getByText('gpt-4').closest('[aria-disabled]') + expect(modelRow).toHaveAttribute('aria-disabled', 'true') + expect(modelRow).toHaveClass('cursor-not-allowed') + + fireEvent.click(screen.getByText('gpt-4')) + expect(mockHandleOpenModalForAddCustomModelToModelList).not.toHaveBeenCalled() + + fireEvent.click(screen.getByText(/modelProvider.auth.addNewModel/)) + expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled() + }) + it('should call handleOpenModalForAddNewCustomModel when clicking "Add New Model" in list', () => { mockCanAddedModels = [{ model: 'gpt-4', model_type: 'llm' }] render( diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx index e871b35954f..3199e26692c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx @@ -4,17 +4,40 @@ import userEvent from '@testing-library/user-event' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import SwitchCredentialInLoadBalancing from '../switch-credential-in-load-balancing' +const mockWorkspacePermissionKeys = vi.hoisted(() => ({ + value: ['credential.use', 'credential.create', 'credential.manage'], +})) + vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({ - workspacePermissionKeys: ['credential.use', 'credential.manage'], + workspacePermissionKeys: mockWorkspacePermissionKeys.value, }), })) // Mock components vi.mock('../authorized', () => ({ - default: ({ renderTrigger, onItemClick, items }: { renderTrigger: () => React.ReactNode, onItemClick: (c: unknown) => void, items: { credentials: unknown[] }[] }) => ( -
-
onItemClick(items[0]!.credentials[0])}> + default: ({ + renderTrigger, + onItemClick, + items, + disabled, + hideAddAction, + triggerOnlyOpenModal, + }: { + renderTrigger: () => React.ReactNode + onItemClick?: (c: unknown) => void + items: { credentials: unknown[] }[] + disabled?: boolean + hideAddAction?: boolean + triggerOnlyOpenModal?: boolean + }) => ( +
+
onItemClick?.(items[0]!.credentials[0])}> {renderTrigger()}
@@ -50,6 +73,7 @@ describe('SwitchCredentialInLoadBalancing', () => { beforeEach(() => { vi.clearAllMocks() + mockWorkspacePermissionKeys.value = ['credential.use', 'credential.create', 'credential.manage'] }) it('should render selected credential name correctly', () => { @@ -82,7 +106,7 @@ describe('SwitchCredentialInLoadBalancing', () => { expect(screen.getByTestId('indicator-error'))!.toBeInTheDocument() }) - it('should render unavailable status when credentials list is empty', () => { + it('should render add credential status when credentials list is empty and create is allowed', () => { render( { />, ) - expect(screen.getByText(/auth.credentialUnavailableInButton/))!.toBeInTheDocument() + expect(screen.getByText(/modelProvider.auth.addCredential/))!.toBeInTheDocument() expect(screen.queryByTestId(/indicator-/)).not.toBeInTheDocument() }) @@ -112,6 +136,27 @@ describe('SwitchCredentialInLoadBalancing', () => { expect(mockSetCustomModelCredential).toHaveBeenCalledWith(mockCredentials[0]) }) + it('should keep credential menu available for manage-only users without allowing selection', () => { + mockWorkspacePermissionKeys.value = ['credential.manage'] + + render( + , + ) + + expect(screen.getByTestId('authorized-mock')).toHaveAttribute('data-disabled', 'false') + expect(screen.getByTestId('authorized-mock')).toHaveAttribute('data-hide-add-action', 'true') + + fireEvent.click(screen.getByTestId('trigger-container')) + + expect(mockSetCustomModelCredential).not.toHaveBeenCalled() + }) + it('should show tooltip when empty and custom credentials not allowed', async () => { const user = userEvent.setup() const restrictedProvider = { ...mockProvider, allow_custom_token: false } @@ -129,8 +174,8 @@ describe('SwitchCredentialInLoadBalancing', () => { expect(await screen.findByText('plugin.auth.credentialUnavailable'))!.toBeInTheDocument() }) - // Empty credentials with allowed custom: no tooltip but still shows unavailable text - it('should show unavailable status without tooltip when custom credentials are allowed', () => { + // Empty credentials with allowed custom: no tooltip but still shows add credential text + it('should show add credential status without tooltip when custom credentials are allowed', () => { // Act render( { // Assert // Assert - expect(screen.getByText(/auth.credentialUnavailableInButton/))!.toBeInTheDocument() + expect(screen.getByText(/modelProvider.auth.addCredential/))!.toBeInTheDocument() expect(screen.queryByText('plugin.auth.credentialUnavailable')).not.toBeInTheDocument() }) @@ -231,9 +276,8 @@ describe('SwitchCredentialInLoadBalancing', () => { />, ) - // credentials is undefined → empty=true → unavailable text shown - // credentials is undefined → empty=true → unavailable text shown - expect(screen.getByText(/auth.credentialUnavailableInButton/))!.toBeInTheDocument() + // credentials is undefined -> empty=true -> add credential text shown when creation is allowed. + expect(screen.getByText(/modelProvider.auth.addCredential/))!.toBeInTheDocument() expect(screen.queryByTestId(/indicator-/)).not.toBeInTheDocument() }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx index 1f27fc96788..4bf5002f3a9 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx @@ -13,8 +13,7 @@ import { import { useTranslation } from 'react-i18next' import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' -import { hasPermission } from '@/utils/permission' +import { useCredentialPermissions } from '@/hooks/use-credential-permissions' type AddCredentialInLoadBalancingProps = { provider: ModelProvider @@ -36,12 +35,11 @@ const AddCredentialInLoadBalancing = ({ onRemove, }: AddCredentialInLoadBalancingProps) => { const { t } = useTranslation() - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canUseCredential = hasPermission(workspacePermissionKeys, ['credential.use', 'credential.manage']) - const canManageCredential = hasPermission(workspacePermissionKeys, 'credential.manage') + const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() const { available_credentials, } = modelCredential + const canOpenCredentialMenu = canUseCredential || canCreateCredential || (canManageCredential && !!available_credentials?.length) const isCustomModel = configurationMethod === ConfigurationMethodEnum.customizableModel const notAllowCustomCredential = provider.allow_custom_token === false const handleUpdate = useCallback((payload?: unknown, formValues?: Record) => { @@ -63,7 +61,7 @@ const AddCredentialInLoadBalancing = ({ return Item }, [t]) - if (!canUseCredential) + if (!canOpenCredentialMenu) return null return ( @@ -76,7 +74,7 @@ const AddCredentialInLoadBalancing = ({ onUpdate: handleUpdate, onRemove, }} - triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential && canManageCredential} + triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential && canCreateCredential} items={[ { title: isCustomModel ? '' : t('modelProvider.auth.apiKeys', { ns: 'common' }), @@ -93,7 +91,7 @@ const AddCredentialInLoadBalancing = ({ } : undefined} onItemClick={onSelectCredential} - hideAddAction={!canManageCredential} + hideAddAction={!canCreateCredential} placement="bottom-start" popupTitle={isCustomModel ? t('modelProvider.auth.modelCredentials', { ns: 'common' }) : ''} /> diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx index d7c677b06c4..def6e42fffe 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx @@ -24,8 +24,7 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' -import { hasPermission } from '@/utils/permission' +import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import ModelIcon from '../model-icon' import { useAuth } from './hooks/use-auth' import { useCanAddedModels } from './hooks/use-custom-models' @@ -46,9 +45,7 @@ const AddCustomModel = ({ const [open, setOpen] = useState(false) const canAddedModels = useCanAddedModels(provider) const noModels = !canAddedModels.length - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canUseCredential = hasPermission(workspacePermissionKeys, ['credential.use', 'credential.manage']) - const canManageCredential = hasPermission(workspacePermissionKeys, 'credential.manage') + const { canUseCredential, canCreateCredential } = useCredentialPermissions() const { handleOpenModal: handleOpenModalForAddNewCustomModel, } = useAuth( @@ -73,7 +70,9 @@ const AddCustomModel = ({ ) const notAllowCustomCredential = provider.allow_custom_token === false const renderTrigger = useCallback((open?: boolean, onClick?: () => void) => { - const disabled = (noModels && !canManageCredential) || (!noModels && !canUseCredential) + const disabled = noModels + ? !canCreateCredential + : !canUseCredential && !canCreateCredential const item = ( ) - if ((empty && notAllowCustomCredential) || !canUseCredential) { + if ((empty && notAllowCustomCredential) || !canOpenCredentialMenu) { return ( @@ -106,7 +110,7 @@ const SwitchCredentialInLoadBalancing = ({ ) } return Item - }, [canUseCredential, customModelCredential, t, credentials, notAllowCustomCredential]) + }, [canCreateCredential, canOpenCredentialMenu, customModelCredential, t, credentials, notAllowCustomCredential]) return ( ) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx index cfb241be7d5..e1a8ac24cf2 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/dialog.spec.tsx @@ -79,7 +79,7 @@ vi.mock('../../model-auth/hooks', () => ({ vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => - selector({ workspacePermissionKeys: ['credential.manage', 'credential.use'] }), + selector({ workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'] }), })) vi.mock('@/hooks/use-i18n', () => ({ diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx index f1d672299c0..b3dad54e25e 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx @@ -29,7 +29,7 @@ const mockState = vi.hoisted(() => ({ credentialData: { credentials: {}, available_credentials: [] } as CredentialData, doingAction: false, deleteCredentialId: null as string | null, - workspacePermissionKeys: ['credential.manage', 'credential.use'] as string[], + workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'] as string[], formSchemas: [] as CredentialFormSchema[], formValues: {} as Record, modelNameAndTypeFormSchemas: [] as CredentialFormSchema[], @@ -184,7 +184,7 @@ describe('ModelModal', () => { mockState.credentialData = { credentials: {}, available_credentials: [] } mockState.doingAction = false mockState.deleteCredentialId = null - mockState.workspacePermissionKeys = ['credential.manage', 'credential.use'] + mockState.workspacePermissionKeys = ['credential.use', 'credential.create', 'credential.manage'] mockState.formSchemas = [] mockState.formValues = {} mockState.modelNameAndTypeFormSchemas = [] diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx index 0c7f68fb361..245554c5c1b 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx @@ -41,9 +41,8 @@ import { useCredentialData, } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks' import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' +import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { useRenderI18nObject } from '@/hooks/use-i18n' -import { hasPermission } from '@/utils/permission' import { ConfigurationMethodEnum, FormTypeEnum, @@ -107,9 +106,7 @@ const ModelModal: FC = ({ available_credentials, } = credentialData as any - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canUseCredential = hasPermission(workspacePermissionKeys, ['credential.use', 'credential.manage']) - const canManageCredential = hasPermission(workspacePermissionKeys, 'credential.manage') + const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() const { t } = useTranslation() const language = useLanguage() const { @@ -135,7 +132,8 @@ const ModelModal: FC = ({ return } - if (!canManageCredential) + const canSubmitCredentialForm = credential ? canManageCredential : canCreateCredential + if (!canSubmitCredentialForm) return let modelNameAndTypeIsCheckValidated = true @@ -197,7 +195,7 @@ const ModelModal: FC = ({ }) } onSave(values) - }, [mode, selectedCredential, model, canUseCredential, canManageCredential, onSave, handleActiveCredential, onCancel, handleSaveCredential, credential?.credential_id]) + }, [mode, selectedCredential, model, canUseCredential, canCreateCredential, canManageCredential, onSave, handleActiveCredential, onCancel, handleSaveCredential, credential]) const modalTitle = useMemo(() => { let label = t('modelProvider.auth.apiKeyModal.title', { ns: 'common' }) @@ -277,7 +275,7 @@ const ModelModal: FC = ({ }, [mode, t]) const canSaveCredentialChange = mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential && !selectedCredential.addNewCredential ? canUseCredential - : canManageCredential + : credential ? canManageCredential : canCreateCredential const handleDeleteCredential = useCallback(() => { handleConfirmDelete() @@ -339,7 +337,7 @@ const ModelModal: FC = ({ onSelect={setSelectedCredential} selectedCredential={selectedCredential} disabled={isLoading} - notAllowAddNewCredential={notAllowCustomCredential || !canManageCredential} + notAllowAddNewCredential={notAllowCustomCredential || !canCreateCredential} /> ) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx index f8511a17855..291b903f62a 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx @@ -72,10 +72,13 @@ vi.mock('@/context/provider-context', () => ({ })) const mockUseAppContext = vi.hoisted(() => vi.fn()) +const mockWorkspacePermissionKeys = vi.hoisted(() => ({ + value: ['credential.use', 'credential.create', 'credential.manage'], +})) vi.mock('@/context/app-context', () => ({ useAppContext: mockUseAppContext, useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({ - workspacePermissionKeys: ['credential.manage', 'credential.use'], + workspacePermissionKeys: mockWorkspacePermissionKeys.value, }), })) @@ -136,6 +139,7 @@ const renderWithCombobox = ( describe('PopupItem', () => { beforeEach(() => { vi.clearAllMocks() + mockWorkspacePermissionKeys.value = ['credential.use', 'credential.create', 'credential.manage'] mockUseLanguage.mockReturnValue('en_US') mockUseProviderContext.mockReturnValue({ modelProviders: [makeProvider()], @@ -412,4 +416,18 @@ describe('PopupItem', () => { expect(onHide).toHaveBeenCalled() }) + + it('should keep the credential dropdown enabled for manage-only users', () => { + mockWorkspacePermissionKeys.value = ['credential.manage'] + + renderWithCombobox() + + const trigger = screen.getByRole('button', { name: /my-api-key/ }) + + expect(trigger).not.toBeDisabled() + + fireEvent.click(trigger) + + expect(screen.getByRole('button', { name: 'close dropdown' })).toBeInTheDocument() + }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index a957ae16577..f0d99838c55 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -8,10 +8,9 @@ import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' -import { hasPermission } from '@/utils/permission' +import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations' import { useLanguage, useUpdateModelList, useUpdateModelProviders } from '../hooks' import ModelIcon from '../model-icon' @@ -50,11 +49,10 @@ function PopupItem({ const updateModelList = useUpdateModelList() const updateModelProviders = useUpdateModelProviders() const currentProvider = modelProviders.find(provider => provider.provider === model.provider) - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canUseCredentials = hasPermission(workspacePermissionKeys, ['credential.manage', 'credential.use']) - const canManageCredentials = hasPermission(workspacePermissionKeys, 'credential.manage') + const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() + const canOpenCredentialDropdown = canUseCredential || canCreateCredential || canManageCredential const handleOpenModelModal = () => { - if (!canManageCredentials) + if (!canCreateCredential) return if (!currentProvider) @@ -110,7 +108,7 @@ function PopupItem({ {isUsingCredits @@ -142,7 +140,7 @@ function PopupItem({ {t('modelProvider.selector.configureRequired', { ns: 'common' })} )} - {canUseCredentials && } + {canOpenCredentialDropdown && } )} /> @@ -193,7 +191,7 @@ function PopupItem({ onPointerDown={onPreviewCardClose} > {rowContent} - {canManageCredentials && ( + {canCreateCredential && ( +
@@ -97,7 +97,7 @@ describe('DropdownContent', () => { beforeEach(() => { vi.clearAllMocks() mockDeleteCredentialId = null - mockWorkspacePermissionKeys = ['credential.manage', 'credential.use'] + mockWorkspacePermissionKeys = ['credential.use', 'credential.create', 'credential.manage'] }) describe('UsagePrioritySection visibility', () => { @@ -397,6 +397,58 @@ describe('DropdownContent', () => { expect(mockHandleOpenModal).not.toHaveBeenCalled() expect(mockOpenConfirmDelete).not.toHaveBeenCalled() }) + + it('should allow create-only users to add credentials but not switch, edit, or delete existing credentials', () => { + mockWorkspacePermissionKeys = ['credential.create'] + + render( + , + ) + + fireEvent.click(screen.getByTestId('click-cred-2')) + fireEvent.click(screen.getByTestId('edit-cred-2')) + fireEvent.click(screen.getByTestId('delete-cred-2')) + fireEvent.click(screen.getByRole('button', { name: /addApiKey/ })) + + expect(mockActivate).not.toHaveBeenCalled() + expect(mockOpenConfirmDelete).not.toHaveBeenCalled() + expect(mockHandleOpenModal).toHaveBeenCalledTimes(1) + expect(mockHandleOpenModal).toHaveBeenCalledWith() + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('should allow manage-only users to edit and delete credentials but not switch or add them', () => { + mockWorkspacePermissionKeys = ['credential.manage'] + + render( + , + ) + + fireEvent.click(screen.getByTestId('click-cred-2')) + fireEvent.click(screen.getByTestId('edit-cred-2')) + fireEvent.click(screen.getByTestId('delete-cred-2')) + + expect(mockActivate).not.toHaveBeenCalled() + expect(mockHandleOpenModal).toHaveBeenCalledWith( + expect.objectContaining({ credential_id: 'cred-2' }), + ) + expect(mockOpenConfirmDelete).toHaveBeenCalledWith( + expect.objectContaining({ credential_id: 'cred-2' }), + ) + expect(screen.queryByRole('button', { name: /addApiKey/ })).not.toBeInTheDocument() + }) }) describe('Add API Key', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx index eba042cb48f..57caf2c1c2c 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/api-key-section.tsx @@ -2,8 +2,7 @@ import type { Credential, CustomModel, ModelProvider } from '../../declarations' import { Button } from '@langgenius/dify-ui/button' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' -import { hasPermission } from '@/utils/permission' +import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import CredentialItem from '../../model-auth/authorized/credential-item' type ApiKeySectionProps = { @@ -29,8 +28,7 @@ function ApiKeySection({ }: ApiKeySectionProps) { const { t } = useTranslation() const notAllowCustomCredential = provider.allow_custom_token === false - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canManageCredential = hasPermission(workspacePermissionKeys, 'credential.manage') + const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions() if (!credentials.length) { return ( @@ -45,7 +43,7 @@ function ApiKeySection({
- {!notAllowCustomCredential && canManageCredential && ( + {!notAllowCustomCredential && canCreateCredential && (