Compare commits

..
Author SHA1 Message Date
hjlarry e007dbaf0e fix: use contracts 2026-06-17 09:52:04 +08:00
hjlarry 9323b9f49d chore: add client country api 2026-06-16 16:04:09 +08:00
48 changed files with 675 additions and 1344 deletions
+2
View File
@@ -34,6 +34,7 @@ for module_name in RESOURCE_MODULES:
# Import other controllers
from . import (
apikey,
client_metadata,
extension,
feature,
human_input_form,
@@ -167,6 +168,7 @@ __all__ = [
"banner",
"billing",
"bp",
"client_metadata",
"completion",
"compliance",
"console_ns",
+11 -31
View File
@@ -53,20 +53,14 @@ class AgentIdPath(BaseModel):
class AgentAppCreatePayload(BaseModel):
name: str = Field(..., min_length=1, description="Agent name")
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
role: str = Field(default="", description="Agent role", max_length=255)
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
class AgentAppUpdatePayload(UpdateAppPayload):
role: str | None = Field(default=None, description="Agent role", max_length=255)
register_schema_models(
console_ns,
AgentAppCreatePayload,
AgentAppUpdatePayload,
AgentInviteOptionsQuery,
AgentIdPath,
AppListQuery,
@@ -95,32 +89,20 @@ def _serialize_agent_app_detail(app_model) -> dict:
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
agent = _agent_roster_service().get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id)
if not agent:
raise AgentNotFoundError()
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
payload.pop("bound_agent_id", None)
payload["app_id"] = str(app_model.id)
payload["id"] = agent.id
payload["role"] = agent.role or ""
agent_id = payload.pop("bound_agent_id", None)
if not agent_id:
raise AgentNotFoundError()
payload["id"] = agent_id
return payload
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
app_ids = [str(app.id) for app in app_pagination.items]
agents_by_app_id = _agent_roster_service().load_app_backing_agents_by_app_id(
tenant_id=tenant_id,
app_ids=app_ids,
)
def _serialize_agent_app_pagination(app_pagination) -> dict:
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
for item in payload["data"]:
app_id = item["id"]
item.pop("bound_agent_id", None)
agent = agents_by_app_id.get(app_id)
if agent:
item["app_id"] = app_id
item["id"] = agent.id
item["role"] = agent.role or ""
agent_id = item.pop("bound_agent_id", None)
if agent_id:
item["id"] = agent_id
return payload
@@ -155,7 +137,7 @@ class AgentAppListApi(Resource):
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json")
return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id)
return _serialize_agent_app_pagination(app_pagination)
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
@console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__])
@@ -174,7 +156,6 @@ class AgentAppListApi(Resource):
name=args.name,
description=args.description,
mode="agent",
agent_role=args.role,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
@@ -196,7 +177,7 @@ class AgentAppApi(Resource):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _serialize_agent_app_detail(app_model)
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
@console_ns.expect(console_ns.models[UpdateAppPayload.__name__])
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@@ -207,7 +188,7 @@ class AgentAppApi(Resource):
@with_current_tenant_id
def put(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
args = UpdateAppPayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
@@ -216,7 +197,6 @@ class AgentAppApi(Resource):
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
}
updated = AppService().update_app(app_model, args_dict)
return _serialize_agent_app_detail(updated)
-6
View File
@@ -400,9 +400,6 @@ class AppPartial(ResponseModel):
has_draft_trigger: bool | None = None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
is_starred: bool = False
@computed_field(return_type=str | None) # type: ignore
@@ -454,9 +451,6 @@ class AppDetailWithSite(AppDetail):
site: Site | None = None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
# For Agent App responses exposed through /agent.
app_id: str | None = None
role: str | None = None
@computed_field(return_type=str | None) # type: ignore
@property
@@ -0,0 +1,52 @@
"""Public client metadata endpoint backed by Cloudflare request metadata."""
import re
from flask import request
from flask_restx import Resource
from pydantic import Field
from controllers.common.schema import JsonResponseWithStatus, register_response_schema_models
from fields.base import ResponseModel
from libs.helper import dump_response
from . import console_ns
COUNTRY_CODE_PATTERN = re.compile(r"^[A-Z0-9]{2}$")
class ClientGeoMetadataResponse(ResponseModel):
country_code: str | None = Field(
default=None,
description="Two-character Cloudflare country code for the current request, or null when unavailable.",
examples=["CN"],
)
class ClientMetadataResponse(ResponseModel):
geo: ClientGeoMetadataResponse = Field(description="Geographic metadata inferred for the current request.")
register_response_schema_models(console_ns, ClientMetadataResponse)
def _extract_cloudflare_country_code() -> str | None:
country_code = request.headers.get("CF-IPCountry", "").strip().upper()
if not COUNTRY_CODE_PATTERN.fullmatch(country_code):
return None
return country_code
@console_ns.route("/client-metadata")
class ClientMetadataApi(Resource):
@console_ns.response(200, "Success", console_ns.models[ClientMetadataResponse.__name__])
def get(self) -> JsonResponseWithStatus:
"""Return client metadata inferred from edge request headers.
NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity
metadata for the current request and performs no tenant, account, or setup checks.
"""
return dump_response(
ClientMetadataResponse,
{"geo": {"country_code": _extract_cloudflare_country_code()}},
), 200
+14 -27
View File
@@ -246,8 +246,6 @@ class AgentAppGenerator(MessageBasedAppGenerator):
"conversation_id": conversation.id,
"message_id": message.id,
"user_from": UserFrom.ACCOUNT if isinstance(user, Account) else UserFrom.END_USER,
# Resume continues a paused agent run; skip input guards (see _generate_worker).
"is_resume": True,
},
)
worker_thread.start()
@@ -272,7 +270,6 @@ class AgentAppGenerator(MessageBasedAppGenerator):
conversation_id: str,
message_id: str,
user_from: UserFrom,
is_resume: bool = False,
) -> None:
from libs.flask_utils import preserve_flask_contexts
@@ -282,30 +279,20 @@ class AgentAppGenerator(MessageBasedAppGenerator):
message = self._get_message(message_id)
app_config = application_generate_entity.app_config
if is_resume:
# ENG-638: a resume continues a paused agent run; the human's
# reply is threaded in by the runner as deferred_tool_results.
# The query is the replayed paused-turn message, kept only to
# match the suspended snapshot's layers — it is NOT new
# end-user input, so input guards must NOT run. Moderation or an
# annotation match on the replayed query would short-circuit the
# turn and drop the human reply, stranding the ask_human session.
query = application_generate_entity.query or ""
else:
# Apply app-level input guards (content moderation + annotation
# reply) before reaching the Agent backend, mirroring the EasyUI
# chat / agent-chat runners. These can short-circuit the turn.
app_model = db.session.get(App, app_config.app_id)
if app_model is None:
raise AgentAppGeneratorError("App not found")
handled, query = self._run_input_guards(
application_generate_entity=application_generate_entity,
app_model=app_model,
message=message,
queue_manager=queue_manager,
)
if handled:
return
# Apply app-level input guards (content moderation + annotation
# reply) before reaching the Agent backend, mirroring the EasyUI
# chat / agent-chat runners. These can short-circuit the turn.
app_model = db.session.get(App, app_config.app_id)
if app_model is None:
raise AgentAppGeneratorError("App not found")
handled, query = self._run_input_guards(
application_generate_entity=application_generate_entity,
app_model=app_model,
message=message,
queue_manager=queue_manager,
)
if handled:
return
dify_context = DifyRunContext(
tenant_id=app_config.tenant_id,
+5 -7
View File
@@ -118,18 +118,16 @@ class WaterCrawlAPIClient(BaseAPIClient):
response.raise_for_status()
if response.status_code == 204:
return None
content_type = response.headers.get("Content-Type", "")
media_type = content_type.split(";", 1)[0].strip().lower()
if media_type == "application/json":
if response.headers.get("Content-Type") == "application/json":
return response.json() or {}
if media_type == "application/octet-stream":
if response.headers.get("Content-Type") == "application/octet-stream":
return response.content
if media_type == "text/event-stream":
if response.headers.get("Content-Type") == "text/event-stream":
return self.process_eventstream(response)
raise Exception(f"Unknown response type: {content_type}")
raise Exception(f"Unknown response type: {response.headers.get('Content-Type')}")
def get_crawl_requests_list(self, page: int | None = None, page_size: int | None = None):
query_params = {"page": page or 1, "page_size": page_size or 10}
@@ -219,7 +217,7 @@ class WaterCrawlAPIClient(BaseAPIClient):
return event_data["data"]
def download_result(self, result_object: dict[str, Any]):
response = httpx.get(result_object["result"], timeout=30)
response = httpx.get(result_object["result"], timeout=None)
try:
response.raise_for_status()
result_object["result"] = response.json()
+1 -19
View File
@@ -382,7 +382,7 @@ Check if activation token is valid
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentAppUpdatePayload](#agentappupdatepayload)<br> |
| Yes | **application/json**: [UpdateAppPayload](#updateapppayload)<br> |
#### Responses
@@ -11286,7 +11286,6 @@ Default namespace
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| name | string | Agent name | Yes |
| role | string | Agent role | No |
#### AgentAppFeaturesPayload
@@ -11305,19 +11304,6 @@ default (the config form sends the full desired feature state on save).
| suggested_questions_after_answer | [AgentSuggestedQuestionsAfterAnswerFeatureConfig](#agentsuggestedquestionsafteranswerfeatureconfig) | Follow-up suggestions config, e.g. {'enabled': true} | No |
| text_to_speech | [AgentTextToSpeechFeatureConfig](#agenttexttospeechfeatureconfig) | Text-to-speech config | No |
#### AgentAppUpdatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | App description (max 400 chars) | No |
| icon | string | Icon | No |
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| max_active_requests | integer | Maximum active requests | No |
| name | string | App name | Yes |
| role | string | Agent role | No |
| use_icon_as_answer_icon | boolean | Use icon as answer icon | No |
#### AgentCliToolAuthorizationStatus
Authorization state for Agent-scoped CLI tools.
@@ -12516,7 +12502,6 @@ Enum class for api provider schema type.
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| api_base_url | string | | No |
| app_id | string | | No |
| bound_agent_id | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
@@ -12533,7 +12518,6 @@ Enum class for api provider schema type.
| mode | string | | Yes |
| model_config | [ModelConfig](#modelconfig) | | No |
| name | string | | Yes |
| role | string | | No |
| site | [Site](#site) | | No |
| tags | [ [Tag](#tag) ] | | No |
| tracing | [JSONValue](#jsonvalue) | | No |
@@ -12638,7 +12622,6 @@ AppMCPServer Status Enum
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| app_id | string | | No |
| author_name | string | | No |
| bound_agent_id | string | | No |
| create_user_name | string | | No |
@@ -12656,7 +12639,6 @@ AppMCPServer Status Enum
| mode | string | | Yes |
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| name | string | | Yes |
| role | string | | No |
| tags | [ [Tag](#tag) ] | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
+4 -56
View File
@@ -282,7 +282,6 @@ class AgentRosterService:
app_id: str,
name: str,
description: str = "",
role: str = "",
icon_type: Any = None,
icon: str | None = None,
icon_background: str | None = None,
@@ -299,7 +298,7 @@ class AgentRosterService:
tenant_id=tenant_id,
name=name,
description=description,
role=role,
role="",
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
@@ -342,21 +341,6 @@ class AgentRosterService:
self._session.flush()
return agent
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:
return {}
agents = self._session.scalars(
select(Agent).where(
Agent.tenant_id == tenant_id,
Agent.app_id.in_(app_ids),
Agent.scope == AgentScope.ROSTER,
Agent.source == AgentSource.AGENT_APP,
Agent.status == AgentStatus.ACTIVE,
)
).all()
return {agent.app_id: agent for agent in agents if agent.app_id}
def get_app_backing_agent(self, *, tenant_id: str, app_id: str) -> Agent | None:
"""Return the roster Agent that backs the given Agent App, if any."""
return self._session.scalar(
@@ -460,36 +444,12 @@ class AgentRosterService:
agent.updated_by = account_id
self._session.commit()
@staticmethod
def _visible_version_operations(agent: Agent) -> set[AgentConfigRevisionOperation]:
if agent.source == AgentSource.AGENT_APP:
return {AgentConfigRevisionOperation.SAVE_NEW_VERSION}
return {
AgentConfigRevisionOperation.CREATE_VERSION,
AgentConfigRevisionOperation.SAVE_NEW_VERSION,
AgentConfigRevisionOperation.SAVE_NEW_AGENT,
AgentConfigRevisionOperation.SAVE_TO_ROSTER,
}
def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[dict[str, Any]]:
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
visible_version_ids = (
select(AgentConfigRevision.current_snapshot_id)
.where(
AgentConfigRevision.tenant_id == tenant_id,
AgentConfigRevision.agent_id == agent_id,
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
)
.subquery()
)
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
versions = list(
self._session.scalars(
select(AgentConfigSnapshot)
.where(
AgentConfigSnapshot.tenant_id == tenant_id,
AgentConfigSnapshot.agent_id == agent_id,
AgentConfigSnapshot.id.in_(select(visible_version_ids.c.current_snapshot_id)),
)
.where(AgentConfigSnapshot.tenant_id == tenant_id, AgentConfigSnapshot.agent_id == agent_id)
.order_by(AgentConfigSnapshot.version.desc())
).all()
)
@@ -500,19 +460,7 @@ class AgentRosterService:
]
def get_agent_version_detail(self, *, tenant_id: str, agent_id: str, version_id: str) -> dict[str, Any]:
agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
visible_revision_id = self._session.scalar(
select(AgentConfigRevision.id)
.where(
AgentConfigRevision.tenant_id == tenant_id,
AgentConfigRevision.agent_id == agent_id,
AgentConfigRevision.current_snapshot_id == version_id,
AgentConfigRevision.operation.in_(self._visible_version_operations(agent)),
)
.limit(1)
)
if not visible_revision_id:
raise AgentVersionNotFoundError()
self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True)
version = self._get_version(tenant_id=tenant_id, agent_id=agent_id, version_id=version_id)
revisions = list(
self._session.scalars(
+1 -10
View File
@@ -2,7 +2,7 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal, NotRequired, TypedDict, cast, override
from typing import Any, Literal, TypedDict, cast, override
import sqlalchemy as sa
from flask_sqlalchemy.pagination import Pagination
@@ -63,7 +63,6 @@ class CreateAppParams(BaseModel):
name: str = Field(min_length=1)
description: str | None = None
mode: Literal["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"]
agent_role: str = Field(default="", max_length=255)
icon_type: str | None = None
icon: str | None = None
icon_background: str | None = None
@@ -91,8 +90,6 @@ class AppService:
filters.append(App.mode == AppMode.AGENT_CHAT)
elif params.mode == "agent":
filters.append(App.mode == AppMode.AGENT)
elif params.mode == "all":
filters.append(App.mode != AppMode.AGENT)
if isinstance(params, AppListParams):
if params.status:
@@ -415,7 +412,6 @@ class AppService:
app_id=app.id,
name=params.name,
description=params.description or "",
role=params.agent_role,
icon_type=icon_type,
icon=params.icon,
icon_background=params.icon_background,
@@ -511,7 +507,6 @@ class AppService:
icon_background: str
use_icon_as_answer_icon: bool
max_active_requests: int
role: NotRequired[str | None]
@staticmethod
def _get_backing_agent_for_update(app: App) -> Agent | None:
@@ -543,7 +538,6 @@ class AppService:
icon_type: IconType | str | None = None,
icon: str | None = None,
icon_background: str | None = None,
role: str | None = None,
account_id: str | None = None,
updated_at: datetime | None = None,
) -> None:
@@ -566,8 +560,6 @@ class AppService:
agent.icon = icon
if icon_background is not None:
agent.icon_background = icon_background
if role is not None:
agent.role = role
agent.updated_by = account_id
if updated_at is not None:
agent.updated_at = updated_at
@@ -602,7 +594,6 @@ class AppService:
icon_type=app.icon_type,
icon=app.icon,
icon_background=app.icon_background,
role=args.get("role"),
account_id=current_user.id,
updated_at=app.updated_at,
)
+2 -8
View File
@@ -43,16 +43,10 @@ class JinaAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
error_message = response.json().get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
error_message = json.loads(response.text).get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+2 -8
View File
@@ -43,16 +43,10 @@ class JinaAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
error_message = response.json().get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
error_message = json.loads(response.text).get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+2 -8
View File
@@ -37,16 +37,10 @@ class WatercrawlAuth(ApiKeyAuthBase):
def _handle_error(self, response):
if response.status_code in {402, 409, 500}:
try:
error_message = response.json().get("error", "Unknown error occurred")
except ValueError:
error_message = response.text or "Unknown error occurred"
error_message = response.json().get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
else:
if response.text:
try:
error_message = json.loads(response.text).get("error", "Unknown error occurred")
except ValueError:
error_message = response.text
error_message = json.loads(response.text).get("error", "Unknown error occurred")
raise Exception(f"Failed to authorize. Status code: {response.status_code}. Error: {error_message}")
raise Exception(f"Unexpected error occurred while trying to authorize. Status code: {response.status_code}")
+2 -16
View File
@@ -95,30 +95,16 @@ def check_and_handle_human_input_timeouts(limit: int = 100) -> None:
timeout_status=HumanInputFormStatus.EXPIRED if is_global else HumanInputFormStatus.TIMEOUT,
reason="global_timeout" if is_global else "node_timeout",
)
assert record.workflow_run_id is not None, "workflow_run_id should not be None for non-test form"
if is_global:
# Global timeout applies only to workflow-owned forms
# (_is_global_timeout requires a workflow_run_id): end the run.
assert record.workflow_run_id is not None, "global timeout requires a workflow_run_id"
_handle_global_timeout(
form_id=record.form_id,
workflow_run_id=record.workflow_run_id,
node_id=record.node_id,
session_factory=session_factory,
)
elif record.workflow_run_id is not None:
# Workflow Agent node / Human Input node form: resume the workflow.
service.enqueue_resume(record.workflow_run_id)
elif record.conversation_id is not None:
# ENG-635: Agent v2 chat ask_human form is conversation-owned (no
# workflow_run_id). Resume the chat turn so the timeout is threaded
# back to the agent run as the ask_human deferred_tool_result
# (status="timeout"), mirroring HumanInputService.submit_form_by_token.
service.enqueue_agent_app_resume(conversation_id=record.conversation_id, form_id=record.form_id)
else:
logger.warning(
"Timed-out form %s has neither workflow_run_id nor conversation_id; skipping resume",
record.form_id,
)
service.enqueue_resume(record.workflow_run_id)
except Exception:
logger.exception(
"Failed to handle timeout for form_id=%s workflow_run_id=%s",
@@ -113,7 +113,6 @@ def _app_detail_obj(**overrides):
"deleted_tools": [],
"site": None,
"bound_agent_id": "00000000-0000-0000-0000-000000000001",
"tenant_id": "tenant-1",
}
data.update(overrides)
return SimpleNamespace(**data)
@@ -196,16 +195,6 @@ def test_agent_app_list_and_create_use_agent_route(
return _app_detail_obj(id="app-created", bound_agent_id="agent-created")
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"load_app_backing_agents_by_app_id",
lambda _self, **kwargs: {"app-list": SimpleNamespace(id="agent-list", role="List role")},
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id="agent-created", role="Created role"),
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
@@ -219,8 +208,6 @@ def test_agent_app_list_and_create_use_agent_route(
assert listed["limit"] == 10
assert listed["total"] == 1
assert listed["data"][0]["id"] == "agent-list"
assert listed["data"][0]["app_id"] == "app-list"
assert listed["data"][0]["role"] == "List role"
assert "bound_agent_id" not in listed["data"][0]
list_call = cast(dict[str, object], captured["list"])
list_params = cast(Any, list_call["params"])
@@ -235,8 +222,6 @@ 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["role"] == "Created role"
assert "bound_agent_id" not in created
create_call = cast(dict[str, object], captured["create"])
create_params = cast(Any, create_call["params"])
@@ -255,11 +240,6 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
"get_agent_app_model",
lambda _self, **kwargs: app_model,
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role"),
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
@@ -282,8 +262,6 @@ 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["role"] == "Resolved role"
assert "bound_agent_id" not in detail
with app.test_request_context(
@@ -294,8 +272,6 @@ 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["role"] == "Resolved role"
assert "bound_agent_id" not in updated
update_call = cast(dict[str, object], captured["update"])
assert update_call["app"] is app_model
@@ -0,0 +1,36 @@
import pytest
from flask import Flask
from controllers.console import bp as console_bp
@pytest.fixture
def app() -> Flask:
app = Flask(__name__)
app.config["TESTING"] = True
app.register_blueprint(console_bp)
return app
def test_console_client_metadata_returns_cloudflare_geo(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata", headers={"CF-IPCountry": "cn"})
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": "CN"}}
def test_console_client_metadata_returns_null_geo_without_cloudflare_header(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata")
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": None}}
def test_console_client_metadata_ignores_invalid_cloudflare_geo(app: Flask):
client = app.test_client()
response = client.get("/console/api/client-metadata", headers={"CF-IPCountry": "China"})
assert response.status_code == 200
assert response.get_json() == {"geo": {"country_code": None}}
@@ -201,18 +201,17 @@ class TestGenerateWorker:
mocker.patch(f"{MODULE}.AgentAppRunner", return_value=runner)
return runner
def _call(self, generator, mocker: MockerFixture, queue_manager, *, is_resume=False, query="query"):
def _call(self, generator, mocker: MockerFixture, queue_manager):
generator._generate_worker(
flask_app=mocker.MagicMock(),
context=mocker.MagicMock(),
application_generate_entity=mocker.MagicMock(
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m"), query=query
agent_id="a", agent_config_snapshot_id="s", model_conf=mocker.MagicMock(model="m")
),
queue_manager=queue_manager,
conversation_id="conv",
message_id="msg",
user_from=UserFrom.END_USER,
is_resume=is_resume,
)
def test_happy_path_runs_backend(self, generator, mocker: MockerFixture):
@@ -228,20 +227,6 @@ class TestGenerateWorker:
self._call(generator, mocker, queue_manager)
runner.run.assert_not_called()
def test_resume_skips_input_guards_and_consumes_reply(self, generator, mocker: MockerFixture):
# ENG-638 (review): on resume the replayed query is NOT new end-user input.
# Input guards must be skipped, even if moderation/annotation would match,
# so the run continues and the human reply (deferred_tool_results) is used.
runner = self._wire(generator, mocker, handled=True) # guards WOULD short-circuit
queue_manager = mocker.MagicMock()
self._call(generator, mocker, queue_manager, is_resume=True, query="the approved reply")
generator._run_input_guards.assert_not_called()
runner.run.assert_called_once()
# the replayed paused-turn query flows straight to the runner (snapshot match)
assert runner.run.call_args.kwargs["query"] == "the approved reply"
def test_generate_task_stopped_is_swallowed(self, generator, mocker: MockerFixture):
self._wire(generator, mocker, run_side_effect=GenerateTaskStoppedError())
queue_manager = mocker.MagicMock()
@@ -168,13 +168,6 @@ class TestWaterCrawlAPIClient:
assert client.process_response(_response(200, {"ok": True})) == {"ok": True}
assert client.process_response(_response(200, None)) == {}
def test_process_response_accepts_json_content_type_parameters(self):
client = WaterCrawlAPIClient(api_key="k")
response = _response(200, {"ok": True}, content_type="application/json; charset=utf-8")
assert client.process_response(response) == {"ok": True}
def test_process_response_octet_stream_returns_bytes(self):
client = WaterCrawlAPIClient(api_key="k")
assert (
@@ -249,18 +242,11 @@ class TestWaterCrawlAPIClient:
client = WaterCrawlAPIClient(api_key="k")
response = _response(200, {"markdown": "body"})
captured = {}
def fake_get(*args, **kwargs):
captured.update(kwargs)
return response
monkeypatch.setattr(client_module.httpx, "get", fake_get)
monkeypatch.setattr(client_module.httpx, "get", lambda *args, **kwargs: response)
result = client.download_result({"result": "https://example.com/result.json"})
assert result["result"] == {"markdown": "body"}
assert captured["timeout"] is not None
response.close.assert_called_once()
@@ -29,7 +29,6 @@ from services.agent.composer_validator import ComposerConfigValidator
from services.agent.errors import InvalidComposerConfigError
from services.agent.roster_service import AgentRosterService
from services.agent.workflow_publish_service import WorkflowAgentPublishService
from services.app_service import AppListParams, AppService
from services.entities.agent_entities import AgentSoulConfig, ComposerSavePayload, ComposerSaveStrategy, ComposerVariant
@@ -770,7 +769,7 @@ def test_roster_update_archive_versions_and_detail(monkeypatch):
created_by="account-1",
created_at=revision_created_at,
)
fake_session = FakeSession(scalar=["visible-revision"], scalars=[[listed_version], [revision]])
fake_session = FakeSession(scalars=[[listed_version], [revision]])
agent = Agent(
id="agent-1",
tenant_id="tenant-1",
@@ -826,7 +825,6 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
payload = roster_service.RosterAgentCreatePayload(
name="Analyst",
description="desc",
role="Research assistant",
icon_type="emoji",
icon="A",
icon_background="#fff",
@@ -840,7 +838,6 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
account_id="account-1",
app_id="app-1",
name="Backing Agent",
role="Support agent",
)
found_agent = service._get_agent(tenant_id="tenant-1", agent_id="agent-1")
with pytest.raises(roster_service.AgentNotFoundError):
@@ -852,11 +849,9 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
assert service._load_versions_by_id([]) == {}
assert created.name == "Analyst"
assert created.role == "Research assistant"
assert created.source == AgentSource.ROSTER
assert created.active_config_snapshot_id is not None
assert created.active_config_has_model is False
assert backing_agent.role == "Support agent"
assert backing_agent.active_config_snapshot_id is not None
assert backing_agent.active_config_has_model is False
assert found_agent.id == "agent-1"
@@ -864,30 +859,6 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch):
assert loaded_versions["version-1"].agent_id == "agent-1"
def test_agent_app_visible_versions_exclude_draft_saves():
agent_app = Agent(source=AgentSource.AGENT_APP)
roster_agent = Agent(source=AgentSource.ROSTER)
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 AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in agent_app_operations
assert AgentConfigRevisionOperation.CREATE_VERSION in roster_operations
assert AgentConfigRevisionOperation.SAVE_CURRENT_VERSION not in roster_operations
def test_app_list_all_excludes_agent_apps_by_default():
filters = AppService._build_app_list_filters(
"account-1",
"tenant-1",
AppListParams(mode="all"),
)
sql = " ".join(str(filter_) for filter_ in filters)
assert "apps.mode != :mode_1" in sql
def test_validator_dict_helpers_wrap_validation_errors():
valid_soul = ComposerConfigValidator.validate_agent_soul_dict({"prompt": {"system_prompt": "x"}})
valid_node_job = ComposerConfigValidator.validate_node_job_dict({"workflow_prompt": "x"})
@@ -68,22 +68,6 @@ class TestJinaAuth:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_http_error_with_non_json_text_response(self, mock_post):
"""Test handling of known HTTP errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 402
mock_response.text = "Payment required"
mock_response.json.side_effect = ValueError("Not JSON")
mock_post.return_value = mock_response
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
auth = JinaAuth(credentials)
with pytest.raises(Exception) as exc_info:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_http_409_error(self, mock_post):
"""Test handling of 409 Conflict error"""
@@ -130,22 +114,6 @@ class TestJinaAuth:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_unexpected_error_with_non_json_text_response(self, mock_post):
"""Test handling of unexpected errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 403
mock_response.text = "Forbidden"
mock_response.json.side_effect = Exception("Not JSON")
mock_post.return_value = mock_response
credentials = {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
auth = JinaAuth(credentials)
with pytest.raises(Exception) as exc_info:
auth.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 403. Error: Forbidden"
@patch("services.auth.jina.jina._http_client.post", autospec=True)
def test_should_handle_unexpected_error_without_text(self, mock_post):
"""Test handling of unexpected errors without text response"""
@@ -118,17 +118,6 @@ def test_handle_error_statuses_default_unknown_error(jina_module: ModuleType) ->
auth._handle_error(response)
def test_handle_error_statuses_fall_back_to_text_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
response.status_code = 402
response.text = "Payment required"
response.json.side_effect = ValueError("Not JSON")
with pytest.raises(Exception, match="Status code: 402.*Payment required"):
auth._handle_error(response)
def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
@@ -139,16 +128,6 @@ def test_handle_error_with_text_json_body(jina_module: ModuleType) -> None:
auth._handle_error(response)
def test_handle_error_with_non_json_text_body(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
response.status_code = 403
response.text = "Forbidden"
with pytest.raises(Exception, match="Status code: 403.*Forbidden"):
auth._handle_error(response)
def test_handle_error_with_text_json_body_missing_error(jina_module: ModuleType) -> None:
auth = jina_module.JinaAuth(_credentials(api_key="k"))
response = MagicMock()
@@ -99,25 +99,12 @@ class TestWatercrawlAuth:
auth_instance.validate_credentials()
assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
def test_should_handle_http_error_with_non_json_text_response(self, mock_get, auth_instance):
"""Test handling of known HTTP errors with non-JSON text response."""
mock_response = MagicMock()
mock_response.status_code = 402
mock_response.text = "Payment required"
mock_response.json.side_effect = ValueError("Not JSON")
mock_get.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert str(exc_info.value) == "Failed to authorize. Status code: 402. Error: Payment required"
@pytest.mark.parametrize(
("status_code", "response_text", "has_json_error", "expected_error_contains"),
[
(403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
(404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
(401, "Not JSON", True, "Failed to authorize. Status code: 401. Error: Not JSON"),
(401, "Not JSON", True, "Expecting value"), # JSON decode error
],
)
@patch("services.auth.watercrawl.watercrawl.httpx.get", autospec=True)
@@ -63,7 +63,6 @@ class _FakeFormRepo:
return SimpleNamespace(
form_id=form_id,
workflow_run_id=getattr(form, "workflow_run_id", None),
conversation_id=getattr(form, "conversation_id", None),
node_id=getattr(form, "node_id", None),
)
@@ -71,15 +70,11 @@ class _FakeFormRepo:
class _FakeService:
def __init__(self, _session_factory, form_repository=None):
self.enqueued: list[str] = []
self.agent_app_resumed: list[tuple[str, str]] = []
def enqueue_resume(self, workflow_run_id: str | None) -> None:
if workflow_run_id is not None:
self.enqueued.append(workflow_run_id)
def enqueue_agent_app_resume(self, *, conversation_id: str, form_id: str) -> None:
self.agent_app_resumed.append((conversation_id, form_id))
def _build_form(
*,
@@ -89,7 +84,6 @@ def _build_form(
expiration_time: datetime,
workflow_run_id: str | None,
node_id: str,
conversation_id: str | None = None,
) -> SimpleNamespace:
return SimpleNamespace(
id=form_id,
@@ -97,7 +91,6 @@ def _build_form(
created_at=created_at,
expiration_time=expiration_time,
workflow_run_id=workflow_run_id,
conversation_id=conversation_id,
node_id=node_id,
status=HumanInputFormStatus.WAITING,
)
@@ -215,45 +208,3 @@ def test_check_and_handle_human_input_timeouts_omits_global_filter_when_disabled
assert stmt is not None
stmt_text = str(stmt)
assert "created_at <=" not in stmt_text
def test_check_and_handle_human_input_timeouts_routes_conversation_owned_form_to_agent_app_resume(
monkeypatch: pytest.MonkeyPatch,
):
# ENG-635 (review): a conversation-owned Agent v2 chat ask_human form has no
# workflow_run_id. On timeout it must enqueue the Agent App resume (so the
# timeout is threaded back as the ask_human result), instead of asserting on
# workflow_run_id — which previously raised and was swallowed by the except.
now = datetime(2025, 1, 1, 12, 0, 0)
monkeypatch.setattr(task_module, "naive_utc_now", lambda: now)
monkeypatch.setattr(task_module.dify_config, "HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS", 3600)
monkeypatch.setattr(task_module, "db", SimpleNamespace(engine=object()))
forms = [
_build_form(
form_id="form-chat",
form_kind=HumanInputFormKind.RUNTIME,
created_at=now - timedelta(minutes=5),
expiration_time=now - timedelta(seconds=1),
workflow_run_id=None,
conversation_id="conv-1",
node_id="agent",
),
]
capture: dict[str, Any] = {}
monkeypatch.setattr(task_module, "sessionmaker", lambda *args, **kwargs: _FakeSessionFactory(forms, capture))
repo = _FakeFormRepo(form_map={form.id: form for form in forms})
service = _FakeService(None)
monkeypatch.setattr(task_module, "HumanInputFormSubmissionRepository", lambda: repo)
monkeypatch.setattr(task_module, "HumanInputService", lambda *_args, **_kwargs: service)
monkeypatch.setattr(task_module, "_handle_global_timeout", lambda **_kwargs: None)
task_module.check_and_handle_human_input_timeouts(limit=100)
# Node timeout (conversation forms are never "global"), routed to Agent App resume.
assert repo.calls == [
{"form_id": "form-chat", "timeout_status": HumanInputFormStatus.TIMEOUT, "reason": "node_timeout"}
]
assert service.agent_app_resumed == [("conv-1", "form-chat")]
assert service.enqueued == []
@@ -8,7 +8,6 @@ This module tests the mail sending functionality including:
- Error handling and logging
"""
import logging
import smtplib
from unittest.mock import ANY, MagicMock, patch
@@ -372,7 +371,8 @@ class TestMailTaskRetryLogic:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
def test_mail_task_logs_success(self, mock_mail, mock_email_service, caplog):
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_success(self, mock_logger, mock_mail, mock_email_service):
"""Test that successful mail sends are logged properly."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -380,8 +380,7 @@ class TestMailTaskRetryLogic:
mock_email_service.return_value = mock_service
# Act
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
mock_service.send_email.assert_called_once_with(
@@ -391,14 +390,12 @@ class TestMailTaskRetryLogic:
template_context={"to": "test@example.com", "code": "123456"},
)
# Verify logging calls
log_messages = [record.getMessage() for record in caplog.records]
assert len(log_messages) == 2 # Start and success logs
assert "Start email register mail to test@example.com" in log_messages[0]
assert "Send email register mail to test@example.com succeeded" in log_messages[1]
assert mock_logger.info.call_count == 2 # Start and success logs
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
def test_mail_task_logs_failure(self, mock_mail, mock_email_service, caplog):
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_failure(self, mock_logger, mock_mail, mock_email_service):
"""Test that failed mail sends are logged with exception details."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -407,13 +404,10 @@ class TestMailTaskRetryLogic:
mock_email_service.return_value = mock_service
# Act
with caplog.at_level(logging.ERROR, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
assert "Send email register mail to test@example.com failed" in caplog.text
assert len(caplog.records) == 1
assert caplog.records[0].exc_info is not None
mock_logger.exception.assert_called_once_with("Send email register mail to %s failed", "test@example.com")
@patch("tasks.mail_reset_password_task.get_email_i18n_service")
@patch("tasks.mail_reset_password_task.mail")
@@ -580,7 +574,8 @@ class TestInnerEmailTask:
@patch("tasks.mail_inner_task.get_email_i18n_service")
@patch("tasks.mail_inner_task.mail")
@patch("tasks.mail_inner_task._render_template_with_strategy")
def test_inner_email_task_logs_failure(self, mock_render, mock_mail, mock_email_service, caplog):
@patch("tasks.mail_inner_task.logger")
def test_inner_email_task_logs_failure(self, mock_logger, mock_render, mock_mail, mock_email_service):
"""Test inner email task logs failures properly."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -592,13 +587,10 @@ class TestInnerEmailTask:
to_list = ["user@example.com"]
# Act
with caplog.at_level(logging.ERROR, logger="tasks.mail_inner_task"):
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
send_inner_email_task(to=to_list, subject="Test", body="Body", substitutions={})
# Assert
assert "Send enterprise mail to ['user@example.com'] failed" in caplog.text
assert len(caplog.records) == 1
assert caplog.records[0].exc_info is not None
mock_logger.exception.assert_called_once()
class TestSendGridIntegration:
@@ -898,8 +890,9 @@ class TestPerformanceAndTiming:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
@patch("tasks.mail_register_task.logger")
@patch("tasks.mail_register_task.time")
def test_mail_task_tracks_execution_time(self, mock_time, mock_mail, mock_email_service, caplog):
def test_mail_task_tracks_execution_time(self, mock_time, mock_logger, mock_mail, mock_email_service):
"""Test that mail tasks track and log execution time."""
# Arrange
mock_mail.is_inited.return_value = True
@@ -910,14 +903,13 @@ class TestPerformanceAndTiming:
mock_time.perf_counter.side_effect = [100.0, 100.5] # 0.5 second execution
# Act
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
send_email_register_mail_task(language="en-US", to="test@example.com", code="123456")
# Assert
assert mock_time.perf_counter.call_count == 2
# Verify latency is logged
assert len(caplog.records) == 2
assert "latency: 0.5" in caplog.records[1].getMessage()
success_log_call = mock_logger.info.call_args_list[1]
assert "latency" in str(success_log_call)
class TestEdgeCasesAndErrorHandling:
@@ -1465,7 +1457,8 @@ class TestLoggingAndMonitoring:
@patch("tasks.mail_register_task.get_email_i18n_service")
@patch("tasks.mail_register_task.mail")
def test_mail_task_logs_recipient_information(self, mock_mail, mock_email_service, caplog):
@patch("tasks.mail_register_task.logger")
def test_mail_task_logs_recipient_information(self, mock_logger, mock_mail, mock_email_service):
"""
Test that mail tasks log recipient information for audit trails.
@@ -1478,16 +1471,17 @@ class TestLoggingAndMonitoring:
mock_email_service.return_value = mock_service
# Act
with caplog.at_level(logging.INFO, logger="tasks.mail_register_task"):
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
send_email_register_mail_task(language="en-US", to="audit@example.com", code="123456")
# Assert
# Check that recipient is logged in start message
assert "audit@example.com" in caplog.records[0].getMessage()
start_log_call = mock_logger.info.call_args_list[0]
assert "audit@example.com" in str(start_log_call)
@patch("tasks.mail_inner_task.get_email_i18n_service")
@patch("tasks.mail_inner_task.mail")
def test_inner_email_task_logs_subject_for_tracking(self, mock_mail, mock_email_service, caplog):
@patch("tasks.mail_inner_task.logger")
def test_inner_email_task_logs_subject_for_tracking(self, mock_logger, mock_mail, mock_email_service):
"""
Test that inner email task logs subject for tracking purposes.
@@ -1500,11 +1494,11 @@ class TestLoggingAndMonitoring:
mock_email_service.return_value = mock_service
# Act
with caplog.at_level(logging.INFO, logger="tasks.mail_inner_task"):
send_inner_email_task(
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
)
send_inner_email_task(
to=["user@example.com"], subject="Important Notification", body="<p>Body</p>", substitutions={}
)
# Assert
# Check that subject is logged
assert "Important Notification" in caplog.records[0].getMessage()
start_log_call = mock_logger.info.call_args_list[0]
assert "Important Notification" in str(start_log_call)
+1 -1
View File
@@ -14,7 +14,7 @@ export type TokenStore = {
const DOC_VERSION = 1
export type TokenDoc = {
type TokenDoc = {
version?: number
tokens?: Record<string, Record<string, string>>
}
+67 -11
View File
@@ -6,7 +6,12 @@ app:
mode: advanced-chat
name: echo-bot
use_icon_as_answer_icon: false
dependencies: []
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
kind: app
version: 0.6.0
workflow:
@@ -63,17 +68,26 @@ workflow:
edges:
- data:
sourceType: start
targetType: answer
id: 1779690795511-answer
targetType: llm
id: 1779690795511-llm
source: '1779690795511'
sourceHandle: source
target: llm
targetHandle: target
type: custom
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
sourceHandle: source
target: answer
targetHandle: target
type: custom
nodes:
- data:
selected: false
title: User Input
title: 用户输入
type: start
variables:
- default: ''
@@ -93,19 +107,42 @@ workflow:
positionAbsolute:
x: 79
y: 282
selected: false
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
answer: 'echo:{{#sys.query#}}'
context:
enabled: false
variable_selector: []
memory:
query_prompt_template: '{{#sys.query#}}
{{#sys.files#}}'
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: 9b866a63-3619-4f5c-a46f-0aed04078587
role: system
text: 'User says: {{{#sys.query#}} Reply exactly: echo:{{#sys.query#}}'
selected: false
title: Reply
type: answer
variables: []
height: 103
id: answer
title: LLM
type: llm
vision:
enabled: false
height: 88
id: llm
position:
x: 380
y: 282
@@ -117,6 +154,25 @@ workflow:
targetPosition: left
type: custom
width: 242
- data:
answer: '{{#llm.text#}}'
selected: false
title: 直接回复
type: answer
variables: []
height: 103
id: answer
position:
x: 680
y: 282
positionAbsolute:
x: 680
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
viewport:
x: 151
y: 125
+57 -7
View File
@@ -6,7 +6,12 @@ app:
mode: workflow
name: basic_auto_test
use_icon_as_answer_icon: false
dependencies: []
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
kind: app
version: 0.6.0
workflow:
@@ -65,10 +70,21 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: end
id: 1779097154262-source-1779171097399-target
targetType: llm
id: 1779097154262-source-1779097204645-target
source: '1779097154262'
sourceHandle: source
target: '1779097204645'
targetHandle: target
type: custom
zIndex: 0
- data:
isInLoop: false
sourceType: llm
targetType: end
id: 1779097204645-source-1779171097399-target
source: '1779097204645'
sourceHandle: source
target: '1779171097399'
targetHandle: target
type: custom
@@ -76,7 +92,7 @@ workflow:
nodes:
- data:
selected: true
title: User Input
title: 用户输入
type: start
variables:
- default: ''
@@ -127,15 +143,49 @@ workflow:
targetPosition: left
type: custom
width: 242
- data:
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: 1ddb3202-d84c-4faf-afe3-424eedc9049a
role: system
text: 'User says:{{#1779097154262.x#}}. Reply exactly: echo:{{#1779097154262.x#}}
'
selected: false
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1779097204645'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1779097154262'
- x
- '1779097204645'
- text
value_type: string
variable: x
selected: false
title: Output
title: 输出
type: end
height: 88
id: '1779171097399'
+66 -10
View File
@@ -6,7 +6,12 @@ app:
mode: advanced-chat
name: DIFY_E2E_FILE_CHAT
use_icon_as_answer_icon: false
dependencies: []
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
kind: app
version: 0.6.0
workflow:
@@ -63,17 +68,26 @@ workflow:
edges:
- data:
sourceType: start
targetType: answer
id: 1780453002656-answer
targetType: llm
id: 1780453002656-llm
source: '1780453002656'
sourceHandle: source
target: llm
targetHandle: target
type: custom
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
sourceHandle: source
target: answer
targetHandle: target
type: custom
nodes:
- data:
selected: false
title: User Input
title: 用户输入
type: start
variables:
- allowed_file_extensions: []
@@ -105,19 +119,61 @@ workflow:
type: custom
width: 242
- data:
answer: ok
context:
enabled: false
variable_selector: []
memory:
query_prompt_template: '{{#sys.query#}}
{{#sys.files#}}'
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: ebc516ad-be6b-4a78-af32-77f447305b34
role: system
text: 输出固定内容:""hello
selected: false
title: Reply
type: answer
variables: []
height: 103
id: answer
title: LLM
type: llm
vision:
enabled: false
height: 88
id: llm
position:
x: 380
y: 282
positionAbsolute:
x: 380
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
answer: '{{#llm.text#}}'
selected: false
title: 直接回复
type: answer
variables: []
height: 103
id: answer
position:
x: 680
y: 282
positionAbsolute:
x: 680
y: 282
selected: false
sourcePosition: right
targetPosition: left
+60 -6
View File
@@ -6,7 +6,12 @@ app:
mode: workflow
name: file_auto_test
use_icon_as_answer_icon: false
dependencies: []
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
kind: app
version: 0.6.0
workflow:
@@ -65,10 +70,22 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: end
id: 1779693724732-source-1779693765299-target
targetType: llm
id: 1779693724732-source-1779693759949-target
source: '1779693724732'
sourceHandle: source
target: '1779693759949'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: llm
targetType: end
id: 1779693759949-source-1779693765299-target
source: '1779693759949'
sourceHandle: source
target: '1779693765299'
targetHandle: target
type: custom
@@ -76,7 +93,7 @@ workflow:
nodes:
- data:
selected: true
title: User Input
title: 用户输入
type: start
variables:
- allowed_file_extensions: []
@@ -123,9 +140,46 @@ workflow:
type: custom
width: 242
- data:
outputs: []
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: bb929f8f-5fa9-415b-91c3-c30228488dcf
role: system
text: 直接输出内容:hello
selected: false
title: Output
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1779693759949'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1779693759949'
- text
value_type: string
variable: x
selected: false
title: 输出
type: end
height: 88
id: '1779693765299'
+59 -7
View File
@@ -6,7 +6,12 @@ app:
mode: workflow
name: auto_test_workspace2
use_icon_as_answer_icon: false
dependencies: []
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/tongyi:0.1.48@966d88dc40611f067311c1c9839139ebc4b55bff471bc5e736dc3e828bc67b46
version: null
kind: app
version: 0.6.0
workflow:
@@ -65,10 +70,22 @@ workflow:
isInIteration: false
isInLoop: false
sourceType: start
targetType: end
id: 1780305524693-source-1780305600095-target
targetType: llm
id: 1780305524693-source-1780305526186-target
source: '1780305524693'
sourceHandle: source
target: '1780305526186'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: llm
targetType: end
id: 1780305526186-source-1780305600095-target
source: '1780305526186'
sourceHandle: source
target: '1780305600095'
targetHandle: target
type: custom
@@ -76,7 +93,7 @@ workflow:
nodes:
- data:
selected: false
title: User Input
title: 用户输入
type: start
variables: []
height: 73
@@ -92,9 +109,45 @@ workflow:
type: custom
width: 242
- data:
outputs: []
context:
enabled: false
variable_selector: []
model:
completion_params:
temperature: 0.7
mode: chat
name: qwen3.6-plus
provider: langgenius/tongyi/tongyi
prompt_template:
- id: cd753cdd-d950-44bf-99ad-7cb19f42d5b6
role: system
text: 输出内容:hello
selected: false
title: Output
title: LLM
type: llm
vision:
enabled: false
height: 88
id: '1780305526186'
position:
x: 382
y: 282
positionAbsolute:
x: 382
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 242
- data:
outputs:
- value_selector:
- '1780305526186'
- text
value_type: string
variable: x
selected: false
title: 输出
type: end
height: 88
id: '1780305600095'
@@ -104,7 +157,6 @@ workflow:
positionAbsolute:
x: 684
y: 282
selected: false
sourcePosition: right
targetPosition: left
type: custom
+11 -8
View File
@@ -8,13 +8,11 @@
* withTempConfig) to prevent session state leaking between tests.
*/
import type { TokenDoc } from '@/store/token-store'
import { Buffer } from 'node:buffer'
import { execSync, spawn } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import yaml from 'js-yaml'
/** Path to the dev entry point — no build required. */
export const BIN = resolve(__dirname, '../../../bin/dev.js')
@@ -210,11 +208,13 @@ function splitHost(host: string): { bare: string, scheme: string } {
}
async function writeFileToken(configDir: string, host: string, email: string, bearer: string): Promise<void> {
const doc: TokenDoc = {
version: 1,
tokens: { [host]: { [email]: bearer } },
const dotParts = `tokens.${host}.${email}`.split('.')
let yaml = ''
for (let i = 0; i < dotParts.length - 1; i++) {
yaml += `${' '.repeat(i) + dotParts[i]}:\n`
}
await writeFile(join(configDir, 'tokens.yml'), yaml.dump(doc, { lineWidth: -1, noRefs: true }), { mode: 0o600 })
yaml += `${' '.repeat(dotParts.length - 1) + (dotParts[dotParts.length - 1] ?? '')}: "${bearer}"\n`
await writeFile(join(configDir, 'tokens.yml'), yaml, { mode: 0o600 })
}
/**
@@ -289,8 +289,11 @@ export async function injectAuth(configDir: string, opts: AuthInjectionOptions):
new Entry('difyctl', account).setPassword(JSON.stringify(opts.bearer))
}
else {
// Fall back to tokens.yml — FileTokenStore uses getTyped<TokenDoc>()
// which expects flat tokens[host][email] with version: 1.
// Fall back to tokens.yml.
// YamlStore.doGet splits the key on '.' and traverses the nested object,
// so "tokens.localhost.user@dify.ai" splits into 4 parts:
// tokens -> localhost -> user@dify -> ai
// The YAML must mirror that exact nesting.
await writeFileToken(configDir, bare, email, opts.bearer)
}
}
@@ -349,11 +349,9 @@ describe('E2E / agent skill — describe app -o json (auth required)', () => {
assertPipeFriendlyJson(r)
})
itWithAuth('[P0] invalid (non-UUID) app id → exit 2 + usage error envelope', async () => {
// 'app-id-nonexistent-e2e-xyz' is not a valid UUID; describe app rejects it
// client-side via isValidUuid() with usage_invalid_flag (exit 2).
itWithAuth('[P0] nonexistent app → exit 1 + JSON error envelope', async () => {
const r = await fx.r(['describe', 'app', 'app-id-nonexistent-e2e-xyz', '-o', 'json'])
expect(r.exitCode).toBe(2)
expect(r.exitCode).toBe(1)
assertErrorEnvelope(r)
})
})
@@ -131,12 +131,11 @@ describe('E2E / difyctl describe app', () => {
// ── Not found ─────────────────────────────────────────────────────────────
it('[P0] invalid (non-UUID) app id returns usage error (exit code 2)', async () => {
// NONEXISTENT_ID is not a valid UUID, so the CLI rejects it client-side via
// isValidUuid() before making any network request → usage_invalid_flag (exit 2).
it('[P0] non-existent app returns exit code 1 with not-found error (3.83)', async () => {
// Spec 3.83: describe non-existent app → stderr contains not-found, exit code 1.
const result = await fx.r(['describe', 'app', NONEXISTENT_ID])
expect(result.exitCode, 'invalid UUID should exit with code 2').toBe(2)
expect(result.stderr).toMatch(/uuid|valid|usage_invalid_flag/i)
expect(result.exitCode, 'non-existent app should exit with code 1').toBe(1)
expect(result.stderr).toMatch(/not.?found|404|does not exist|server_5xx/i)
})
it('[P1] non-existent app in JSON mode outputs JSON error envelope', async () => {
@@ -37,7 +37,6 @@ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest'
import { ZERO } from '@/util/uuid.js'
import {
assertErrorEnvelope,
assertExitCode,
assertNoAnsi,
assertNonZeroExit,
} from '../../helpers/assert.js'
@@ -142,7 +141,7 @@ describe('E2E / error message standards (spec 5.3)', () => {
// Spec 5.70: submitting a value of the wrong type must fail.
// The workflow app (workflowAppId) expects x as a string; passing a JSON
// number causes the server to reject the request.
// After the @accepts/@returns contract unification, the server returns HTTP 422 for request schema failures.
// In v1.0 the server returns HTTP 500 for type validation failures.
const result = await fx.r([
'run',
'app',
@@ -157,206 +156,6 @@ describe('E2E / error message standards (spec 5.3)', () => {
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
// ── 5.70a/b/c P4 sanitization — 422 error body is clean (no leaks) ────────
it('[P0] 5.70a validation failure message is a plain string, not double-encoded JSON', async () => {
// After the @accepts contract fix, the server aborts with
// abort(422, message="Request validation failed", errors=[...])
// The CLI wraps this into its envelope. The message field must be a plain
// human-readable string — NOT a JSON-serialised string that itself contains
// pydantic error details (which was the double-encoding bug in P4).
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
const envelope = assertErrorEnvelope(result)
// message must be a plain string, not a JSON string (no double encoding)
expect(typeof envelope.error.message).toBe('string')
expect(() => JSON.parse(envelope.error.message)).toThrow()
})
it('[P1] 5.70b validation error response does not leak pydantic version URL', async () => {
// Before the P4 fix, exc.json() included a "url" field pointing to
// https://errors.pydantic.dev/<version>/... — exposing the server's pydantic
// version. The sanitised response must not contain this URL.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
expect(result.stderr).not.toMatch(/errors\.pydantic\.dev|pydantic\.dev\//)
})
it('[P1] 5.70c validation error response does not echo back user input', async () => {
// Before the P4 fix, exc.json() included the user's original "input" value
// inside the error details. The sanitised response must not repeat the
// submitted value so that sensitive payloads are not reflected to callers.
const sentValue = 'not-a-number-sentinel-12345'
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: sentValue, enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
assertNonZeroExit(result)
expect(result.stderr).not.toContain(sentValue)
})
// ── 5.70d-h ErrorBody contract — error.server structure and rendering priorities ──
// PR #37285 introduces canonical ErrorBody on every /openapi/v1 non-2xx response.
// CLI strict-parses via zErrorBody.safeParse; success → full struct at error.server.
//
// V2 rendering priorities (format.ts, verified against codebase):
// header code : server?.code ?? cliCode — server wins, CLI fallback
// hint : cliHint ?? server?.hint — CLI wins, server fallback (V2 correction)
// details : server?.details[] — " - loc: msg (type)" per entry, no -v
it('[P0] 5.70d JSON envelope contains error.server with canonical code/status/message', async () => {
// Trigger: describe app ZERO — server returns canonical 404 ErrorBody
// { code:"not_found", status:404, message:"app not found" }.
// zErrorBody.safeParse succeeds → error.server is populated on the current server.
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, server?: { code: string, status: number, message: string } }
}
expect(envelope.error.server, 'error.server must be present when server returns canonical ErrorBody').toBeDefined()
expect(typeof envelope.error.server?.code, 'error.server.code must be a string').toBe('string')
expect(envelope.error.server?.code.length).toBeGreaterThan(0)
expect(typeof envelope.error.server?.status, 'error.server.status must be a number').toBe('number')
expect(typeof envelope.error.server?.message, 'error.server.message must be a string').toBe('string')
expect(envelope.error.server?.message.length).toBeGreaterThan(0)
})
it('[P1] 5.70e @accepts query validation returns canonical 422 with details array', async () => {
// Trigger: direct fetch to GET /apps?page=not-integer — @accepts(query=AppListQuery)
// validates page as int and emits canonical 422 ErrorBody with details[].
// Direct fetch is used because the CLI validates --page as integer client-side
// (would exit 2 before hitting the server); this pins the server-side contract.
const res = await fetch(
`${E.host.replace(/\/$/, '')}/openapi/v1/apps?workspace_id=${E.workspaceId}&page=not-an-integer`,
{ headers: { Authorization: `Bearer ${E.token}` }, signal: AbortSignal.timeout(8_000) },
)
expect(res.status).toBe(422)
const body = await res.json() as {
code?: string
status?: number
details?: Array<{ type: string, loc: Array<string | number>, msg: string }>
}
expect(body.code).toBe('invalid_param')
expect(body.status).toBe(422)
expect(Array.isArray(body.details), 'details must be an array').toBe(true)
expect(body.details!.length).toBeGreaterThan(0)
const entry = body.details![0]!
expect(typeof entry.type).toBe('string')
expect(typeof entry.msg).toBe('string')
expect(Array.isArray(entry.loc)).toBe(true)
})
it('[P1] 5.70g rendering priority — header code: server code wins over CLI classification code', async () => {
// renderHuman: headerCode = server?.code ?? e.code (server wins, V2 unchanged)
// When canonical ErrorBody is parsed, the server semantic code replaces the CLI
// classification code ("server_4xx_other") in the human-readable output header.
// Trigger: describe app ZERO → canonical 404; header starts with "not_found:".
const result = await fx.r(['describe', 'app', ZERO])
assertNonZeroExit(result)
expect(result.stderr.trimStart()).not.toMatch(/^server_4xx_other:/)
expect(result.stderr.trimStart()).toMatch(/^not_found:/)
})
it('[P1] 5.70g2 rendering priority — hint: CLI hint wins over server hint (V2 correction)', async () => {
// renderHuman: hint = cliHint ?? server?.hint (CLI wins — V2 spec correction)
// V1 incorrectly documented "server wins"; V2 aligns with codebase: CLI wins.
// Test: 401 AuthExpired — classifyResponse sets c.hint = AUTH_LOGIN_HINT before
// serverError is parsed; CLI hint takes precedence over any server-provided hint.
// Verified on current server (no @accepts deployment required).
const unauthTmp = await withTempConfig()
try {
const result = await run(['get', 'app', '-o', 'json'], { configDir: unauthTmp.configDir })
assertExitCode(result, 4)
const envelope = JSON.parse(result.stderr.trim()) as { error: { hint?: string } }
expect(envelope.error.hint, 'CLI login hint must appear for auth error').toMatch(/auth login/i)
}
finally {
await unauthTmp.cleanup()
}
})
it('[P1] 5.70h JSON envelope: error.code = CLI classification; error.server.code = server semantic code', async () => {
// toEnvelope() sets error.code from HTTP status bucket (e.g. "server_4xx_other")
// while the server's semantic code is separate in error.server.code.
// Agents can branch on error.server.code without parsing human-readable text.
// Trigger: describe app ZERO → canonical 404; error.code="server_4xx_other",
// error.server.code="not_found" — always distinct when ErrorBody is present.
const result = await fx.r(['describe', 'app', ZERO, '-o', 'json'])
assertNonZeroExit(result)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, server?: { code: string } }
}
expect(envelope.error.code).toBe('server_4xx_other')
expect(envelope.error.server?.code).toBeDefined()
expect(envelope.error.server?.code).not.toBe('server_4xx_other')
})
// ── 5.70i / 5.70j PR #37285 boundary contract ───────────────────────────
it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => {
// PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration.
// Previously, an unknown path under /openapi/v1/ returned flask-restx's default
// 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table.
// After the fix it must return a canonical ErrorBody and contain no suggestions.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, {
headers: { Authorization: `Bearer ${E.token}` },
signal: AbortSignal.timeout(8_000),
})
expect(res.status).toBe(404)
const body = await res.json() as Record<string, unknown>
// canonical ErrorBody fields must be present
expect(typeof body.code, '404 body must have a string code field').toBe('string')
expect(body.status, '404 body must have status: 404').toBe(404)
// no flask-restx route enumeration in the response
const raw = JSON.stringify(body)
expect(raw).not.toMatch(/did you mean/i)
expect(raw).not.toMatch(/you might want/i)
})
it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => {
// PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the
// ErrorBody contract. This test pins that contract:
// - The device/token endpoint returns RFC 8628 {error: string} on failure,
// not the canonical {code, status, message} shape.
// - When the CLI's classifyResponse encounters this, zErrorBody.safeParse
// returns failure → serverError = undefined → generic status-based message,
// no error.server field, no crash.
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }),
signal: AbortSignal.timeout(8_000),
})
// device flow errors are 4xx (400 bad_request or 401 expired_token etc.)
expect(res.status).toBeGreaterThanOrEqual(400)
expect(res.status).toBeLessThan(500)
const body = await res.json() as Record<string, unknown>
// RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair
expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string')
expect(body).not.toHaveProperty('status')
// zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message
})
// ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ────────
it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => {
-407
View File
@@ -1,407 +0,0 @@
/**
* E2E: difyctl get/create/delete/set member — Member Management
*
* Data lifecycle:
* beforeAll — generates two random emails, invites them as test fixtures
* afterAll — removes both fixtures (best-effort)
*
* Email format: auto_test+<timestamp>@dify.ai
* No extra env vars required.
*
* JSON response shape (MemberListResponse):
* { page, limit, total, has_more, data: MemberResponse[] }
*
* MemberResponse fields:
* { id, name, email, role, status, avatar?, current: bool }
*/
import type { AuthFixture } from '../../helpers/cli.js'
import { afterAll, beforeAll, describe, expect, inject, it } from 'vitest'
import {
assertErrorEnvelope,
assertExitCode,
assertJson,
assertNoAnsi,
assertNonZeroExit,
} from '../../helpers/assert.js'
import { run, withAuthFixture, withTempConfig } from '../../helpers/cli.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
const E = resolveEnv(caps)
// ── Fixture state ─────────────────────────────────────────────────────────────
let fx: AuthFixture
/** ID of the member used by get / set tests. */
let testMemberId: string
/** ID of the member reserved for the delete-success test. */
let deleteTargetId: string
const ts = Date.now()
const memberEmail = `auto_test+${ts}@dify.ai`
const deleteTargetEmail = `auto_test+${ts + 1}@dify.ai`
// ── Response type helpers ─────────────────────────────────────────────────────
type MemberItem = {
id: string
name: string
email: string
role: string
status: string
current?: boolean
}
type MemberListJson = {
data: MemberItem[]
total: number
page: number
limit: number
has_more: boolean
}
// ── Setup / teardown ──────────────────────────────────────────────────────────
beforeAll(async () => {
fx = await withAuthFixture(E)
// Invite the main test member; capture member_id from response
const createMain = await fx.r([
'create',
'member',
'--email',
memberEmail,
'--role',
'normal',
'-o',
'json',
])
if (createMain.exitCode !== 0) {
throw new Error(
`beforeAll: failed to create test member (${memberEmail}): ${createMain.stderr}`,
)
}
const mainData = JSON.parse(createMain.stdout.trim()) as { member_id?: string }
testMemberId = mainData.member_id as string
if (!testMemberId)
throw new Error(`beforeAll: missing member_id in: ${createMain.stdout}`)
// Invite the delete-target member
const createTarget = await fx.r([
'create',
'member',
'--email',
deleteTargetEmail,
'--role',
'normal',
'-o',
'json',
])
if (createTarget.exitCode !== 0) {
throw new Error(
`beforeAll: failed to create delete-target member (${deleteTargetEmail}): ${createTarget.stderr}`,
)
}
const targetData = JSON.parse(createTarget.stdout.trim()) as { member_id?: string }
deleteTargetId = targetData.member_id as string
if (!deleteTargetId)
throw new Error(`beforeAll: missing member_id in: ${createTarget.stdout}`)
})
afterAll(async () => {
if (testMemberId) {
await fx.r(['delete', 'member', testMemberId, '--yes']).catch(() => {})
}
if (deleteTargetId) {
await fx.r(['delete', 'member', deleteTargetId, '--yes']).catch(() => {})
}
await fx.cleanup()
})
// ── get member ────────────────────────────────────────────────────────────────
describe('E2E / difyctl get member', () => {
it('[P0] member list contains the created test member', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const ids = (data.data ?? []).map(m => m.id)
expect(ids, `testMemberId ${testMemberId} must appear in member list`).toContain(testMemberId)
})
it('[P0] default table output contains required column headers', async () => {
const result = await fx.r(['get', 'member'])
assertExitCode(result, 0)
expect(result.stdout).toMatch(/\bID\b/)
expect(result.stdout).toMatch(/\bNAME\b/)
expect(result.stdout).toMatch(/\bEMAIL\b/)
expect(result.stdout).toMatch(/\bROLE\b/)
expect(result.stdout).toMatch(/\bSTATUS\b/)
})
it('[P0] authenticated account appears in member list', async () => {
// The token owner (auto_test@dify.ai) must appear in the member list
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const ownerRow = data.data.find(m => m.email === E.email)
expect(ownerRow, `owner email ${E.email} must be in member list`).toBeDefined()
expect(ownerRow?.role).toBe('owner')
expect(ownerRow?.status).toBe('active')
})
it('[P0] -o json returns valid JSON with data array', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
expect(Array.isArray(data.data), 'data must be an array').toBe(true)
expect(data.data.length).toBeGreaterThan(0)
})
it('[P0] -o json each member has id, email, role, status fields', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
const member = data.data[0]!
expect(typeof member.id).toBe('string')
expect(typeof member.email).toBe('string')
expect(typeof member.role).toBe('string')
expect(typeof member.status).toBe('string')
})
it('[P0] output has no ANSI colour codes (non-TTY)', async () => {
const result = await fx.r(['get', 'member'])
assertExitCode(result, 0)
assertNoAnsi(result.stdout, 'stdout')
})
it('[P0] unauthenticated get member returns auth error (exit code 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(['get', 'member'], { configDir: tmp.configDir })
assertExitCode(result, 4)
expect(result.stderr).toMatch(/not.?logged.?in|auth.?login/i)
}
finally {
await tmp.cleanup()
}
})
it('[P1] -o yaml returns valid YAML (non-empty, no JSON braces)', async () => {
const result = await fx.r(['get', 'member', '-o', 'yaml'])
assertExitCode(result, 0)
expect(result.stdout.trim().length).toBeGreaterThan(0)
expect(result.stdout.trimStart()).not.toMatch(/^\{/)
})
it('[P1] -o json output is pipe-friendly (no ANSI, ends with newline)', async () => {
const result = await fx.r(['get', 'member', '-o', 'json'])
assertExitCode(result, 0)
assertNoAnsi(result.stdout, 'stdout')
expect(result.stdout.endsWith('\n')).toBe(true)
})
it('[P1] -w overrides the workspace', async () => {
const result = await fx.r(['get', 'member', '-w', E.workspaceId, '-o', 'json'])
assertExitCode(result, 0)
const data = assertJson<MemberListJson>(result)
expect(Array.isArray(data.data)).toBe(true)
})
})
// ── set member ────────────────────────────────────────────────────────────────
describe('E2E / difyctl set member', () => {
it('[P0] owner/admin can promote normal → admin', async () => {
const result = await fx.r(['set', 'member', testMemberId, '--role', 'admin', '-o', 'json'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const updated = data.data.find(m => m.id === testMemberId)
expect(updated?.role).toBe('admin')
})
it('[P0] owner/admin can demote admin → normal', async () => {
await fx.r(['set', 'member', testMemberId, '--role', 'admin'])
const result = await fx.r(['set', 'member', testMemberId, '--role', 'normal', '-o', 'json'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const updated = data.data.find(m => m.id === testMemberId)
expect(updated?.role).toBe('normal')
})
it('[P0] --role owner is rejected client-side (exit 2, no API call)', async () => {
const result = await fx.r(['set', 'member', testMemberId, '--role', 'owner'])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role|owner/i)
})
it('[P0] missing --role returns usage error', async () => {
const result = await fx.r(['set', 'member', testMemberId])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/role|required|missing/i)
})
it('[P0] unauthenticated set member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(['set', 'member', testMemberId, '--role', 'normal'], {
configDir: tmp.configDir,
})
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
it('[P1] missing member-id returns usage error', async () => {
const result = await fx.r(['set', 'member', '--role', 'normal'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/missing|required|arg|member/i)
})
it('[P1] non-existent member-id returns server error', async () => {
const result = await fx.r([
'set',
'member',
'00000000-0000-0000-0000-000000000000',
'--role',
'normal',
])
assertNonZeroExit(result)
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
})
// ── create member — error paths ───────────────────────────────────────────────
describe('E2E / difyctl create member (error paths)', () => {
it('[P0] --role with invalid value is rejected client-side (exit 2)', async () => {
const result = await fx.r([
'create',
'member',
'--email',
`auto_test+unused${Date.now()}@dify.ai`,
'--role',
'superadmin',
])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role/i)
})
it('[P0] --role owner is rejected client-side (exit 2)', async () => {
const result = await fx.r([
'create',
'member',
'--email',
`auto_test+unused${Date.now()}@dify.ai`,
'--role',
'owner',
])
assertExitCode(result, 2)
expect(result.stderr).toMatch(/invalid|role|owner/i)
})
it('[P0] missing --email returns usage error', async () => {
const result = await fx.r(['create', 'member', '--role', 'normal'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/email|required|missing/i)
})
it('[P0] missing --role returns usage error', async () => {
const result = await fx.r(['create', 'member', '--email', memberEmail])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/role|required|missing/i)
})
it('[P0] unauthenticated create member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(
['create', 'member', '--email', `auto_test+unauth${Date.now()}@dify.ai`, '--role', 'normal'],
{ configDir: tmp.configDir },
)
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
})
// ── delete member ─────────────────────────────────────────────────────────────
describe('E2E / difyctl delete member', () => {
it('[P0] owner/admin can remove a member from the workspace', async () => {
const result = await fx.r(['delete', 'member', deleteTargetId, '--yes'])
assertExitCode(result, 0)
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const ids = data.data.map(m => m.id)
expect(ids).not.toContain(deleteTargetId)
deleteTargetId = ''
})
it('[P0] attempting to delete self returns server error', async () => {
const list = await fx.r(['get', 'member', '-o', 'json'])
const data = assertJson<MemberListJson>(list)
const self = data.data.find(m => m.email === E.email)
if (!self) {
console.warn('[E2E] could not identify self in member list — skipping')
return
}
const result = await fx.r(['delete', 'member', self.id, '--yes'])
assertNonZeroExit(result)
expect(result.stderr).toMatch(/self|yourself|cannot|not.*allow|400|forbidden/i)
})
it('[P0] missing member-id argument returns usage error', async () => {
const result = await fx.r(['delete', 'member'])
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toMatch(/missing|required|arg|member/i)
})
it('[P0] unauthenticated delete member returns auth error (exit 4)', async () => {
const tmp = await withTempConfig()
try {
const result = await run(
['delete', 'member', '00000000-0000-0000-0000-000000000000', '--yes'],
{ configDir: tmp.configDir },
)
assertExitCode(result, 4)
}
finally {
await tmp.cleanup()
}
})
it('[P1] non-existent member-id returns server error', async () => {
const result = await fx.r([
'delete',
'member',
'00000000-0000-0000-0000-000000000000',
'--yes',
])
assertNonZeroExit(result)
expect(result.stderr.trim().length).toBeGreaterThan(0)
})
it('[P1] -o json outputs structured envelope on error', async () => {
const result = await fx.r([
'delete',
'member',
'00000000-0000-0000-0000-000000000000',
'--yes',
'-o',
'json',
])
assertNonZeroExit(result)
assertErrorEnvelope(result)
})
})
+2 -22
View File
@@ -8,7 +8,7 @@
*
* Staging app prerequisites (specified via DIFY_E2E_* env vars):
* echo-chat — mode=chat, query variable, outputs "echo: {query}"
* echo-workflow — mode=workflow, x variable (required), outputs x directly (no echo prefix)
* echo-workflow — mode=workflow, x variable (required), outputs "echo: {x}"
*/
import type { AuthFixture } from '../../helpers/cli.js'
@@ -263,7 +263,7 @@ describe('E2E / difyctl run app', () => {
JSON.stringify({ x: 'hello', num: 42, enum_var: 'A', paragraph: 'short text' }),
])
assertExitCode(happyResult, 0)
assertStdoutContains(happyResult, 'hello') // workflow outputs x directly; echo: prefix removed (no sandbox on server)
assertStdoutContains(happyResult, 'echo:hello')
// ── 4.1.17: number field receives a string value ─────────────────────
const typedResult = await fx.r([
@@ -289,26 +289,6 @@ describe('E2E / difyctl run app', () => {
})
})
it('[P1] validation failure returns http_status 422 in JSON error envelope', async () => {
// After the @accepts/@returns server contract unification, input schema
// validation failures consistently return HTTP 422 (not 400 or 500).
// This verifies the CLI propagates the unified status code.
const result = await fx.r([
'run',
'app',
E.workflowAppId,
'--inputs',
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
'-o',
'json',
])
expect(result.exitCode).not.toBe(0)
const envelope = JSON.parse(result.stderr.trim()) as {
error: { code: string, message: string, http_status?: number }
}
expect(envelope.error.http_status, 'validation failure must return http_status 422').toBe(422)
})
// =========================================================================
// Error scenarios
// =========================================================================
@@ -118,7 +118,7 @@ describe('E2E / difyctl run app --conversation', () => {
'invalid-conv-id-xyz-not-exist',
])
assertExitCode(result, 1)
expect(result.stderr).toMatch(/not.?found|conversation|404|422|validation/i)
expect(result.stderr).toMatch(/not.?found|conversation|404/i)
})
// ── Combined flags ──────────────────────────────────────────────────────
@@ -277,7 +277,7 @@ describe('E2E / difyctl run app --stream (specialisation)', () => {
'--stream',
])
expect(result.exitCode, 'wrong-type input should cause non-zero exit').not.toBe(0)
expect(result.stderr).toMatch(/validation|invalid|type|422|must be/i)
expect(result.stderr).toMatch(/validation|invalid|type|400|server_5xx|must be/i)
})
// ── Non-existent app with positional query (4.2.16) ────────────────────
-2
View File
@@ -92,8 +92,6 @@ export default defineConfig({
'test/e2e/suites/framework/**/*.e2e.ts',
// discovery (get app / describe app)
'test/e2e/suites/discovery/**/*.e2e.ts',
// member management (get/create/delete/set member)
'test/e2e/suites/member/**/*.e2e.ts',
// dsl (export / import)
'test/e2e/suites/dsl/**/*.e2e.ts',
// run tests (require valid token)
@@ -18,13 +18,11 @@ export type AgentAppCreatePayload = {
icon_background?: string | null
icon_type?: IconType | null
name: string
role?: string
}
export type AppDetailWithSite = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -41,7 +39,6 @@ export type AppDetailWithSite = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: Site | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -59,14 +56,13 @@ export type AgentInviteOptionsResponse = {
total: number
}
export type AgentAppUpdatePayload = {
export type UpdateAppPayload = {
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: IconType | null
max_active_requests?: number | null
name: string
role?: string | null
use_icon_as_answer_icon?: boolean | null
}
@@ -259,7 +255,6 @@ export type AgentConfigSnapshotDetailResponse = {
export type AppPartial = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -277,7 +272,6 @@ export type AppPartial = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1225,7 +1219,6 @@ export type AppPaginationWritable = {
export type AppDetailWithSiteWritable = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -1241,7 +1234,6 @@ export type AppDetailWithSiteWritable = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: SiteWritable | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -1253,7 +1245,6 @@ export type AppDetailWithSiteWritable = {
export type AppPartialWritable = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -1270,7 +1261,6 @@ export type AppPartialWritable = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1397,7 +1387,7 @@ export type GetAgentByAgentIdResponses = {
export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses]
export type PutAgentByAgentIdData = {
body: AgentAppUpdatePayload
body: UpdateAppPayload
path: {
agent_id: string
}
@@ -92,20 +92,18 @@ export const zAgentAppCreatePayload = z.object({
icon_background: z.string().nullish(),
icon_type: zIconType.nullish(),
name: z.string().min(1),
role: z.string().max(255).optional().default(''),
})
/**
* AgentAppUpdatePayload
* UpdateAppPayload
*/
export const zAgentAppUpdatePayload = z.object({
export const zUpdateAppPayload = z.object({
description: z.string().max(400).nullish(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: zIconType.nullish(),
max_active_requests: z.int().nullish(),
name: z.string().min(1),
role: z.string().max(255).nullish(),
use_icon_as_answer_icon: z.boolean().nullish(),
})
@@ -475,7 +473,6 @@ export const zModelConfigPartial = z.object({
*/
export const zAppPartial = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -493,7 +490,6 @@ export const zAppPartial = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -535,7 +531,6 @@ export const zModelConfig = z.object({
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -552,7 +547,6 @@ export const zAppDetailWithSite = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSite.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -1735,7 +1729,6 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({
*/
export const zAppPartialWritable = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -1752,7 +1745,6 @@ export const zAppPartialWritable = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -1796,7 +1788,6 @@ export const zSiteWritable = z.object({
export const zAppDetailWithSiteWritable = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -1812,7 +1803,6 @@ export const zAppDetailWithSiteWritable = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSiteWritable.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -1890,7 +1880,7 @@ export const zGetAgentByAgentIdPath = z.object({
*/
export const zGetAgentByAgentIdResponse = zAppDetailWithSite
export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload
export const zPutAgentByAgentIdBody = zUpdateAppPayload
export const zPutAgentByAgentIdPath = z.object({
agent_id: z.string(),
@@ -24,7 +24,6 @@ export type CreateAppPayload = {
export type AppDetailWithSite = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -41,7 +40,6 @@ export type AppDetailWithSite = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: Site | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -1155,7 +1153,6 @@ export type ApiKeyItem = {
export type AppPartial = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -1173,7 +1170,6 @@ export type AppPartial = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -2544,7 +2540,6 @@ export type AppPaginationWritable = {
export type AppDetailWithSiteWritable = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
@@ -2560,7 +2555,6 @@ export type AppDetailWithSiteWritable = {
mode: string
model_config?: ModelConfig | null
name: string
role?: string | null
site?: SiteWritable | null
tags?: Array<Tag>
tracing?: JsonValue | null
@@ -2595,7 +2589,6 @@ export type WorkflowCommentDetailWritable = {
export type AppPartialWritable = {
access_mode?: string | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
create_user_name?: string | null
@@ -2612,7 +2605,6 @@ export type AppPartialWritable = {
mode: string
model_config?: ModelConfigPartial | null
name: string
role?: string | null
tags?: Array<Tag>
updated_at?: number | null
updated_by?: string | null
@@ -1946,7 +1946,6 @@ export const zModelConfigPartial = z.object({
*/
export const zAppPartial = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -1964,7 +1963,6 @@ export const zAppPartial = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -2006,7 +2004,6 @@ export const zModelConfig = z.object({
export const zAppDetailWithSite = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -2023,7 +2020,6 @@ export const zAppDetailWithSite = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSite.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -3437,7 +3433,6 @@ export const zGeneratedAppResponseWritable = zJsonValue
*/
export const zAppPartialWritable = z.object({
access_mode: z.string().nullish(),
app_id: z.string().nullish(),
author_name: z.string().nullish(),
bound_agent_id: z.string().nullish(),
create_user_name: z.string().nullish(),
@@ -3454,7 +3449,6 @@ export const zAppPartialWritable = z.object({
mode: z.string(),
model_config: zModelConfigPartial.nullish(),
name: z.string(),
role: z.string().nullish(),
tags: z.array(zTag).optional(),
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
@@ -3498,7 +3492,6 @@ export const zSiteWritable = z.object({
export const zAppDetailWithSiteWritable = z.object({
access_mode: z.string().nullish(),
api_base_url: z.string().nullish(),
app_id: z.string().nullish(),
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
@@ -3514,7 +3507,6 @@ export const zAppDetailWithSiteWritable = z.object({
mode: z.string(),
model_config: zModelConfig.nullish(),
name: z.string(),
role: z.string().nullish(),
site: zSiteWritable.nullish(),
tags: z.array(zTag).optional(),
tracing: zJsonValue.nullish(),
@@ -0,0 +1,32 @@
// This file is auto-generated by @hey-api/openapi-ts
import { oc } from '@orpc/contract'
import { zGetClientMetadataResponse } from './zod.gen'
/**
* Return client metadata inferred from edge request headers
*
* NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity
* metadata for the current request and performs no tenant, account, or setup checks.
*/
export const get = oc
.route({
description:
'NOTE: This endpoint is unauthenticated by design. It exposes only low-sensitivity\nmetadata for the current request and performs no tenant, account, or setup checks.',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getClientMetadata',
path: '/client-metadata',
summary: 'Return client metadata inferred from edge request headers',
tags: ['console'],
})
.output(zGetClientMetadataResponse)
export const clientMetadata = {
get,
}
export const contract = {
clientMetadata,
}
@@ -0,0 +1,26 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: `${string}://${string}/console/api` | (string & {})
}
export type ClientMetadataResponse = {
geo: ClientGeoMetadataResponse
}
export type ClientGeoMetadataResponse = {
country_code?: string | null
}
export type GetClientMetadataData = {
body?: never
path?: never
query?: never
url: '/client-metadata'
}
export type GetClientMetadataResponses = {
200: ClientMetadataResponse
}
export type GetClientMetadataResponse = GetClientMetadataResponses[keyof GetClientMetadataResponses]
@@ -0,0 +1,22 @@
// This file is auto-generated by @hey-api/openapi-ts
import * as z from 'zod'
/**
* ClientGeoMetadataResponse
*/
export const zClientGeoMetadataResponse = z.object({
country_code: z.string().nullish(),
})
/**
* ClientMetadataResponse
*/
export const zClientMetadataResponse = z.object({
geo: zClientGeoMetadataResponse,
})
/**
* Success
*/
export const zGetClientMetadataResponse = zClientMetadataResponse
@@ -11,6 +11,7 @@ import { app } from './app/orpc.gen'
import { apps } from './apps/orpc.gen'
import { auth } from './auth/orpc.gen'
import { billing } from './billing/orpc.gen'
import { clientMetadata } from './client-metadata/orpc.gen'
import { codeBasedExtension } from './code-based-extension/orpc.gen'
import { compliance } from './compliance/orpc.gen'
import { dataSource } from './data-source/orpc.gen'
@@ -62,6 +63,7 @@ export const contract = {
apps,
auth,
billing,
clientMetadata,
codeBasedExtension,
compliance,
dataSource,
+88 -171
View File
@@ -607,8 +607,6 @@ overrides:
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
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
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
@@ -619,9 +617,6 @@ overrides:
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:
@@ -1188,7 +1183,7 @@ importers:
version: 11.1.8
jotai:
specifier: 'catalog:'
version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
version: 2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7)
js-audio-recorder:
specifier: 'catalog:'
version: 1.0.7
@@ -1233,13 +1228,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.0)(@playwright/test@1.60.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.0)(@playwright/test@1.60.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
@@ -1399,7 +1394,7 @@ importers:
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)))
'@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.4(@babel/core@7.29.0)(@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.0)(@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/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)
@@ -1537,7 +1532,7 @@ 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.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.0)(@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)
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)'
@@ -1708,81 +1703,56 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
'@babel/compat-data@7.29.0':
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.29.7':
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.7':
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
'@babel/core@7.29.0':
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
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'}
'@babel/helper-compilation-targets@7.29.7':
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
'@babel/helper-compilation-targets@7.28.6':
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
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==}
'@babel/helper-module-imports@7.28.6':
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.29.7':
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.29.7':
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
'@babel/helper-module-transforms@7.28.6':
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.29.1
'@babel/core': ^7.0.0
'@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'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.29.7':
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.29.7':
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
'@babel/helpers@7.29.2':
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
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'}
hasBin: true
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
@@ -1791,26 +1761,14 @@ packages:
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'}
'@base-ui/react@1.5.0':
resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==}
engines: {node: '>=14.0.0'}
@@ -5757,7 +5715,6 @@ packages:
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
cssfontparser@1.2.1:
resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==}
@@ -7202,7 +7159,7 @@ packages:
resolution: {integrity: sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': ^7.29.1
'@babel/core': '>=7.0.0'
'@babel/template': '>=7.0.0'
'@types/react': '>=17.0.0'
react: '>=17.0.0'
@@ -7234,6 +7191,9 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
@@ -8283,12 +8243,12 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@6.1.4:
resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss-selector-parser@7.1.4:
resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
@@ -9024,8 +8984,8 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tar@7.5.16:
resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==}
tar@7.5.11:
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
engines: {node: '>=18'}
tiny-case@1.0.3:
@@ -9995,25 +9955,19 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.0': {}
'@babel/compat-data@7.29.7': {}
'@babel/core@7.29.7':
'@babel/core@7.29.0':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/helpers': 7.29.2
'@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3(supports-color@10.2.2)
@@ -10031,65 +9985,47 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/generator@7.29.7':
'@babel/helper-compilation-targets@7.28.6':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.29.7':
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
'@babel/compat-data': 7.29.0
'@babel/helper-validator-option': 7.27.1
browserslist: 4.28.1
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':
'@babel/helper-module-imports@7.28.6':
dependencies:
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
'@babel/core': 7.29.0
'@babel/helper-module-imports': 7.28.6
'@babel/helper-validator-identifier': 7.28.5
'@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helper-validator-option@7.29.7': {}
'@babel/helpers@7.29.7':
'@babel/helpers@7.29.2':
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/template': 7.28.6
'@babel/types': 7.29.0
'@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':
@@ -10098,12 +10034,6 @@ snapshots:
'@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
@@ -10116,28 +10046,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3(supports-color@10.2.2)
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
'@babel/helper-validator-identifier': 7.29.7
'@base-ui/react@1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@babel/runtime': 7.29.2
@@ -10848,7 +10761,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.2.0
js-yaml: 4.1.1
'@hey-api/openapi-ts@0.98.2(magicast@0.5.2)(typescript@6.0.3)':
dependencies:
@@ -10918,7 +10831,7 @@ snapshots:
local-pkg: 1.1.2
pathe: 2.0.3
svgo: 3.3.3
tar: 7.5.16
tar: 7.5.11
transitivePeerDependencies:
- supports-color
@@ -12435,18 +12348,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.4(@babel/core@7.29.0)(@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.0)(@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)':
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)
next: 16.2.9(@babel/core@7.29.0)(@playwright/test@1.60.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))
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.0)(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-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.0)(@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)
optionalDependencies:
'@types/react': 19.2.17
'@types/react-dom': 19.2.3(@types/react@19.2.17)
@@ -12628,7 +12541,7 @@ snapshots:
'@tailwindcss/typography@0.5.20(tailwindcss@4.3.1)':
dependencies:
postcss-selector-parser: 6.1.4
postcss-selector-parser: 6.0.10
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))':
@@ -13186,13 +13099,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.0)(@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)':
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.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@upsetjs/venn.js@2.0.0':
optionalDependencies:
@@ -15293,7 +15206,7 @@ snapshots:
eslint: 10.5.0(jiti@2.7.0)(supports-color@10.2.2)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.4
postcss-selector-parser: 7.1.1
semver: 7.8.4
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
xml-name-validator: 4.0.0
@@ -15307,7 +15220,7 @@ snapshots:
eslint: 10.5.0(jiti@2.7.0)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.4
postcss-selector-parser: 7.1.1
semver: 7.8.4
vue-eslint-parser: 10.4.0(eslint@10.5.0(jiti@2.7.0))
xml-name-validator: 4.0.0
@@ -16182,10 +16095,10 @@ snapshots:
jiti@2.7.0: {}
jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7):
jotai@2.20.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.17)(react@19.2.7):
optionalDependencies:
'@babel/core': 7.29.7
'@babel/template': 7.29.7
'@babel/core': 7.29.0
'@babel/template': 7.28.6
'@types/react': 19.2.17
react: 19.2.7
@@ -16201,6 +16114,10 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
js-yaml@4.2.0:
dependencies:
argparse: 2.0.1
@@ -17058,7 +16975,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.0)(@playwright/test@1.60.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
@@ -17067,7 +16984,7 @@ snapshots:
postcss: 8.5.15
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.9
'@next/swc-darwin-x64': 16.2.9
@@ -17113,12 +17030,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.0)(@playwright/test@1.60.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.0)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
object-assign@4.1.1: {}
@@ -17570,12 +17487,12 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.1.4:
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-selector-parser@7.1.4:
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
@@ -17669,7 +17586,7 @@ snapshots:
react-docgen@8.0.3:
dependencies:
'@babel/core': 7.29.7
'@babel/core': 7.29.0
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@types/babel__core': 7.20.5
@@ -18470,12 +18387,12 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7):
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7):
dependencies:
client-only: 0.0.1
react: 19.2.7
optionalDependencies:
'@babel/core': 7.29.7
'@babel/core': 7.29.0
stylis@4.3.6: {}
@@ -18530,7 +18447,7 @@ snapshots:
readable-stream: 3.6.2
optional: true
tar@7.5.16:
tar@7.5.11:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -18865,9 +18782,9 @@ 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.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.0)(@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):
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.0)(@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)
'@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))
image-size: 2.0.2
@@ -18920,13 +18837,13 @@ 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.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.0)(@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):
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)
next: 16.2.9(@babel/core@7.29.0)(@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))
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)'
-5
View File
@@ -27,24 +27,19 @@ packages:
- packages/*
- cli
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-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
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
ws@>=8.0.0 <8.20.1: ^8.21.0