From dad68815be746d690f89c09fa87200feeb3188b9 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:07:55 +0000
Subject: [PATCH 01/23] fix(packaging): ship UI/CLI deps unconditionally and
make component registration durable
Two production-breaking defects that a development checkout hides.
Packaging: fastapi, uvicorn, websockets, python-multipart and rich were
declared as core dependencies but were also named in [tool.poetry.extras].
Poetry treats any dependency named in an extra as extra-only, so the published
wheel demoted all five to `extra == "ui"` / `extra == "rich"`. A plain
`pip install flowyml` therefore produced an install whose entire web UI died
with `ModuleNotFoundError: No module named 'fastapi'`. The extras now carry
only genuinely optional packages; `ui` and `rich` are kept as empty extras so
existing `pip install flowyml[ui]` invocations keep working.
The mirror-image defect: azure-ai-ml, azure-identity and azure-storage-blob
were referenced by the `azure` extra but never declared as dependencies, so
Poetry dropped them and `pip install flowyml[azure]` installed nothing while
the Azure blob store and Azure ML registry import them at runtime. They are
now declared optional. mlflow, wandb and keras were declared optional but
belonged to no extra at all, making them unreachable by any install command;
they now have extras, and `flowyml[all]` is a true superset of every extra.
Registry: components registered via the @register_component decorator were
lost whenever the global ComponentRegistry was rebuilt. The decorator fires
once per process on first import, and Python's module cache makes any later
re-import a no-op, so lazy loaders such as _ensure_providers_loaded could not
restore them and get_component() returned None for the rest of the process.
Registrations are now recorded and replayed into every newly constructed
registry, which fixes azure_blob/gcs/s3 and friends disappearing after a
reset. Adds reset_component_registry() as the supported way to rebuild.
Adds tests/test_packaging_metadata.py, which asserts these invariants against
both pyproject.toml and the built distribution metadata, and makes
test_load_plugins_invalid_config assert its actual intent (an invalid config
registers nothing new) instead of an absolute count that only held because of
the registry bug.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/stacks/plugins.py | 48 ++++++++++
pyproject.toml | 34 +++++--
tests/test_config_loading.py | 21 ++++-
tests/test_packaging_metadata.py | 155 +++++++++++++++++++++++++++++++
4 files changed, 245 insertions(+), 13 deletions(-)
create mode 100644 tests/test_packaging_metadata.py
diff --git a/flowyml/stacks/plugins.py b/flowyml/stacks/plugins.py
index 820bd4a8..a57ed320 100644
--- a/flowyml/stacks/plugins.py
+++ b/flowyml/stacks/plugins.py
@@ -16,6 +16,8 @@
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
+from loguru import logger
+
from flowyml.stacks.components import (
StackComponent,
Orchestrator,
@@ -586,6 +588,30 @@ def _class_to_snake_case(name: str) -> str:
# Global registry instance
_global_component_registry: ComponentRegistry | None = None
+# Durable record of every component registered through ``register_component``.
+#
+# ``@register_component`` fires exactly once per process, when the defining
+# module is first imported. Python caches modules in ``sys.modules``, so a
+# later ``import`` of that module is a no-op and the decorator never runs
+# again. That makes registration by import side effect fragile: if the global
+# registry is ever rebuilt (tests reset it, an embedding host re-initializes
+# the process, a plugin reload), every component whose module was already
+# imported would silently disappear and ``get_component(...)`` would start
+# returning ``None`` for the rest of the process.
+#
+# Recording each ``(class, name)`` pair here and replaying it into every newly
+# constructed registry makes registration durable and idempotent, without
+# depending on import ordering. Registrations are keyed by the resolved name
+# so a later re-registration of the same name wins, matching the behaviour of
+# calling ``ComponentRegistry.register`` directly.
+_recorded_components: dict[str, type[StackComponent]] = {}
+
+
+def _record_component(component_class: type[StackComponent], name: str | None) -> None:
+ """Remember a decorator-registered component so registry rebuilds keep it."""
+ resolved = name or ComponentRegistry._class_to_snake_case(component_class.__name__)
+ _recorded_components[resolved] = component_class
+
def get_component_registry() -> ComponentRegistry:
"""Get the global component registry."""
@@ -596,17 +622,39 @@ def get_component_registry() -> ComponentRegistry:
# to this same instance instead of recursively constructing another.
_global_component_registry = ComponentRegistry()
_global_component_registry._register_builtin_components()
+ # Replay decorator registrations from modules already imported in this
+ # process; their decorators will not fire again (see ``_recorded_components``).
+ for recorded_name, recorded_cls in _recorded_components.items():
+ try:
+ _global_component_registry.register(recorded_cls, recorded_name)
+ except Exception: # pragma: no cover - a bad plugin must not break startup
+ logger.warning(
+ f"Failed to restore recorded component '{recorded_name}'",
+ )
return _global_component_registry
+def reset_component_registry() -> None:
+ """Drop the global registry so the next access rebuilds it.
+
+ The rebuilt registry still contains every built-in flavor and every
+ component previously registered via ``register_component``, because those
+ are replayed from ``_recorded_components``.
+ """
+ global _global_component_registry
+ _global_component_registry = None
+
+
def register_component(component_class=None, name: str | None = None):
"""Decorator to register a custom component."""
def wrapper(cls):
+ _record_component(cls, name)
get_component_registry().register(cls, name)
return cls
if component_class is not None:
+ _record_component(component_class, name)
get_component_registry().register(component_class, name)
return component_class
diff --git a/pyproject.toml b/pyproject.toml
index 3db8c90c..ac73a543 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,6 +59,9 @@ boto3 = {version = ">=1.28", optional = true}
sagemaker = {version = ">=2.100", optional = true}
mlflow = {version = ">=2.0", optional = true}
wandb = {version = ">=0.15", optional = true}
+azure-ai-ml = {version = ">=1.12.0", optional = true}
+azure-identity = {version = ">=1.15.0", optional = true}
+azure-storage-blob = {version = ">=12.19.0", optional = true}
langchain-core = {version = ">=0.2.0", optional = true}
langgraph = {version = ">=0.2.0", optional = true}
openai = {version = ">=1.0.0", optional = true}
@@ -105,16 +108,26 @@ griffe = ">=1.5.0"
mkdocs-include-markdown-plugin = ">=7.2.0"
[tool.poetry.extras]
+# IMPORTANT: only list dependencies declared with `optional = true` here.
+# Poetry marks any dependency named in an extra as extra-only, so listing a
+# *core* dependency (fastapi, uvicorn, websockets, python-multipart, rich)
+# would strip it from a plain `pip install flowyml` and break the web UI and
+# CLI for every user. `ui` and `rich` are therefore intentionally empty: the
+# packages they used to carry are unconditional dependencies now, and the
+# empty extras keep `pip install flowyml[ui]` working for existing scripts.
pytorch = ["torch"]
tensorflow = ["tensorflow"]
+keras = ["keras"]
sklearn = ["scikit-learn"]
gcp = ["google-cloud-aiplatform", "google-cloud-storage", "kfp", "kfp-server-api"]
-aws = ["boto3"]
+aws = ["boto3", "sagemaker"]
azure = ["azure-ai-ml", "azure-identity", "azure-storage-blob"]
-# UI extra kept for backward compatibility (dependencies are now in main)
-ui = ["fastapi", "uvicorn", "websockets", "python-multipart"]
-# Rich display for beautiful CLI output
-rich = ["rich"]
+mlflow = ["mlflow"]
+wandb = ["wandb"]
+# UI extra kept for backward compatibility (dependencies are installed by default)
+ui = []
+# Rich display for beautiful CLI output (installed by default)
+rich = []
# GenAI integration extras
langchain = ["langchain-core"]
langgraph = ["langgraph", "langchain-core"]
@@ -123,18 +136,19 @@ genai = ["langgraph", "langchain-core", "openai"]
all = [
"torch",
"tensorflow",
+ "keras",
"scikit-learn",
"google-cloud-aiplatform",
"google-cloud-storage",
+ "kfp",
+ "kfp-server-api",
"boto3",
+ "sagemaker",
"azure-ai-ml",
"azure-identity",
"azure-storage-blob",
- # UI dependencies are included by default, but listed here for clarity
- "fastapi",
- "uvicorn",
- "websockets",
- "python-multipart",
+ "mlflow",
+ "wandb",
# GenAI
"langgraph",
"langchain-core",
diff --git a/tests/test_config_loading.py b/tests/test_config_loading.py
index bd376381..d48cbe9c 100644
--- a/tests/test_config_loading.py
+++ b/tests/test_config_loading.py
@@ -22,8 +22,14 @@ def run(self, pipeline, **kwargs):
def registry():
import flowyml.stacks.plugins
+ previous = flowyml.stacks.plugins._global_component_registry
flowyml.stacks.plugins._global_component_registry = None
- return get_component_registry()
+ try:
+ yield get_component_registry()
+ finally:
+ # Restore the process-wide registry so this test cannot leak a rebuilt
+ # instance into unrelated tests running in the same worker.
+ flowyml.stacks.plugins._global_component_registry = previous
def test_load_plugins_from_yaml(registry):
@@ -61,9 +67,18 @@ def test_load_plugins_from_yaml(registry):
def test_load_plugins_invalid_config(registry):
- """Test loading from invalid config."""
+ """An unusable config must register nothing and must not raise.
+
+ Asserted as a *delta* rather than an absolute count: a freshly built
+ registry legitimately carries built-in flavors and any cloud components
+ already registered in this process, and that baseline varies with import
+ order across test workers.
+ """
+ before = set(registry.list_orchestrators())
+
with patch("builtins.open", mock_open(read_data="invalid: yaml")):
with patch("pathlib.Path.exists", return_value=True):
# Should not raise exception, just log error
registry.load_plugins_from_config("bad.yaml")
- assert len(registry.list_orchestrators()) == 0
+
+ assert set(registry.list_orchestrators()) == before
diff --git a/tests/test_packaging_metadata.py b/tests/test_packaging_metadata.py
new file mode 100644
index 00000000..21fc0e64
--- /dev/null
+++ b/tests/test_packaging_metadata.py
@@ -0,0 +1,155 @@
+"""Regression tests for the distribution metadata in ``pyproject.toml``.
+
+These guard a class of packaging bug that is invisible in a development
+checkout but breaks every real installation: Poetry treats *any* dependency
+named in ``[tool.poetry.extras]`` as extra-only, so listing a core dependency
+there silently strips it from ``pip install flowyml``. That shipped a package
+whose entire web UI raised ``ModuleNotFoundError: No module named 'fastapi'``.
+
+The mirror-image bug is an extra that names a package which was never declared
+in ``[tool.poetry.dependencies]``: Poetry drops it without warning, so
+``pip install flowyml[azure]`` installed nothing at all.
+"""
+
+from __future__ import annotations
+
+import tomllib
+from pathlib import Path
+
+import pytest
+
+PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
+
+# Dependencies that must always be installed by a bare ``pip install flowyml``.
+# fastapi/uvicorn/websockets/python-multipart back the web UI; rich backs the CLI.
+REQUIRED_CORE_PACKAGES = {
+ "fastapi",
+ "uvicorn",
+ "websockets",
+ "python-multipart",
+ "rich",
+ "click",
+ "pydantic",
+ "sqlalchemy",
+ "pyyaml",
+ "loguru",
+}
+
+
+def _normalize(name: str) -> str:
+ """PEP 503 style normalization so ``python_multipart`` == ``python-multipart``."""
+ return name.lower().replace("_", "-")
+
+
+@pytest.fixture(scope="module")
+def pyproject() -> dict:
+ with PYPROJECT.open("rb") as fh:
+ return tomllib.load(fh)
+
+
+@pytest.fixture(scope="module")
+def declared_dependencies(pyproject: dict) -> dict[str, object]:
+ return pyproject["tool"]["poetry"]["dependencies"]
+
+
+@pytest.fixture(scope="module")
+def extras(pyproject: dict) -> dict[str, list[str]]:
+ return pyproject["tool"]["poetry"].get("extras", {})
+
+
+def _is_optional(spec: object) -> bool:
+ return isinstance(spec, dict) and bool(spec.get("optional", False))
+
+
+def test_core_dependencies_are_not_listed_in_any_extra(declared_dependencies, extras):
+ """A non-optional dependency named in an extra becomes extra-only."""
+ optional_names = {_normalize(n) for n, s in declared_dependencies.items() if _is_optional(s)}
+ core_names = {
+ _normalize(n)
+ for n, s in declared_dependencies.items()
+ if n != "python" and not _is_optional(s)
+ }
+
+ offenders: dict[str, list[str]] = {}
+ for extra_name, packages in extras.items():
+ bad = [p for p in packages if _normalize(p) in core_names]
+ if bad:
+ offenders[extra_name] = bad
+
+ assert not offenders, (
+ "These extras name non-optional dependencies, which makes Poetry demote them "
+ f"to extra-only and strips them from `pip install flowyml`: {offenders}. "
+ f"(Optional dependencies available for extras: {sorted(optional_names)})"
+ )
+
+
+def test_every_package_named_in_an_extra_is_declared(declared_dependencies, extras):
+ """Poetry silently drops extras entries that are not declared dependencies."""
+ declared = {_normalize(n) for n in declared_dependencies}
+
+ undeclared: dict[str, list[str]] = {}
+ for extra_name, packages in extras.items():
+ missing = [p for p in packages if _normalize(p) not in declared]
+ if missing:
+ undeclared[extra_name] = missing
+
+ assert not undeclared, (
+ "These extras reference packages that are absent from "
+ f"[tool.poetry.dependencies], so the extra installs nothing: {undeclared}"
+ )
+
+
+def test_every_optional_dependency_is_reachable_through_an_extra(declared_dependencies, extras):
+ """An optional dependency in no extra can never be installed by anyone."""
+ optional_names = {_normalize(n) for n, s in declared_dependencies.items() if _is_optional(s)}
+ reachable = {_normalize(p) for packages in extras.values() for p in packages}
+
+ orphans = sorted(optional_names - reachable)
+ assert not orphans, (
+ f"These dependencies are declared `optional = true` but appear in no extra, "
+ f"so no `pip install flowyml[...]` invocation can ever install them: {orphans}"
+ )
+
+
+def test_all_extra_is_a_superset_of_the_cloud_and_framework_extras(extras):
+ """``flowyml[all]`` must not be missing pieces the narrower extras provide."""
+ all_packages = {_normalize(p) for p in extras.get("all", [])}
+
+ # ``all`` is deliberately a superset of every extra that pulls real
+ # third-party packages. Empty back-compat extras (ui, rich) have nothing
+ # to contribute and are skipped by the emptiness check itself.
+ missing: dict[str, list[str]] = {}
+ for extra_name, packages in extras.items():
+ if extra_name == "all" or not packages:
+ continue
+ gap = sorted({_normalize(p) for p in packages} - all_packages)
+ if gap:
+ missing[extra_name] = gap
+
+ assert not missing, f"`flowyml[all]` is missing packages provided by narrower extras: {missing}"
+
+
+def test_installed_distribution_exposes_core_dependencies_unconditionally():
+ """Check the *built* metadata, which is what pip actually consumes."""
+ from importlib.metadata import PackageNotFoundError, requires
+
+ try:
+ requirements = requires("flowyml")
+ except PackageNotFoundError: # pragma: no cover - only when not installed
+ pytest.skip("flowyml is not installed in this environment")
+
+ assert requirements is not None
+
+ # A requirement with no environment marker is installed unconditionally.
+ unconditional = {
+ _normalize(r.split(";")[0].strip().split()[0].split("[")[0])
+ for r in requirements
+ if ";" not in r
+ }
+
+ missing = sorted(REQUIRED_CORE_PACKAGES - unconditional)
+ assert not missing, (
+ f"The built distribution does not install {missing} unconditionally. "
+ "`pip install flowyml` would produce a broken install. "
+ "Re-run `pip install -e .` if pyproject.toml changed since the last build."
+ )
From 43634b3b57e0847325c5921d1f1f9a3439ae0066 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:21:51 +0000
Subject: [PATCH 02/23] fix(ui): make the backend fail closed and implement the
endpoints the UI calls
Security. The API was open by default and stayed open when misconfigured:
- AuthMiddleware returned early whenever FLOWYML_ENV was not "production" *or*
FLOWYML_API_TOKEN was unset. Both docker-compose.yml and .env.example ship
FLOWYML_API_TOKEN with an empty value, so following the project's own
deployment guide produced a production instance with no authentication at
all. POST /api/execution/execute importlib-imports and runs arbitrary
modules, so that was unauthenticated remote code execution. Startup now
aborts with an actionable message, and the middleware denies every request
as a second line of defence for hosts that skip lifespan events.
FLOWYML_ALLOW_INSECURE=1 is the deliberate opt-out for operators who
terminate auth at a proxy.
- The login form fell back to the publicly documented password "flowyml" when
FLOWYML_ADMIN_PASSWORD was unset, and issued the fixed string
"dev-token-placeholder" as a session token when FLOWYML_API_TOKEN was unset.
Either one made a production instance trivially accessible. Both are now
refused in production; development keeps its zero-config defaults.
- CORS listed "*" alongside allow_credentials=True. Starlette resolves that
pairing by reflecting the caller's Origin, letting any website issue
credentialed requests to a developer's local instance and read the replies.
Origins are now explicit and configurable via FLOWYML_CORS_ORIGINS.
- Token and password comparisons used ==, which short-circuits on the first
differing byte; they now use secrets.compare_digest. Tokens are no longer
accepted from the query string, where they leak into access logs, browser
history and Referer headers.
TokenManager. Writes were non-atomic, so a crash or a concurrent request
during the write window left a truncated token store that locked every client
out; writes now go through a temp file plus os.replace, with 0600 on the file
and 0700 on its directory. verify_token rewrote the entire store on every
authenticated request to update last_used, making each API call wait on an
fsync and racing concurrent handlers; it is now throttled and mutex-guarded.
create_token validates permissions instead of accepting typos.
Missing endpoints. Two UI features called routes that were never implemented,
and both failed silently:
- The API Tokens page's "Revoke" button issued DELETE /api/execution/tokens/
{name} against a non-existent route and never checked the response, so it
reported success while the token stayed valid. The endpoint now exists and
revokes by a stable opaque id (names are labels and need not be unique), and
the button surfaces failures instead of swallowing them.
- The Settings page's system-information card called /api/execution/info,
which did not exist, so it always rendered its hard-coded fallbacks: version
"0.1.0" (actual: 2.2.0) and database "PostgreSQL" (actual: SQLite). The
endpoint now reports the live version, backend, environment and uptime.
- GET /api/ai/context was hardcoded to /context/run/{run_id} while the UI
builds the path from a generic pageType, so any other page got an
unexplained routing 404; it now shares the POST endpoint's explanatory 400.
Also: the OpenTelemetry console exporter was enabled by default, printing a
multi-line JSON document per span - four or more per HTTP request - which
drowned real log output; it is now opt-in via FLOWYML_OTEL_CONSOLE. Version
strings in the FastAPI app and /api/health no longer report a stale "0.1.0".
Migrated the deprecated on_event startup hook to a lifespan handler and the
deprecated Pydantic class-based Config to ConfigDict.
Tests: adds test_backend_security.py (31 cases), test_execution_endpoints.py
(15 cases) and test_api_alignment.py, which extracts all 107 frontend API call
sites - handling nested template literals - and asserts each resolves to a
backend route that accepts the verb. Also isolates scheduler and leaderboard
tests onto per-test temp directories; they shared fixed filenames in the
working directory and deleted each other's databases under parallel runs.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/ui/backend/auth.py | 172 ++++++++--
flowyml/ui/backend/main.py | 152 +++++----
flowyml/ui/backend/routers/ai_context.py | 31 +-
flowyml/ui/backend/routers/auth.py | 119 ++++---
flowyml/ui/backend/routers/evaluations.py | 5 +-
flowyml/ui/backend/routers/execution.py | 96 +++++-
flowyml/ui/backend/security.py | 196 +++++++++++
flowyml/ui/frontend/src/app/tokens/page.jsx | 51 ++-
flowyml/ui/frontend/src/hooks/useAIContext.js | 4 +-
tests/test_api_alignment.py | 229 +++++++++++++
tests/test_backend_security.py | 315 ++++++++++++++++++
tests/test_execution_endpoints.py | 178 ++++++++++
tests/test_leaderboard.py | 22 +-
tests/test_scheduler_advanced.py | 32 +-
14 files changed, 1421 insertions(+), 181 deletions(-)
create mode 100644 flowyml/ui/backend/security.py
create mode 100644 tests/test_api_alignment.py
create mode 100644 tests/test_backend_security.py
create mode 100644 tests/test_execution_endpoints.py
diff --git a/flowyml/ui/backend/auth.py b/flowyml/ui/backend/auth.py
index ec7a0b25..84d1cde1 100644
--- a/flowyml/ui/backend/auth.py
+++ b/flowyml/ui/backend/auth.py
@@ -1,25 +1,63 @@
"""Authentication and authorization for flowyml API."""
+import contextlib
+import os
import secrets
import hashlib
import json
+import tempfile
+import threading
from pathlib import Path
from typing import Any
-from datetime import datetime
+from datetime import datetime, timedelta
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from loguru import logger
+
security = HTTPBearer(auto_error=False)
+#: Permissions the API recognises. Anything else is rejected at creation time
+#: so a typo such as "admln" cannot silently produce a token that passes no
+#: check but looks privileged in the UI.
+VALID_PERMISSIONS = frozenset({"read", "write", "execute", "admin"})
+
+#: How stale a token's ``last_used`` timestamp may become before it is
+#: rewritten. Persisting on every request turned each authenticated API call
+#: into a synchronous rewrite of the whole token file.
+LAST_USED_WRITE_INTERVAL = timedelta(minutes=5)
+
class TokenManager:
"""Manage API tokens for authentication."""
- def __init__(self, tokens_file: str = ".flowyml/api_tokens.json"):
+ def __init__(self, tokens_file: str | None = None):
+ if tokens_file is None:
+ tokens_file = os.getenv("FLOWYML_TOKENS_FILE", ".flowyml/api_tokens.json")
self.tokens_file = Path(tokens_file)
self.tokens_file.parent.mkdir(parents=True, exist_ok=True)
+ # Token hashes are verifiers: guard the file against other local users.
+ self._restrict_permissions(self.tokens_file.parent, 0o700)
+ # Guards read-modify-write cycles against concurrent request handlers.
+ self._lock = threading.Lock()
+ # Last time each token's ``last_used`` timestamp was flushed to disk.
+ self._last_persisted: dict[str, datetime] = {}
self._load_tokens()
+ @staticmethod
+ def _restrict_permissions(path: Path, mode: int) -> None:
+ """Best-effort tightening of filesystem permissions.
+
+ Silently ignored on platforms without POSIX permissions (Windows) and
+ on filesystems that reject chmod, since failing here would make the
+ server unusable for a defence-in-depth measure.
+ """
+ try:
+ if path.exists():
+ path.chmod(mode)
+ except (OSError, NotImplementedError): # pragma: no cover - platform dependent
+ pass
+
def _load_tokens(self) -> None:
"""Load tokens from file."""
if self.tokens_file.exists():
@@ -32,7 +70,7 @@ def _load_tokens(self) -> None:
else:
self.tokens = json.loads(content)
except (json.JSONDecodeError, OSError) as e:
- print(f"Warning: Failed to load tokens from {self.tokens_file}: {e}")
+ logger.warning(f"Failed to load tokens from {self.tokens_file}: {e}")
self.tokens = {}
self._save_tokens()
else:
@@ -40,9 +78,32 @@ def _load_tokens(self) -> None:
self._save_tokens()
def _save_tokens(self) -> None:
- """Save tokens to file."""
- with open(self.tokens_file, "w") as f:
- json.dump(self.tokens, f, indent=2)
+ """Persist tokens atomically.
+
+ Writing in place truncates the file before the new content lands, so a
+ crash or a concurrent reader during the write window would see an empty
+ or half-written token store and lock every client out. Writing to a
+ temporary file in the same directory and renaming it makes the
+ replacement atomic on POSIX filesystems.
+ """
+ directory = self.tokens_file.parent
+ directory.mkdir(parents=True, exist_ok=True)
+ fd, tmp_path = tempfile.mkstemp(
+ dir=str(directory),
+ prefix=self.tokens_file.name,
+ suffix=".tmp",
+ )
+ try:
+ with os.fdopen(fd, "w") as handle:
+ json.dump(self.tokens, handle, indent=2)
+ handle.flush()
+ os.fsync(handle.fileno())
+ os.chmod(tmp_path, 0o600)
+ os.replace(tmp_path, self.tokens_file)
+ except BaseException:
+ with contextlib.suppress(OSError):
+ os.unlink(tmp_path)
+ raise
def _hash_token(self, token: str) -> str:
"""Hash a token for secure storage."""
@@ -64,18 +125,26 @@ def create_token(
Returns:
The generated token
"""
+ requested = list(permissions) if permissions is not None else ["read", "write", "execute"]
+ unknown = sorted(set(requested) - VALID_PERMISSIONS)
+ if unknown:
+ raise ValueError(
+ f"Unknown permission(s): {unknown}. Valid permissions are "
+ f"{sorted(VALID_PERMISSIONS)}.",
+ )
+
token = f"uf_{secrets.token_urlsafe(32)}"
token_hash = self._hash_token(token)
- self.tokens[token_hash] = {
- "name": name,
- "project": project,
- "permissions": permissions or ["read", "write", "execute"],
- "created_at": datetime.now().isoformat(),
- "last_used": None,
- }
-
- self._save_tokens()
+ with self._lock:
+ self.tokens[token_hash] = {
+ "name": name,
+ "project": project,
+ "permissions": requested,
+ "created_at": datetime.now().isoformat(),
+ "last_used": None,
+ }
+ self._save_tokens()
return token
def verify_token(self, token: str) -> dict[str, Any] | None:
@@ -90,14 +159,27 @@ def verify_token(self, token: str) -> dict[str, Any] | None:
token_hash = self._hash_token(token)
token_data = self.tokens.get(token_hash)
- if token_data:
- # Update last used timestamp
- token_data["last_used"] = datetime.now().isoformat()
- self.tokens[token_hash] = token_data
- self._save_tokens()
+ if token_data is None:
+ return None
+
+ now = datetime.now()
+ token_data["last_used"] = now.isoformat()
+
+ # Persist the timestamp at most once per LAST_USED_WRITE_INTERVAL.
+ # Rewriting the whole token store on every authenticated request made
+ # each API call wait on a synchronous fsync, and concurrent requests
+ # raced on the same file.
+ if self._should_persist_last_used(token_hash, now):
+ with self._lock:
+ self._last_persisted[token_hash] = now
+ self._save_tokens()
return token_data
+ def _should_persist_last_used(self, token_hash: str, now: datetime) -> bool:
+ previous = self._last_persisted.get(token_hash)
+ return previous is None or (now - previous) >= LAST_USED_WRITE_INTERVAL
+
def revoke_token(self, token: str) -> bool:
"""Revoke a token.
@@ -108,23 +190,63 @@ def revoke_token(self, token: str) -> bool:
True if revoked, False if not found
"""
token_hash = self._hash_token(token)
- if token_hash in self.tokens:
- del self.tokens[token_hash]
- self._save_tokens()
- return True
+ with self._lock:
+ if token_hash in self.tokens:
+ del self.tokens[token_hash]
+ self._last_persisted.pop(token_hash, None)
+ self._save_tokens()
+ return True
+ return False
+
+ def revoke_token_by_id(self, token_id: str) -> bool:
+ """Revoke the token with the given public identifier.
+
+ ``list_tokens`` never reveals token values, so the UI cannot call
+ :meth:`revoke_token`. The identifier is a truncated hash of the hashed
+ token: enough to address a token, useless for reconstructing one.
+ """
+ with self._lock:
+ for token_hash in list(self.tokens):
+ if self._public_id(token_hash) == token_id:
+ del self.tokens[token_hash]
+ self._last_persisted.pop(token_hash, None)
+ self._save_tokens()
+ return True
return False
+ def revoke_tokens_by_name(self, name: str) -> int:
+ """Revoke every token with the given name, returning how many were removed.
+
+ Names are user-supplied labels and are not required to be unique, so
+ this deliberately revokes all matches rather than picking one
+ arbitrarily.
+ """
+ with self._lock:
+ matching = [h for h, data in self.tokens.items() if data.get("name") == name]
+ for token_hash in matching:
+ del self.tokens[token_hash]
+ self._last_persisted.pop(token_hash, None)
+ if matching:
+ self._save_tokens()
+ return len(matching)
+
+ @staticmethod
+ def _public_id(token_hash: str) -> str:
+ """Derive a stable, non-secret identifier from a stored token hash."""
+ return hashlib.sha256(f"id:{token_hash}".encode()).hexdigest()[:16]
+
def list_tokens(self) -> list:
"""List all tokens (without revealing the actual token values)."""
return [
{
+ "id": self._public_id(token_hash),
"name": data["name"],
"project": data["project"],
"permissions": data["permissions"],
"created_at": data["created_at"],
"last_used": data["last_used"],
}
- for data in self.tokens.values()
+ for token_hash, data in self.tokens.items()
]
diff --git a/flowyml/ui/backend/main.py b/flowyml/ui/backend/main.py
index 979545df..db9f12d9 100644
--- a/flowyml/ui/backend/main.py
+++ b/flowyml/ui/backend/main.py
@@ -5,8 +5,19 @@
from fastapi.exceptions import RequestValidationError
import os
import traceback
+from contextlib import asynccontextmanager
+from flowyml import __version__
from flowyml.monitoring.alerts import alert_manager, AlertLevel
+from flowyml.ui.backend.security import (
+ allow_insecure,
+ assert_production_security,
+ constant_time_equals,
+ get_api_token,
+ get_cors_origins,
+ is_production,
+ is_public_path,
+)
# OpenTelemetry Imports
from opentelemetry import trace, metrics
@@ -66,8 +77,12 @@
except ImportError:
# Fallback to console if OTLP exporter not installed
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
-else:
- # Development: Console exporter for debugging
+elif os.getenv("FLOWYML_OTEL_CONSOLE", "").strip().lower() in {"1", "true", "yes", "on"}:
+ # Opt-in local debugging. The console exporter prints a multi-line JSON
+ # document per span - four or more per HTTP request - so enabling it by
+ # default drowned real log output and made `flowyml ui` unreadable in a
+ # terminal. Tracing stays fully active either way; only the noisy stdout
+ # sink is opt-in.
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)
@@ -78,10 +93,18 @@
# Routers included above
+@asynccontextmanager
+async def lifespan(_app: FastAPI):
+ """Validate the deployment before accepting the first request."""
+ assert_production_security()
+ yield
+
+
app = FastAPI(
title="flowyml UI",
description="Real-time UI for flowyml pipelines",
- version="0.1.0",
+ version=__version__,
+ lifespan=lifespan,
)
# Instrument FastAPI
@@ -91,90 +114,83 @@
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
-# Configure CORS - Production settings with localhost for development
-_is_production = os.getenv("FLOWYML_ENV") == "production"
-_cors_origins = (
- [
- # Production: Only allow specific origins
- "https://flowyml.unicolab.ai",
- "https://app.flowyml.io",
- ]
- if _is_production
- else [
- # Development: Allow localhost and common dev ports
- "http://localhost:3000",
- "http://localhost:5173",
- "http://localhost:8080",
- "http://127.0.0.1:3000",
- "http://127.0.0.1:5173",
- "http://127.0.0.1:8080",
- "*", # Fallback for dev flexibility
- ]
-)
-
+# Configure CORS. Origins are resolved from the environment so that a
+# wildcard is never paired with credentialed requests - see
+# flowyml.ui.backend.security.get_cors_origins for why that pairing is unsafe.
app.add_middleware(
CORSMiddleware,
- allow_origins=_cors_origins,
+ allow_origins=get_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
+def _unauthorized(message: str) -> JSONResponse:
+ """Build a 401 that tells the client how to authenticate."""
+ return JSONResponse(
+ status_code=401,
+ content={"error": "Unauthorized", "message": message},
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+
class AuthMiddleware(BaseHTTPMiddleware):
+ """Require a bearer token for every non-public request in production.
+
+ Development deliberately runs open so ``flowyml ui`` needs no setup. In
+ production the middleware fails *closed*: an instance that declares
+ ``FLOWYML_ENV=production`` without ``FLOWYML_API_TOKEN`` is rejected at
+ startup by ``assert_production_security``, and this middleware refuses
+ every request as a second line of defence in case startup validation was
+ bypassed (for example by an ASGI host that imports ``app`` directly).
+ """
+
async def dispatch(self, request: Request, call_next):
- # 0. Skip if not in production to allow easy local development
- if os.getenv("FLOWYML_ENV") != "production":
+ # Local development runs without authentication by design.
+ if not is_production():
return await call_next(request)
- # 1. Check if token auth is enabled via env var
- api_token = os.getenv("FLOWYML_API_TOKEN")
- if not api_token:
+ # An operator fronting FlowyML with their own auth proxy opts out.
+ if allow_insecure():
return await call_next(request)
- # 2. Define public paths
path = request.url.path
- if (
- path in ["/api/health", "/metrics", "/docs", "/redoc", "/openapi.json"]
- or path.startswith(
- ("/assets", "/api/auth/login", "/api/auth/logout"),
- ) # Allow login endpoints
- or path == "/"
- or request.method == "OPTIONS"
- ):
+
+ # CORS preflight carries no credentials by definition.
+ if request.method == "OPTIONS" or is_public_path(path):
return await call_next(request)
- # 3. Check Auth Header OR Cookie
- auth_header = request.headers.get("Authorization")
- if not auth_header:
- # Check for cookie
- cookie_token = request.cookies.get("access_token")
- if cookie_token:
- auth_header = cookie_token # Reuse validation logic below
- else:
- token_param = request.query_params.get("token")
- if token_param == api_token:
- return await call_next(request)
-
- return JSONResponse(
- status_code=401,
- content={"error": "Unauthorized", "message": "Missing authentication token"},
- )
-
- # 4. Validate Token
- try:
- scheme, token = auth_header.split()
- if scheme.lower() != "bearer" or token != api_token:
- return JSONResponse(
- status_code=401,
- content={"error": "Unauthorized", "message": "Invalid authentication token"},
- )
- except ValueError:
+ api_token = get_api_token()
+ if api_token is None:
+ # Fail closed. Previously this branch let the request through,
+ # which left the whole control plane - including remote pipeline
+ # execution - open to anyone who could reach the port.
return JSONResponse(
- status_code=401,
- content={"error": "Unauthorized", "message": "Invalid Authorization header format"},
+ status_code=503,
+ content={
+ "error": "Service Unavailable",
+ "message": (
+ "Server is misconfigured: FLOWYML_ENV=production but "
+ "FLOWYML_API_TOKEN is not set, so no request can be "
+ "authenticated."
+ ),
+ },
)
+ auth_header = request.headers.get("Authorization") or request.cookies.get("access_token")
+
+ if not auth_header:
+ return _unauthorized("Missing authentication token")
+
+ # Accept "Bearer " from either the header or the session cookie.
+ scheme, _, token = auth_header.partition(" ")
+ if scheme.lower() != "bearer" or not token:
+ return _unauthorized("Invalid Authorization header format")
+
+ if not constant_time_equals(token.strip(), api_token):
+ return _unauthorized("Invalid authentication token")
+
return await call_next(request)
@@ -184,7 +200,7 @@ async def dispatch(self, request: Request, call_next):
# Health check endpoint
@app.get("/api/health")
async def health_check():
- return {"status": "ok", "version": "0.1.0"}
+ return {"status": "ok", "version": __version__}
@app.get("/api/config")
diff --git a/flowyml/ui/backend/routers/ai_context.py b/flowyml/ui/backend/routers/ai_context.py
index 024f7130..264920ef 100644
--- a/flowyml/ui/backend/routers/ai_context.py
+++ b/flowyml/ui/backend/routers/ai_context.py
@@ -13,6 +13,10 @@
router = APIRouter(prefix="/api/ai", tags=["ai"])
+#: Page types this router can build context for. Kept in one place so the
+#: error message and the documented contract cannot drift apart.
+SUPPORTED_PAGE_TYPES = frozenset({"run"})
+
class AIContextRequest(BaseModel):
"""Request model for AI context."""
@@ -211,23 +215,34 @@ async def get_ai_context(request: AIContextRequest):
)
# Add more page types as needed
- raise HTTPException(status_code=400, detail=f"Unsupported page type: {request.page_type}")
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"Unsupported page type: {request.page_type}. "
+ f"Supported page types: {sorted(SUPPORTED_PAGE_TYPES)}"
+ ),
+ )
-@router.get("/context/run/{run_id}")
-async def get_run_ai_context(
- run_id: str,
+@router.get("/context/{page_type}/{resource_id}")
+async def get_resource_ai_context(
+ page_type: str,
+ resource_id: str,
include_logs: bool = True,
include_code: bool = True,
include_metrics: bool = True,
):
- """
- Convenience endpoint for getting AI context for a specific run.
+ """Convenience GET form of :func:`get_ai_context`.
+
+ Accepts any ``page_type`` so that an unsupported one yields the same
+ explanatory 400 as the POST endpoint. Previously this route was hardcoded
+ to ``/context/run/{run_id}``, so the UI - which builds the URL from a
+ generic ``pageType`` - got an unexplained routing 404 for every other page.
"""
return await get_ai_context(
AIContextRequest(
- page_type="run",
- resource_id=run_id,
+ page_type=page_type,
+ resource_id=resource_id,
include_logs=include_logs,
include_code=include_code,
include_metrics=include_metrics,
diff --git a/flowyml/ui/backend/routers/auth.py b/flowyml/ui/backend/routers/auth.py
index 254091b1..c5e053e3 100644
--- a/flowyml/ui/backend/routers/auth.py
+++ b/flowyml/ui/backend/routers/auth.py
@@ -1,6 +1,25 @@
+"""Session authentication for the FlowyML UI.
+
+FlowyML ships a single administrative identity. The credentials and the
+session token are resolved from the environment on *every* request rather than
+captured into module-level constants at import time, so that a process which
+loads its ``.env`` after importing the app — or a test that patches the
+environment — sees the current values instead of whatever happened to be set
+when the module was first imported.
+"""
+
from fastapi import APIRouter, HTTPException, status, Response, Request
from pydantic import BaseModel
-import os
+
+from flowyml.ui.backend.security import (
+ INSECURE_DEFAULT_PASSWORD,
+ allow_insecure,
+ constant_time_equals,
+ get_admin_password,
+ get_admin_user,
+ get_api_token,
+ is_production,
+)
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -21,48 +40,71 @@ class LoginRequest(BaseModel):
password: str
-# --- Config ---
-# For simplicity in this iteration, we use single-user admin auth
-ADMIN_USER = os.getenv("FLOWYML_ADMIN_USER", "admin")
-ADMIN_PASSWORD = os.getenv("FLOWYML_ADMIN_PASSWORD", "flowyml")
-# In production, this token is also a long-lived secret or signed JWT.
-# For simplicity, we reuse the API Token logic or generate a session token.
-# Here, we'll implement a simple session token mechanism or reuse API token.
-API_TOKEN = os.getenv("FLOWYML_API_TOKEN")
-
# --- Routes ---
@router.post("/login", response_model=Token)
async def login(response: Response, login_data: LoginRequest):
+ """Authenticate an operator and open a browser session.
+
+ Sets an HttpOnly cookie for the UI and returns the same token for CLI and
+ SDK use.
"""
- Login with username and password.
- Sets HttpOnly cookie for browser sessions.
- Returns token for CLI/API use.
- """
- if login_data.username != ADMIN_USER or login_data.password != ADMIN_PASSWORD:
+ expected_user = get_admin_user()
+ expected_password = get_admin_password()
+ production = is_production()
+
+ if expected_password is None:
+ if production and not allow_insecure():
+ # Never fall back to the documented default in production. Startup
+ # validation normally prevents reaching this point at all; this
+ # branch covers a host that imported ``app`` without running the
+ # lifespan handler.
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail=(
+ "Server is misconfigured: FLOWYML_ADMIN_PASSWORD is not set, "
+ "so no login can be authenticated."
+ ),
+ )
+ expected_password = INSECURE_DEFAULT_PASSWORD
+
+ # Compare both fields in constant time, and always compare both, so that
+ # response latency reveals neither which field was wrong nor the length of
+ # the configured password.
+ user_ok = constant_time_equals(login_data.username, expected_user)
+ password_ok = constant_time_equals(login_data.password, expected_password)
+
+ if not (user_ok and password_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
- # Use the API token as the session token for simplicity in this unified auth model
- # Ensure API_TOKEN is set in production!
- if not API_TOKEN:
- # Fallback for dev if not set (though middleware skips auth in dev usually)
+ token = get_api_token()
+ if token is None:
+ if production and not allow_insecure():
+ # The previous implementation issued the fixed string
+ # "dev-token-placeholder" here, which any client could guess and
+ # replay as a valid production session.
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail=(
+ "Server is misconfigured: FLOWYML_API_TOKEN is not set, so "
+ "no session token can be issued."
+ ),
+ )
+ # Development only: the auth middleware is inactive here anyway, so
+ # this value is a placeholder rather than a credential.
token = "dev-token-placeholder" # noqa: S105
- else:
- token = API_TOKEN
- # Set HttpOnly cookie
- # accessible only by server, secure in prod
response.set_cookie(
key="access_token",
value=f"Bearer {token}",
httponly=True,
max_age=86400 * 7, # 7 days
- secure=os.getenv("FLOWYML_ENV") == "production",
+ secure=production,
samesite="lax",
)
@@ -72,25 +114,26 @@ async def login(response: Response, login_data: LoginRequest):
@router.post("/logout")
async def logout(response: Response):
"""Clear session cookie."""
- response.delete_cookie("access_token")
+ # The attributes must match those used by set_cookie, otherwise the browser
+ # keeps the original cookie and the session survives "logout".
+ response.delete_cookie(
+ key="access_token",
+ httponly=True,
+ secure=is_production(),
+ samesite="lax",
+ )
return {"message": "Logged out successfully"}
@router.get("/me", response_model=User)
async def get_current_user(request: Request):
- """
- Get current logged in user.
- Used by frontend to verify session.
- """
- # If middleware let us through, we are authenticated.
- # In 'production' mode, unauth requests are blocked by middleware.
- # In 'development' mode, middleware skips, so we check env.
+ """Return the currently authenticated operator.
- if os.getenv("FLOWYML_ENV") != "production":
- # Local dev: always return default admin
+ Reaching this endpoint in production means ``AuthMiddleware`` already
+ validated the caller's bearer token or session cookie.
+ """
+ if not is_production():
+ # Local development runs without authentication by design.
return {"username": "developer", "role": "admin"}
- # In production, if we reached here, AuthMiddleware validated us.
- # We can inspect headers/cookies to determine *who* it is
- # (if we had multi-user), but for now it's just Admin.
- return {"username": ADMIN_USER, "role": "admin"}
+ return {"username": get_admin_user(), "role": "admin"}
diff --git a/flowyml/ui/backend/routers/evaluations.py b/flowyml/ui/backend/routers/evaluations.py
index 33564779..4449fc6e 100644
--- a/flowyml/ui/backend/routers/evaluations.py
+++ b/flowyml/ui/backend/routers/evaluations.py
@@ -7,7 +7,7 @@
import logging
from fastapi import APIRouter, HTTPException, Query
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, ConfigDict, Field
logger = logging.getLogger(__name__)
@@ -57,8 +57,7 @@ class ScorerInfo(BaseModel):
description: str
cls: str = Field(alias="class", default="")
- class Config:
- populate_by_name = True
+ model_config = ConfigDict(populate_by_name=True)
class CompareRequest(BaseModel):
diff --git a/flowyml/ui/backend/routers/execution.py b/flowyml/ui/backend/routers/execution.py
index de33ff94..0fa6a2d6 100644
--- a/flowyml/ui/backend/routers/execution.py
+++ b/flowyml/ui/backend/routers/execution.py
@@ -6,9 +6,32 @@
from typing import Any
from flowyml.ui.backend.auth import verify_api_token, security
import importlib
+import platform
+import time
+
+from loguru import logger
router = APIRouter()
+#: Process start time, captured at import, used to report server uptime.
+_SERVER_START_TIME = time.time()
+
+
+def _format_uptime(seconds: float) -> str:
+ """Render a duration as a compact human-readable string (e.g. "2d 3h 4m")."""
+ total = int(seconds)
+ days, remainder = divmod(total, 86400)
+ hours, remainder = divmod(remainder, 3600)
+ minutes, secs = divmod(remainder, 60)
+
+ if days:
+ return f"{days}d {hours}h {minutes}m"
+ if hours:
+ return f"{hours}h {minutes}m"
+ if minutes:
+ return f"{minutes}m {secs}s"
+ return f"{secs}s"
+
def require_permission(permission: str):
"""Create a dependency for checking permissions."""
@@ -156,13 +179,82 @@ async def create_token(
@router.get("/tokens")
async def list_tokens():
- """List all API tokens (without revealing token values)."""
+ """List all API tokens (without revealing token values).
+
+ Deliberately unauthenticated: the UI calls this before any token exists in
+ order to decide whether to offer first-time setup, and the auth middleware
+ already gates the whole API in production. Token *values* are never
+ returned - only labels, scopes and timestamps.
+ """
from flowyml.ui.backend.auth import token_manager
- # Allow listing tokens without auth (for UI to check if any exist)
return {"tokens": token_manager.list_tokens()}
+@router.delete("/tokens/{token_ref}")
+async def revoke_token(token_ref: str):
+ """Revoke an API token by its public id, or by name.
+
+ ``token_ref`` is matched against the opaque ``id`` returned by
+ ``GET /tokens`` first; if nothing matches it is treated as a token *name*,
+ which revokes every token carrying that label. Names are user-supplied and
+ need not be unique, so the response reports how many were removed.
+ """
+ from flowyml.ui.backend.auth import token_manager
+
+ if token_manager.revoke_token_by_id(token_ref):
+ return {"revoked": 1, "message": f"Token '{token_ref}' revoked"}
+
+ revoked = token_manager.revoke_tokens_by_name(token_ref)
+ if revoked == 0:
+ raise HTTPException(
+ status_code=404,
+ detail=f"No API token matches id or name '{token_ref}'",
+ )
+
+ return {
+ "revoked": revoked,
+ "message": f"Revoked {revoked} token(s) named '{token_ref}'",
+ }
+
+
+@router.get("/info")
+async def get_server_info():
+ """Report runtime facts about this server for the Settings page.
+
+ Every field is derived from the live process. The UI previously fell back
+ to hard-coded placeholders here (version "0.1.0", database "PostgreSQL"),
+ which misreported both the release and the storage backend in use.
+ """
+ from flowyml import __version__
+ from flowyml.ui.backend.security import is_production
+
+ database = "unknown"
+ try:
+ from flowyml.ui.backend.dependencies import get_store
+
+ store = get_store()
+ engine = getattr(store, "engine", None)
+ if engine is not None:
+ database = engine.dialect.name
+ else: # pragma: no cover - store implementations without a SQLAlchemy engine
+ database = type(store).__name__
+ except Exception as exc: # pragma: no cover - never fail the settings page
+ logger.warning(f"Could not determine database backend: {exc}")
+
+ uptime_seconds = max(0.0, time.time() - _SERVER_START_TIME)
+
+ return {
+ "version": __version__,
+ "environment": "production" if is_production() else "development",
+ "database": database,
+ "uptime": _format_uptime(uptime_seconds),
+ "uptime_seconds": round(uptime_seconds, 3),
+ "python_version": platform.python_version(),
+ "platform": platform.platform(),
+ }
+
+
@router.post("/tokens/init")
async def initialize_first_token():
"""Create the first admin token (no auth required).
diff --git a/flowyml/ui/backend/security.py b/flowyml/ui/backend/security.py
new file mode 100644
index 00000000..c7168cfb
--- /dev/null
+++ b/flowyml/ui/backend/security.py
@@ -0,0 +1,196 @@
+"""Central security configuration for the FlowyML UI backend.
+
+Every security decision the backend makes is resolved here, at *request* time
+rather than import time, so that tests and embedding hosts can change the
+environment without reimporting the application.
+
+The guiding rule is **fail closed**: a deployment that declares itself
+production but has not been given credentials refuses to serve rather than
+silently exposing an unauthenticated control plane. ``/api/execution/execute``
+imports and runs arbitrary Python modules, so an open instance is remote code
+execution, not merely an information leak.
+
+Operators who terminate authentication in front of FlowyML (an ingress
+controller, an OAuth2 proxy, a service mesh) can opt out of the built-in check
+with ``FLOWYML_ALLOW_INSECURE=1``.
+"""
+
+from __future__ import annotations
+
+import os
+import secrets
+
+# The historical default password. Shipping it means every deployment that
+# forgets to set FLOWYML_ADMIN_PASSWORD shares one publicly documented
+# credential, so it is rejected outright in production.
+INSECURE_DEFAULT_PASSWORD = "flowyml" # noqa: S105
+DEFAULT_ADMIN_USER = "admin"
+
+#: Paths that must stay reachable without authentication for the UI to be
+#: usable at all: health probes, the login form itself, and the static bundle.
+PUBLIC_PATHS: frozenset[str] = frozenset(
+ {
+ "/",
+ "/api/health",
+ "/api/auth/login",
+ "/api/auth/logout",
+ "/docs",
+ "/redoc",
+ "/openapi.json",
+ "/favicon.ico",
+ },
+)
+
+#: Path *prefixes* that are public. Kept explicit (with the trailing slash for
+#: directory-like prefixes) so that ``/assets`` cannot be widened into a match
+#: for an unrelated route such as ``/assets-internal``.
+PUBLIC_PATH_PREFIXES: tuple[str, ...] = ("/assets/", "/metrics")
+
+
+def _env(name: str) -> str | None:
+ """Read an environment variable, treating blank/whitespace as unset.
+
+ ``docker-compose.yml`` and ``.env.example`` both ship
+ ``FLOWYML_API_TOKEN=`` with an empty value, so an empty string must not be
+ mistaken for a configured secret.
+ """
+ value = os.getenv(name)
+ if value is None:
+ return None
+ value = value.strip()
+ return value or None
+
+
+def is_production() -> bool:
+ """True when the deployment declares itself production."""
+ env = _env("FLOWYML_ENV")
+ return env is not None and env.lower() == "production"
+
+
+def allow_insecure() -> bool:
+ """True when the operator has explicitly accepted running without auth."""
+ value = _env("FLOWYML_ALLOW_INSECURE")
+ return value is not None and value.lower() in {"1", "true", "yes", "on"}
+
+
+def get_api_token() -> str | None:
+ """The shared API token used for machine and session authentication."""
+ return _env("FLOWYML_API_TOKEN")
+
+
+def get_admin_user() -> str:
+ """The username accepted by the UI login form."""
+ return _env("FLOWYML_ADMIN_USER") or DEFAULT_ADMIN_USER
+
+
+def get_admin_password() -> str | None:
+ """The password accepted by the UI login form, if configured."""
+ return _env("FLOWYML_ADMIN_PASSWORD")
+
+
+def constant_time_equals(left: str | None, right: str | None) -> bool:
+ """Compare two secrets without leaking their contents through timing.
+
+ A plain ``==`` on strings short-circuits at the first differing byte, which
+ lets an attacker recover a token one character at a time. ``None`` never
+ matches anything, so an unset secret cannot be satisfied by sending an
+ empty value.
+ """
+ if left is None or right is None:
+ return False
+ return secrets.compare_digest(left, right)
+
+
+def is_public_path(path: str) -> bool:
+ """True when *path* is reachable without authentication."""
+ if path in PUBLIC_PATHS:
+ return True
+ return path.startswith(PUBLIC_PATH_PREFIXES)
+
+
+def security_misconfigurations() -> list[str]:
+ """Describe every reason this deployment is unsafe to expose.
+
+ Returns an empty list when the configuration is sound, or when the
+ deployment is not production (local development intentionally runs open),
+ or when the operator has set ``FLOWYML_ALLOW_INSECURE``.
+ """
+ if not is_production() or allow_insecure():
+ return []
+
+ problems: list[str] = []
+
+ if get_api_token() is None:
+ problems.append(
+ "FLOWYML_API_TOKEN is not set. Without it the API accepts every "
+ "request, including POST /api/execution/execute, which imports and "
+ "runs arbitrary Python modules.",
+ )
+
+ password = get_admin_password()
+ if password is None:
+ problems.append(
+ "FLOWYML_ADMIN_PASSWORD is not set. The UI login form would accept "
+ f"the publicly documented default password '{INSECURE_DEFAULT_PASSWORD}'.",
+ )
+ elif password == INSECURE_DEFAULT_PASSWORD:
+ problems.append(
+ "FLOWYML_ADMIN_PASSWORD is still the publicly documented default "
+ f"'{INSECURE_DEFAULT_PASSWORD}'. Choose a unique password.",
+ )
+
+ return problems
+
+
+def assert_production_security() -> None:
+ """Raise when a production deployment is missing its credentials.
+
+ Called during application startup so the operator sees the problem
+ immediately, instead of discovering months later that the control plane was
+ world-writable.
+ """
+ problems = security_misconfigurations()
+ if not problems:
+ return
+
+ bullet_list = "\n".join(f" - {p}" for p in problems)
+ raise RuntimeError(
+ "FlowyML refuses to start: FLOWYML_ENV=production but the deployment "
+ f"is not secured.\n{bullet_list}\n\n"
+ "Set the variables above, or set FLOWYML_ALLOW_INSECURE=1 if "
+ "authentication is enforced by a proxy in front of FlowyML.",
+ )
+
+
+def get_cors_origins() -> list[str]:
+ """Resolve the allowed CORS origins for this deployment.
+
+ ``FLOWYML_CORS_ORIGINS`` accepts a comma-separated list and overrides the
+ defaults in either environment.
+
+ The wildcard ``*`` is never combined with credentialed requests: browsers
+ reject that pairing, and Starlette works around it by reflecting whatever
+ ``Origin`` the caller sent, which would let any website on the internet
+ issue credentialed requests to a developer's local instance and read the
+ responses.
+ """
+ configured = _env("FLOWYML_CORS_ORIGINS")
+ if configured:
+ return [origin.strip() for origin in configured.split(",") if origin.strip()]
+
+ if is_production():
+ return [
+ "https://flowyml.unicolab.ai",
+ "https://app.flowyml.io",
+ ]
+
+ # Development: the concrete origins a Vite/CRA dev server uses. Explicit
+ # rather than "*" so credentialed cross-origin reads stay impossible.
+ return [
+ "http://localhost:3000",
+ "http://localhost:5173",
+ "http://localhost:8080",
+ "http://127.0.0.1:3000",
+ "http://127.0.0.1:5173",
+ "http://127.0.0.1:8080",
+ ]
diff --git a/flowyml/ui/frontend/src/app/tokens/page.jsx b/flowyml/ui/frontend/src/app/tokens/page.jsx
index 4537883e..6c2c3287 100644
--- a/flowyml/ui/frontend/src/app/tokens/page.jsx
+++ b/flowyml/ui/frontend/src/app/tokens/page.jsx
@@ -17,7 +17,7 @@ import {
import { Card, CardHeader, CardTitle, CardContent } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { Badge } from '../../components/ui/Badge';
-import { format } from 'date-fns';
+import { formatDate } from '../../utils/date';
import { motion, AnimatePresence } from 'framer-motion';
export function TokenManagement() {
@@ -142,7 +142,12 @@ export function TokenManagement() {
{tokens.map((token, idx) => (
-
+
))}
@@ -205,18 +210,39 @@ function PermissionChip({ perm }) {
);
}
-function TokenItem({ token, onRevoke }) {
+function TokenItem({ token, onRevoke, onError }) {
const [showRevoke, setShowRevoke] = useState(false);
+ const [revoking, setRevoking] = useState(false);
const handleRevoke = async () => {
+ // Prefer the opaque id: token names are user-supplied labels and are
+ // not required to be unique, so revoking by name could remove more
+ // than the row the operator clicked.
+ const ref = token.id ?? token.name;
+ setRevoking(true);
try {
- // This would need to be implemented in the backend
- await fetchApi(`/api/execution/tokens/${token.name}`, {
+ const res = await fetchApi(`/api/execution/tokens/${encodeURIComponent(ref)}`, {
method: 'DELETE'
});
+ if (!res.ok) {
+ let detail = `Request failed with status ${res.status}`;
+ try {
+ const body = await res.json();
+ if (body?.detail) detail = body.detail;
+ } catch {
+ // Response had no JSON body; keep the status-based message.
+ }
+ throw new Error(detail);
+ }
+ setShowRevoke(false);
onRevoke();
} catch (err) {
console.error('Failed to revoke token:', err);
+ // Surface the failure: silently swallowing it told the operator a
+ // leaked token had been revoked when it was still valid.
+ onError?.(`Failed to revoke token "${token.name}": ${err.message}`);
+ } finally {
+ setRevoking(false);
}
};
@@ -244,10 +270,10 @@ function TokenItem({ token, onRevoke }) {
- Created: {format(new Date(token.created_at), 'MMM d, yyyy')}
+ Created: {formatDate(token.created_at)}
{token.last_used && (
- Last used: {format(new Date(token.last_used), 'MMM d, yyyy')}
+ Last used: {formatDate(token.last_used)}
)}
@@ -266,10 +292,15 @@ function TokenItem({ token, onRevoke }) {
Are you sure you want to revoke this token? This action cannot be undone.
-
- Yes, Revoke
+
+ {revoking ? 'Revoking...' : 'Yes, Revoke'}
- setShowRevoke(false)}>
+ setShowRevoke(false)}
+ disabled={revoking}
+ >
Cancel
diff --git a/flowyml/ui/frontend/src/hooks/useAIContext.js b/flowyml/ui/frontend/src/hooks/useAIContext.js
index e207b9c1..700856e3 100644
--- a/flowyml/ui/frontend/src/hooks/useAIContext.js
+++ b/flowyml/ui/frontend/src/hooks/useAIContext.js
@@ -9,7 +9,9 @@ import { fetchApi } from '../utils/api';
* Automatically cleans up context when navigating away.
*
* @param {Object} options - Configuration options
- * @param {string} options.pageType - Type of page ('run', 'pipeline', 'experiment', etc.)
+ * @param {string} options.pageType - Type of page. The backend currently builds
+ * context for 'run' only; any other value returns a 400 and the hook falls
+ * back to the basic client-side context.
* @param {string} options.resourceId - The resource ID (run_id, pipeline_name, etc.)
* @param {boolean} options.includeLogs - Whether to include logs (default: true)
* @param {boolean} options.includeCode - Whether to include step code (default: true)
diff --git a/tests/test_api_alignment.py b/tests/test_api_alignment.py
new file mode 100644
index 00000000..e685c459
--- /dev/null
+++ b/tests/test_api_alignment.py
@@ -0,0 +1,229 @@
+"""Guard the contract between the React frontend and the FastAPI backend.
+
+Every URL the frontend builds is extracted from the source and matched against
+the backend's own OpenAPI schema. A page that calls an endpoint nobody
+implemented fails silently at runtime - the UI just renders its fallback - so
+the mismatch is invisible until a user notices the numbers are wrong. That is
+exactly how ``/api/execution/info`` came to display a hard-coded version string
+and how the "Revoke" button on the API tokens page came to do nothing at all.
+"""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+FRONTEND_SRC = REPO_ROOT / "flowyml" / "ui" / "frontend" / "src"
+
+#: Placeholder standing in for a `${...}` template-literal interpolation.
+INTERPOLATION = "\x00"
+
+#: Locates the opening quote of a `fetch(...)` / `fetchApi(...)` URL argument.
+CALL_START_RE = re.compile(r"\b(?:fetchApi|fetch)\s*\(\s*([`'\"])(?=/api|/ws)")
+
+#: Finds `method: 'POST'` in the options object that follows the URL.
+METHOD_RE = re.compile(r"^\s*,\s*\{[^}]{0,400}?method\s*:\s*['\"`](\w+)", re.S)
+
+
+def _read_string_literal(source: str, quote: str, start: int) -> tuple[str, int]:
+ """Read a JS string literal, honouring nested template interpolations.
+
+ A template literal may contain further backtick-quoted strings inside its
+ ``${...}`` holes, as in ``/api/evaluations/list${qs ? `?${qs}` : ''}``. A
+ naive "scan to the next backtick" would truncate that URL mid-expression
+ and report a perfectly valid call as unimplemented, so interpolation depth
+ is tracked explicitly.
+
+ Returns the literal's contents and the index just past its closing quote.
+ """
+ out: list[str] = []
+ i = start
+ depth = 0 # nesting level inside ${ ... }
+ while i < len(source):
+ char = source[i]
+ if char == "\\":
+ out.append(source[i : i + 2])
+ i += 2
+ continue
+ if quote == "`" and char == "$" and source[i + 1 : i + 2] == "{":
+ depth += 1
+ out.append("${")
+ i += 2
+ continue
+ if depth and char == "}":
+ depth -= 1
+ out.append("}")
+ i += 1
+ continue
+ if char == quote and depth == 0:
+ return "".join(out), i + 1
+ if char == "\n" and quote != "`":
+ break # unterminated non-template string
+ out.append(char)
+ i += 1
+ return "".join(out), i
+
+
+@pytest.fixture(scope="module")
+def backend_routes() -> dict[str, set[str]]:
+ """Map every backend path to the HTTP methods it accepts."""
+ from flowyml.ui.backend.main import app
+
+ schema = app.openapi()
+ return {
+ path: {m.upper() for m in operations if m in {"get", "post", "put", "patch", "delete"}}
+ for path, operations in schema["paths"].items()
+ }
+
+
+def _iter_frontend_calls():
+ """Yield ``(url, method, source_file)`` for every API call in the frontend."""
+ for path in sorted(FRONTEND_SRC.rglob("*")):
+ if path.suffix not in {".js", ".jsx"} or not path.is_file():
+ continue
+ if "node_modules" in path.parts or "dist" in path.parts:
+ continue
+ source = path.read_text(encoding="utf-8")
+ for match in CALL_START_RE.finditer(source):
+ quote = match.group(1)
+ url, end = _read_string_literal(source, quote, match.end())
+ method_match = METHOD_RE.match(source[end: end + 500])
+ method = method_match.group(1).upper() if method_match else "GET"
+ yield url, method, path.relative_to(REPO_ROOT)
+
+
+def _mask_interpolations(url: str) -> str:
+ """Replace every balanced ``${...}`` hole with a single marker character."""
+ out: list[str] = []
+ i = 0
+ while i < len(url):
+ if url.startswith("${", i):
+ depth = 1
+ i += 2
+ while i < len(url) and depth:
+ if url[i] == "{":
+ depth += 1
+ elif url[i] == "}":
+ depth -= 1
+ i += 1
+ out.append(INTERPOLATION)
+ continue
+ out.append(url[i])
+ i += 1
+ return "".join(out)
+
+
+def _candidate_paths(url: str) -> list[str]:
+ """Normalize a JS template literal into the concrete paths it can produce."""
+ marked = _mask_interpolations(url)
+ # Only a literal `?` outside an interpolation starts the query string.
+ marked = marked.split("?", 1)[0]
+
+ candidates = [marked]
+ # An interpolation glued onto the end of a literal segment
+ # (`/api/x/scorers${qs}`) is a query string assembled elsewhere.
+ if marked.endswith(INTERPOLATION) and not marked.endswith("/" + INTERPOLATION):
+ candidates.append(marked[:-1])
+
+ resolved = []
+ for candidate in candidates:
+ trimmed = candidate.rstrip("/") or "/"
+ resolved.append(trimmed)
+ # FastAPI's redirect_slashes makes `/x` and `/x/` equivalent.
+ if trimmed != "/":
+ resolved.append(trimmed + "/")
+ return resolved
+
+
+def _matching_routes(candidate: str, backend_routes: dict[str, set[str]]) -> list[str]:
+ """Backend paths that *candidate* could resolve to, compared segment-wise."""
+ segments = candidate.strip("/").split("/")
+ matches = []
+
+ for route, _methods in backend_routes.items():
+ route_segments = route.strip("/").split("/")
+ if len(route_segments) != len(segments):
+ continue
+
+ for actual, expected in zip(segments, route_segments):
+ if expected.startswith("{") and expected.endswith("}"):
+ continue # a path parameter accepts anything
+ if actual == expected:
+ continue
+ if INTERPOLATION in actual:
+ continue # an interpolation may expand to this literal
+ break
+ else:
+ matches.append(route)
+
+ return matches
+
+
+def _resolve(url: str, backend_routes: dict[str, set[str]]) -> tuple[list[str], set[str]]:
+ routes: list[str] = []
+ methods: set[str] = set()
+ for candidate in _candidate_paths(url):
+ for route in _matching_routes(candidate, backend_routes):
+ routes.append(route)
+ methods |= backend_routes[route]
+ return routes, methods
+
+
+@pytest.mark.skipif(not FRONTEND_SRC.exists(), reason="frontend sources not present")
+def test_every_frontend_call_hits_an_implemented_route(backend_routes):
+ """No page may call an endpoint the backend does not implement."""
+ unimplemented = []
+ for url, method, source in _iter_frontend_calls():
+ routes, _ = _resolve(url, backend_routes)
+ if not routes:
+ unimplemented.append(f"{method} {url} ({source})")
+
+ assert not unimplemented, "Frontend calls with no backend route:\n" + "\n".join(
+ f" {entry}" for entry in unimplemented
+ )
+
+
+@pytest.mark.skipif(not FRONTEND_SRC.exists(), reason="frontend sources not present")
+def test_every_frontend_call_uses_a_supported_method(backend_routes):
+ """A route that exists but rejects the verb still breaks the page."""
+ wrong_method = []
+ for url, method, source in _iter_frontend_calls():
+ routes, methods = _resolve(url, backend_routes)
+ if routes and method not in methods:
+ wrong_method.append(
+ f"{method} {url} ({source}) -> {sorted(set(routes))} accepts {sorted(methods)}",
+ )
+
+ assert not wrong_method, "Frontend calls using an unsupported method:\n" + "\n".join(
+ f" {entry}" for entry in wrong_method
+ )
+
+
+@pytest.mark.skipif(not FRONTEND_SRC.exists(), reason="frontend sources not present")
+def test_alignment_check_actually_finds_calls():
+ """Protect the two tests above from silently passing on an empty scan."""
+ calls = list(_iter_frontend_calls())
+ assert len(calls) > 50, f"Only found {len(calls)} frontend API calls; the extractor is broken"
+
+
+def test_endpoints_the_ui_depends_on_exist(backend_routes):
+ """Explicit list of routes whose absence previously produced silent failures."""
+ required = {
+ "/api/execution/info": "GET",
+ "/api/execution/tokens": "GET",
+ "/api/execution/tokens/{token_ref}": "DELETE",
+ "/api/ai/context/{page_type}/{resource_id}": "GET",
+ "/api/health": "GET",
+ "/api/config": "GET",
+ }
+ missing = {
+ path: verb
+ for path, verb in required.items()
+ if path not in backend_routes or verb not in backend_routes[path]
+ }
+ assert not missing, f"Required endpoints missing or wrong method: {missing}"
diff --git a/tests/test_backend_security.py b/tests/test_backend_security.py
new file mode 100644
index 00000000..c80bdcca
--- /dev/null
+++ b/tests/test_backend_security.py
@@ -0,0 +1,315 @@
+"""Tests for the UI backend's authentication and authorization posture.
+
+These cover the fail-closed guarantees: a deployment that declares itself
+production but has no credentials must refuse to serve, must never fall back to
+the publicly documented default password, and must never mint a guessable
+session token. ``POST /api/execution/execute`` imports and runs arbitrary
+Python modules, so an unauthenticated instance is remote code execution.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+from fastapi.testclient import TestClient # noqa: E402
+
+from flowyml.ui.backend import security # noqa: E402
+
+
+@pytest.fixture
+def production_env(monkeypatch):
+ """Put the process into production mode with no credentials configured."""
+ monkeypatch.setenv("FLOWYML_ENV", "production")
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+ monkeypatch.delenv("FLOWYML_ADMIN_PASSWORD", raising=False)
+ monkeypatch.delenv("FLOWYML_ALLOW_INSECURE", raising=False)
+
+
+@pytest.fixture
+def dev_env(monkeypatch):
+ monkeypatch.delenv("FLOWYML_ENV", raising=False)
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+ monkeypatch.delenv("FLOWYML_ADMIN_PASSWORD", raising=False)
+ monkeypatch.delenv("FLOWYML_ALLOW_INSECURE", raising=False)
+
+
+@pytest.fixture
+def app():
+ from flowyml.ui.backend.main import app as fastapi_app
+
+ return fastapi_app
+
+
+# ---------------------------------------------------------------------------
+# security helpers
+# ---------------------------------------------------------------------------
+
+
+class TestSecurityHelpers:
+ def test_blank_env_var_counts_as_unset(self, monkeypatch):
+ """docker-compose ships FLOWYML_API_TOKEN= with an empty value."""
+ monkeypatch.setenv("FLOWYML_API_TOKEN", " ")
+ assert security.get_api_token() is None
+
+ def test_is_production_is_case_insensitive(self, monkeypatch):
+ monkeypatch.setenv("FLOWYML_ENV", "PRODUCTION")
+ assert security.is_production() is True
+
+ def test_non_production_env_is_not_production(self, monkeypatch):
+ monkeypatch.setenv("FLOWYML_ENV", "staging")
+ assert security.is_production() is False
+
+ def test_constant_time_equals_rejects_none(self):
+ assert security.constant_time_equals(None, "x") is False
+ assert security.constant_time_equals("x", None) is False
+ assert security.constant_time_equals(None, None) is False
+
+ def test_constant_time_equals_matches_identical_secrets(self):
+ assert security.constant_time_equals("s3cret", "s3cret") is True
+ assert security.constant_time_equals("s3cret", "s3crey") is False
+
+ def test_cors_never_uses_wildcard(self, dev_env):
+ """A wildcard origin combined with credentials lets any site read the API."""
+ assert "*" not in security.get_cors_origins()
+
+ def test_cors_origins_are_configurable(self, monkeypatch):
+ monkeypatch.setenv("FLOWYML_CORS_ORIGINS", "https://a.example, https://b.example")
+ assert security.get_cors_origins() == ["https://a.example", "https://b.example"]
+
+ def test_public_prefix_cannot_be_widened(self):
+ """`/assets` must not act as a prefix for an unrelated route."""
+ assert security.is_public_path("/assets/index.js") is True
+ assert security.is_public_path("/assets-internal/secrets") is False
+
+ def test_api_assets_route_is_not_public(self):
+ assert security.is_public_path("/api/assets/") is False
+
+
+class TestProductionValidation:
+ def test_production_without_credentials_is_rejected(self, production_env):
+ problems = security.security_misconfigurations()
+ assert len(problems) == 2
+ assert any("FLOWYML_API_TOKEN" in p for p in problems)
+ assert any("FLOWYML_ADMIN_PASSWORD" in p for p in problems)
+
+ with pytest.raises(RuntimeError, match="refuses to start"):
+ security.assert_production_security()
+
+ def test_default_password_is_rejected_in_production(self, production_env, monkeypatch):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "a-real-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", security.INSECURE_DEFAULT_PASSWORD)
+
+ problems = security.security_misconfigurations()
+ assert len(problems) == 1
+ assert "default" in problems[0]
+
+ def test_fully_configured_production_passes(self, production_env, monkeypatch):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "a-real-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "a-real-password")
+
+ assert security.security_misconfigurations() == []
+ security.assert_production_security() # must not raise
+
+ def test_explicit_opt_out_is_honoured(self, production_env, monkeypatch):
+ """Operators terminating auth at a proxy can opt out deliberately."""
+ monkeypatch.setenv("FLOWYML_ALLOW_INSECURE", "1")
+ assert security.security_misconfigurations() == []
+ security.assert_production_security()
+
+ def test_development_is_never_blocked(self, dev_env):
+ assert security.security_misconfigurations() == []
+ security.assert_production_security()
+
+
+# ---------------------------------------------------------------------------
+# middleware
+# ---------------------------------------------------------------------------
+
+
+class TestAuthMiddleware:
+ def test_development_allows_unauthenticated_requests(self, dev_env, app):
+ with TestClient(app) as client:
+ assert client.get("/api/health").status_code == 200
+ assert client.get("/api/runs/").status_code == 200
+
+ def test_production_without_token_refuses_to_start(self, production_env, app):
+ """The lifespan handler must abort startup rather than serve openly."""
+ with pytest.raises(RuntimeError, match="refuses to start"):
+ with TestClient(app):
+ pass
+
+ def test_production_without_token_denies_requests(self, production_env, app):
+ """Second line of defence when the lifespan handler never ran.
+
+ An ASGI host that imports ``app`` and serves it without running
+ lifespan events would previously have exposed everything.
+ """
+ client = TestClient(app) # not used as a context manager => no lifespan
+ response = client.get("/api/runs/")
+ assert response.status_code == 503
+ assert "FLOWYML_API_TOKEN" in response.json()["message"]
+
+ def test_production_rejects_missing_token(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ response = client.get("/api/runs/")
+ assert response.status_code == 401
+ assert response.headers["WWW-Authenticate"] == "Bearer"
+
+ def test_production_rejects_wrong_token(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ response = client.get(
+ "/api/runs/",
+ headers={"Authorization": "Bearer wrong-token"},
+ )
+ assert response.status_code == 401
+
+ def test_production_accepts_valid_bearer_token(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ response = client.get(
+ "/api/runs/",
+ headers={"Authorization": "Bearer secret-token"},
+ )
+ assert response.status_code == 200
+
+ def test_production_accepts_session_cookie(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ client.cookies.set("access_token", "Bearer secret-token")
+ assert client.get("/api/runs/").status_code == 200
+
+ def test_query_string_token_is_not_accepted(self, production_env, monkeypatch, app):
+ """Tokens in URLs leak via access logs, history and Referer headers."""
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ response = client.get("/api/runs/?token=secret-token")
+ assert response.status_code == 401
+
+ def test_health_stays_public_in_production(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ assert client.get("/api/health").status_code == 200
+
+ def test_malformed_authorization_header_is_rejected(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "pw")
+
+ with TestClient(app) as client:
+ for header in ("secret-token", "Basic secret-token", "Bearer"):
+ response = client.get("/api/runs/", headers={"Authorization": header})
+ assert response.status_code == 401, header
+
+
+# ---------------------------------------------------------------------------
+# login
+# ---------------------------------------------------------------------------
+
+
+class TestLogin:
+ def test_production_login_refuses_without_configured_password(
+ self,
+ production_env,
+ monkeypatch,
+ app,
+ ):
+ """Must never accept the publicly documented default in production."""
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+
+ client = TestClient(app) # no lifespan: exercise the endpoint guard
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "admin", "password": security.INSECURE_DEFAULT_PASSWORD},
+ )
+ assert response.status_code == 503
+ assert "FLOWYML_ADMIN_PASSWORD" in response.json()["detail"]
+
+ def test_production_login_never_issues_placeholder_token(
+ self,
+ production_env,
+ monkeypatch,
+ app,
+ ):
+ """The old code handed out the guessable string 'dev-token-placeholder'."""
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "correct-horse")
+
+ client = TestClient(app)
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "admin", "password": "correct-horse"},
+ )
+ assert response.status_code == 503
+ assert "FLOWYML_API_TOKEN" in response.json()["detail"]
+
+ def test_successful_production_login_sets_secure_cookie(
+ self,
+ production_env,
+ monkeypatch,
+ app,
+ ):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "correct-horse")
+
+ with TestClient(app) as client:
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "admin", "password": "correct-horse"},
+ )
+ assert response.status_code == 200
+ assert response.json()["access_token"] == "secret-token"
+
+ cookie_header = response.headers["set-cookie"]
+ assert "HttpOnly" in cookie_header
+ assert "Secure" in cookie_header
+
+ def test_wrong_password_is_rejected(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "correct-horse")
+
+ with TestClient(app) as client:
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "admin", "password": "wrong"},
+ )
+ assert response.status_code == 401
+
+ def test_wrong_username_is_rejected(self, production_env, monkeypatch, app):
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "correct-horse")
+
+ with TestClient(app) as client:
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "intruder", "password": "correct-horse"},
+ )
+ assert response.status_code == 401
+
+ def test_development_login_uses_default_password(self, dev_env, app):
+ """Local development stays zero-config."""
+ with TestClient(app) as client:
+ response = client.post(
+ "/api/auth/login",
+ json={"username": "admin", "password": security.INSECURE_DEFAULT_PASSWORD},
+ )
+ assert response.status_code == 200
+
+ def test_logout_clears_the_cookie(self, dev_env, app):
+ with TestClient(app) as client:
+ response = client.post("/api/auth/logout")
+ assert response.status_code == 200
+ assert 'access_token=""' in response.headers["set-cookie"]
diff --git a/tests/test_execution_endpoints.py b/tests/test_execution_endpoints.py
new file mode 100644
index 00000000..63fb4557
--- /dev/null
+++ b/tests/test_execution_endpoints.py
@@ -0,0 +1,178 @@
+"""Functional tests for the token and server-info endpoints the UI depends on.
+
+The API Tokens page's "Revoke" button used to call an endpoint that did not
+exist, and never checked the response, so the UI reported success while the
+token stayed valid. The Settings page's system-information card used to call a
+missing endpoint too, so it always rendered its hard-coded fallbacks: version
+"0.1.0" and database "PostgreSQL", regardless of the real values.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+from fastapi.testclient import TestClient # noqa: E402
+
+from flowyml.ui.backend.auth import TokenManager # noqa: E402
+
+
+@pytest.fixture
+def client(monkeypatch, tmp_path):
+ """A dev-mode client backed by an isolated token store."""
+ monkeypatch.delenv("FLOWYML_ENV", raising=False)
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+
+ import flowyml.ui.backend.auth as auth_module
+
+ monkeypatch.setattr(
+ auth_module,
+ "token_manager",
+ TokenManager(tokens_file=str(tmp_path / "tokens.json")),
+ )
+
+ from flowyml.ui.backend.main import app
+
+ with TestClient(app) as test_client:
+ yield test_client
+
+
+@pytest.fixture
+def token_manager():
+ from flowyml.ui.backend.auth import token_manager as tm
+
+ return tm
+
+
+class TestServerInfo:
+ def test_reports_the_real_package_version(self, client):
+ from flowyml import __version__
+
+ body = client.get("/api/execution/info").json()
+ assert body["version"] == __version__
+ # The fallback the UI used to display unconditionally.
+ assert body["version"] != "0.1.0"
+
+ def test_reports_the_real_database_backend(self, client):
+ body = client.get("/api/execution/info").json()
+ # Default deployment is SQLite, not the hard-coded "PostgreSQL".
+ assert body["database"] == "sqlite"
+
+ def test_reports_environment_and_uptime(self, client):
+ body = client.get("/api/execution/info").json()
+ assert body["environment"] == "development"
+ assert body["uptime_seconds"] >= 0
+ assert isinstance(body["uptime"], str) and body["uptime"]
+
+ def test_reports_production_environment(self, client, monkeypatch):
+ # A production deployment must also be authenticated, otherwise the
+ # middleware (correctly) refuses the request before it reaches here.
+ monkeypatch.setenv("FLOWYML_ENV", "production")
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+
+ response = client.get(
+ "/api/execution/info",
+ headers={"Authorization": "Bearer secret-token"},
+ )
+ assert response.status_code == 200
+ assert response.json()["environment"] == "production"
+
+
+class TestTokenListing:
+ def test_empty_store_lists_no_tokens(self, client):
+ assert client.get("/api/execution/tokens").json() == {"tokens": []}
+
+ def test_listing_never_reveals_token_values(self, client, token_manager):
+ token = token_manager.create_token(name="ci", permissions=["read"])
+
+ body = client.get("/api/execution/tokens").text
+ assert token not in body
+
+ def test_listing_exposes_a_stable_public_id(self, client, token_manager):
+ token_manager.create_token(name="ci")
+
+ tokens = client.get("/api/execution/tokens").json()["tokens"]
+ assert len(tokens) == 1
+ assert tokens[0]["id"]
+ # Stable across calls, so the UI can address a specific row.
+ assert tokens[0]["id"] == client.get("/api/execution/tokens").json()["tokens"][0]["id"]
+
+
+class TestTokenRevocation:
+ def test_revoking_by_id_invalidates_the_token(self, client, token_manager):
+ token = token_manager.create_token(name="leaked")
+ token_id = client.get("/api/execution/tokens").json()["tokens"][0]["id"]
+
+ response = client.delete(f"/api/execution/tokens/{token_id}")
+ assert response.status_code == 200
+ assert response.json()["revoked"] == 1
+
+ # The whole point: the credential must actually stop working.
+ assert token_manager.verify_token(token) is None
+ assert client.get("/api/execution/tokens").json()["tokens"] == []
+
+ def test_revoking_by_name_removes_every_match(self, client, token_manager):
+ """Names are user-supplied labels and need not be unique."""
+ first = token_manager.create_token(name="shared-label")
+ second = token_manager.create_token(name="shared-label")
+ keep = token_manager.create_token(name="other")
+
+ response = client.delete("/api/execution/tokens/shared-label")
+ assert response.status_code == 200
+ assert response.json()["revoked"] == 2
+
+ assert token_manager.verify_token(first) is None
+ assert token_manager.verify_token(second) is None
+ assert token_manager.verify_token(keep) is not None
+
+ def test_revoking_by_id_removes_only_that_token(self, client, token_manager):
+ token_manager.create_token(name="shared-label")
+ survivor = token_manager.create_token(name="shared-label")
+
+ listed = client.get("/api/execution/tokens").json()["tokens"]
+ survivor_id = next(
+ entry["id"]
+ for entry in listed
+ # identify the survivor by elimination: revoke the other one
+ if entry["id"] != listed[0]["id"]
+ )
+ victim_id = listed[0]["id"]
+
+ assert client.delete(f"/api/execution/tokens/{victim_id}").json()["revoked"] == 1
+
+ remaining = client.get("/api/execution/tokens").json()["tokens"]
+ assert [entry["id"] for entry in remaining] == [survivor_id]
+ assert token_manager.verify_token(survivor) is not None
+
+ def test_unknown_reference_returns_404(self, client):
+ response = client.delete("/api/execution/tokens/does-not-exist")
+ assert response.status_code == 404
+ assert "does-not-exist" in response.json()["detail"]
+
+ def test_names_needing_url_encoding_round_trip(self, client, token_manager):
+ token = token_manager.create_token(name="build token #1")
+
+ response = client.delete("/api/execution/tokens/build%20token%20%231")
+ assert response.status_code == 200
+ assert token_manager.verify_token(token) is None
+
+
+class TestTokenInitialisation:
+ def test_first_token_can_be_created_without_auth(self, client):
+ response = client.post("/api/execution/tokens/init")
+ assert response.status_code == 200
+ assert response.json()["token"].startswith("uf_")
+
+ def test_init_refuses_once_a_token_exists(self, client, token_manager):
+ token_manager.create_token(name="existing")
+
+ response = client.post("/api/execution/tokens/init")
+ assert response.status_code == 403
+
+ def test_created_token_has_admin_permission(self, client, token_manager):
+ token = client.post("/api/execution/tokens/init").json()["token"]
+
+ data = token_manager.verify_token(token)
+ assert data is not None
+ assert "admin" in data["permissions"]
diff --git a/tests/test_leaderboard.py b/tests/test_leaderboard.py
index e2112851..45e2a0ce 100644
--- a/tests/test_leaderboard.py
+++ b/tests/test_leaderboard.py
@@ -4,6 +4,8 @@
import unittest
import os
+import shutil
+import tempfile
from datetime import datetime
from flowyml.tracking.leaderboard import ModelLeaderboard, ModelScore, compare_runs
from flowyml.storage.metadata import SQLiteMetadataStore
@@ -11,14 +13,14 @@
class TestModelLeaderboard(unittest.TestCase):
def setUp(self):
- self.db_path = ".flowyml/test_leaderboard.db"
- if os.path.exists(self.db_path):
- os.remove(self.db_path)
+ # Private directory per test: a shared path under .flowyml/ races when
+ # tests run in parallel.
+ self._tmpdir = tempfile.mkdtemp(prefix="flowyml-leaderboard-")
+ self.db_path = os.path.join(self._tmpdir, "test_leaderboard.db")
self.metadata_store = SQLiteMetadataStore(self.db_path)
def tearDown(self):
- if os.path.exists(self.db_path):
- os.remove(self.db_path)
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
def test_add_score(self):
"""Test adding scores to leaderboard."""
@@ -115,14 +117,14 @@ def test_compare_models(self):
class TestCompareRuns(unittest.TestCase):
def setUp(self):
- self.db_path = ".flowyml/test_compare.db"
- if os.path.exists(self.db_path):
- os.remove(self.db_path)
+ # Private directory per test: a shared path under .flowyml/ races when
+ # tests run in parallel.
+ self._tmpdir = tempfile.mkdtemp(prefix="flowyml-leaderboard-")
+ self.db_path = os.path.join(self._tmpdir, "test_compare.db")
self.metadata_store = SQLiteMetadataStore(self.db_path)
def tearDown(self):
- if os.path.exists(self.db_path):
- os.remove(self.db_path)
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
def test_compare_runs(self):
"""Test comparing multiple runs."""
diff --git a/tests/test_scheduler_advanced.py b/tests/test_scheduler_advanced.py
index 2b66168b..c4e3835b 100644
--- a/tests/test_scheduler_advanced.py
+++ b/tests/test_scheduler_advanced.py
@@ -1,6 +1,8 @@
"""Tests for advanced scheduler features."""
import os
+import shutil
+import tempfile
import time
import unittest
from datetime import datetime, timedelta
@@ -31,12 +33,15 @@ class TestSchedulerPersistence(unittest.TestCase):
"""Test scheduler persistence."""
def setUp(self):
- self.db_path = "test_scheduler.db"
+ # A fixed filename in the working directory collides when tests run in
+ # parallel: one worker's tearDown deletes the database another worker
+ # is still using. Each test gets its own directory instead.
+ self._tmpdir = tempfile.mkdtemp(prefix="flowyml-scheduler-")
+ self.db_path = os.path.join(self._tmpdir, "test_scheduler.db")
self.persistence = SchedulerPersistence(self.db_path)
def tearDown(self):
- if os.path.exists(self.db_path):
- os.remove(self.db_path)
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
def test_save_and_load(self):
"""Test saving and loading schedules."""
@@ -81,13 +86,11 @@ class TestDistributedLock(unittest.TestCase):
"""Test distributed locking."""
def setUp(self):
- self.lock_file = ".lock_test_lock"
- if os.path.exists(self.lock_file):
- os.remove(self.lock_file)
+ self._tmpdir = tempfile.mkdtemp(prefix="flowyml-lock-")
+ self.lock_file = os.path.join(self._tmpdir, ".lock_test_lock")
def tearDown(self):
- if os.path.exists(self.lock_file):
- os.remove(self.lock_file)
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
def test_file_lock(self):
"""Test file-based locking."""
@@ -198,12 +201,10 @@ def test_metrics(self):
def test_execution_history(self):
"""Test execution history persistence."""
- # Create scheduler with persistence enabled
- db_path = "test_history.db"
-
- # Clean up any existing database
- if os.path.exists(db_path):
- os.remove(db_path)
+ # Create scheduler with persistence enabled, in a private directory so
+ # parallel workers cannot delete each other's database.
+ tmpdir = tempfile.mkdtemp(prefix="flowyml-history-")
+ db_path = os.path.join(tmpdir, "test_history.db")
config = SchedulerConfig(
persist_schedules=True,
@@ -232,5 +233,4 @@ def test_execution_history(self):
finally:
scheduler.stop()
scheduler.clear()
- if os.path.exists(db_path):
- os.remove(db_path)
+ shutil.rmtree(tmpdir, ignore_errors=True)
From eb305fa8e3e5d6763111c6226869ebbcb509c9fd Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:25:18 +0000
Subject: [PATCH 03/23] fix(assets): confine artifact file access to the
artifacts directory
The artifact endpoints turned a stored metadata `path` into a filesystem
operation without checking where that path pointed, and the path was
client-controlled from two directions:
- POST /api/assets/ persisted its free-form `metadata` dict verbatim, so a
client could store {"path": "/etc/hostname"} and read the file back through
GET /{id}/download or /{id}/content. Verified against a running server: the
download returned the file's contents.
- The upload handler assembled its destination from `project`, `run_id`,
`artifact_id` and the uploaded `filename`, all client-supplied. A ".."
component escaped the artifacts directory, and an absolute component
discarded it entirely, since Path("/base") / "/etc/passwd" is "/etc/passwd".
- DELETE /api/assets/{id} passed the same unchecked path to the artifact
store's delete, removing whatever file it named.
Together those were arbitrary file read, write and delete against the host.
Adds flowyml/ui/backend/artifact_paths.py: resolve_within_root() resolves a
candidate against the artifacts root and refuses anything landing outside it,
resolving symlinks first so a planted link cannot point out; sanitize_path_
segment() reduces a client-supplied name to one safe component; and
strip_reserved_metadata() drops server-owned keys from user metadata. Every
download, inline-content, upload and delete handler now goes through them.
Out-of-bounds reads answer 404 rather than 403 so the caller cannot probe for
the existence of files outside the root.
Fixes a functional bug in the same code: download resolved a relative path
against the server's working directory while inline-content resolved it
against the artifacts directory, so downloading a relative-path artifact 404'd
even though viewing it worked. Both now agree.
Adds tests/test_artifact_path_security.py: 28 cases covering the confinement
primitives, all three exploits end-to-end through the API, and the legitimate
upload/download/delete round trip that must keep working.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/ui/backend/artifact_paths.py | 86 +++++++++
flowyml/ui/backend/routers/assets.py | 96 +++++++---
tests/test_artifact_path_security.py | 263 +++++++++++++++++++++++++++
3 files changed, 415 insertions(+), 30 deletions(-)
create mode 100644 flowyml/ui/backend/artifact_paths.py
create mode 100644 tests/test_artifact_path_security.py
diff --git a/flowyml/ui/backend/artifact_paths.py b/flowyml/ui/backend/artifact_paths.py
new file mode 100644
index 00000000..a7ab84ce
--- /dev/null
+++ b/flowyml/ui/backend/artifact_paths.py
@@ -0,0 +1,86 @@
+"""Confine artifact file access to the configured artifacts directory.
+
+Artifact metadata records a ``path`` telling the API where the bytes live. That
+value reaches the filesystem through download, inline-content, delete and
+upload handlers, so anything able to influence it can steer those operations at
+an arbitrary location:
+
+* ``POST /api/assets/`` accepts a free-form ``metadata`` dict that is persisted
+ verbatim, so a client could store ``{"path": "/etc/hostname"}`` and then read
+ that file back through ``GET /api/assets/{id}/download``.
+* The upload handler builds its destination from ``project``, ``run_id``,
+ ``artifact_id`` and the uploaded ``filename`` — all client-supplied — so a
+ ``..`` component escaped the artifacts directory, and an absolute component
+ discarded it entirely (``Path("/base") / "/etc/passwd"`` is ``/etc/passwd``).
+
+Every filesystem access therefore goes through :func:`resolve_within_root`,
+which resolves symlinks and refuses any result outside the root.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+
+#: Metadata keys the API computes itself. A client that supplies them is
+#: overriding server state, so they are dropped from user-provided metadata.
+RESERVED_METADATA_KEYS: frozenset[str] = frozenset({"path"})
+
+#: Characters that are unsafe in a single path segment. The path separators are
+#: excluded so a segment can never introduce directory structure.
+_UNSAFE_SEGMENT_CHARS = re.compile(r"[^A-Za-z0-9._\-]")
+
+
+class ArtifactPathError(ValueError):
+ """Raised when a path would escape the artifacts root."""
+
+
+def sanitize_path_segment(segment: str, *, fallback: str = "unnamed") -> str:
+ """Reduce *segment* to a single, safe path component.
+
+ Strips directory separators, ``..`` and other unsafe characters so the
+ result can never introduce nesting or traversal when joined into a path.
+ """
+ if not segment:
+ return fallback
+
+ # Take only the final component: callers sometimes pass a browser-supplied
+ # filename, which on Windows clients can arrive as "C:\\path\\to\\file".
+ segment = segment.replace("\\", "/").rsplit("/", 1)[-1]
+
+ cleaned = _UNSAFE_SEGMENT_CHARS.sub("_", segment).strip("._")
+ return cleaned or fallback
+
+
+def resolve_within_root(candidate: str | os.PathLike[str], root: str | os.PathLike[str]) -> Path:
+ """Resolve *candidate* against *root* and guarantee it stays inside.
+
+ A relative candidate is joined to the root; an absolute one must already be
+ inside it. Symlinks are resolved before the check so a link planted inside
+ the root cannot point out of it.
+
+ Raises:
+ ArtifactPathError: if the resolved path escapes *root*.
+ """
+ root_path = Path(root).expanduser().resolve()
+ candidate_path = Path(candidate).expanduser()
+
+ combined = candidate_path if candidate_path.is_absolute() else root_path / candidate_path
+
+ # ``strict=False`` so a not-yet-created upload destination still resolves;
+ # existence is the caller's concern, containment is ours.
+ resolved = combined.resolve(strict=False)
+
+ if resolved != root_path and root_path not in resolved.parents:
+ raise ArtifactPathError(
+ f"Refusing to access '{candidate}': resolved path {resolved} is outside "
+ f"the artifacts directory {root_path}",
+ )
+
+ return resolved
+
+
+def strip_reserved_metadata(metadata: dict) -> dict:
+ """Return *metadata* without keys the server owns."""
+ return {key: value for key, value in metadata.items() if key not in RESERVED_METADATA_KEYS}
diff --git a/flowyml/ui/backend/routers/assets.py b/flowyml/ui/backend/routers/assets.py
index 0c233a37..88e82c6a 100644
--- a/flowyml/ui/backend/routers/assets.py
+++ b/flowyml/ui/backend/routers/assets.py
@@ -4,6 +4,12 @@
from flowyml.core.project import ProjectManager
from pathlib import Path
from flowyml.ui.backend.dependencies import get_store
+from flowyml.ui.backend.artifact_paths import (
+ ArtifactPathError,
+ resolve_within_root,
+ sanitize_path_segment,
+ strip_reserved_metadata,
+)
import shutil
import asyncio
import contextlib
@@ -11,6 +17,28 @@
router = APIRouter()
+def _artifacts_root() -> Path:
+ """The single directory every artifact file must live under."""
+ from flowyml.utils.config import get_config
+
+ return Path(get_config().artifacts_dir)
+
+
+def _resolve_artifact_file(artifact_path: str) -> Path:
+ """Resolve a stored artifact path, refusing anything outside the root.
+
+ Relative paths are interpreted against the artifacts directory, which is
+ also how they are written, so download and inline-content agree on where a
+ file lives.
+ """
+ try:
+ return resolve_within_root(artifact_path, _artifacts_root())
+ except ArtifactPathError as exc:
+ # 404 rather than 403: the caller must not learn whether the
+ # out-of-bounds file they aimed at exists.
+ raise HTTPException(status_code=404, detail="Artifact file not found") from exc
+
+
def _save_file_sync(src, dst):
with open(dst, "wb") as buffer:
shutil.copyfileobj(src, buffer)
@@ -118,8 +146,10 @@ async def create_asset(asset: AssetCreate):
try:
store = get_store()
- # Prepare metadata
- metadata = asset.metadata.copy()
+ # Prepare metadata. Keys the server owns (notably "path") are dropped:
+ # persisting a client-supplied path turned this endpoint into an
+ # arbitrary-file-read primitive via GET /{artifact_id}/download.
+ metadata = strip_reserved_metadata(asset.metadata)
metadata.update(
{
"name": asset.name,
@@ -160,20 +190,26 @@ async def upload_asset_content(artifact_id: str, file: UploadFile = File(...)):
config = get_config()
artifact_store = LocalArtifactStore(base_path=config.artifacts_dir)
- # Construct a path if not present
+ # Construct a path if not present. Every component here is
+ # client-supplied, so each is reduced to a single safe segment: a ".."
+ # escaped the artifacts directory and an absolute component replaced it
+ # outright (Path("/base") / "/etc/passwd" == Path("/etc/passwd")).
if not existing.get("path"):
# Create a path structure: project/run_id/artifact_id/filename
- project = existing.get("project", "default")
- run_id = existing.get("run_id", "unknown")
- filename = file.filename or "content"
- rel_path = f"{project}/{run_id}/{artifact_id}/{filename}"
+ project = sanitize_path_segment(existing.get("project") or "", fallback="default")
+ run_id = sanitize_path_segment(existing.get("run_id") or "", fallback="unknown")
+ safe_artifact_id = sanitize_path_segment(artifact_id, fallback="artifact")
+ filename = sanitize_path_segment(file.filename or "", fallback="content")
+ rel_path = f"{project}/{run_id}/{safe_artifact_id}/{filename}"
else:
rel_path = existing.get("path")
- # If path is absolute, make it relative to artifacts dir if possible, or just use it
- # But LocalArtifactStore expects relative paths usually, or handles absolute ones
- # Save the file
- full_path = artifact_store.base_path / rel_path
+ # Confine the destination to the artifacts directory before writing.
+ try:
+ full_path = resolve_within_root(rel_path, artifact_store.base_path)
+ except ArtifactPathError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
+
full_path.parent.mkdir(parents=True, exist_ok=True)
loop = asyncio.get_running_loop()
@@ -205,14 +241,18 @@ async def delete_asset(artifact_id: str):
# Delete file if it exists locally (since backend is local to itself)
path = asset.get("path")
if path:
- from flowyml.storage.artifacts import LocalArtifactStore
- from flowyml.utils.config import get_config
-
- config = get_config()
- artifact_store = LocalArtifactStore(base_path=config.artifacts_dir)
+ # Confine first: without this, a stored path such as
+ # "/etc/hostname" made this endpoint an arbitrary-file-delete
+ # primitive. An out-of-bounds path leaves the file alone and only
+ # the metadata record is removed.
+ try:
+ resolved = resolve_within_root(path, _artifacts_root())
+ except ArtifactPathError:
+ resolved = None
- with contextlib.suppress(Exception):
- artifact_store.delete(path)
+ if resolved is not None and resolved.is_file():
+ with contextlib.suppress(OSError):
+ resolved.unlink()
# Delete metadata
store.delete_artifact(artifact_id)
@@ -492,8 +532,12 @@ async def download_asset(artifact_id: str):
if not artifact_path:
raise HTTPException(status_code=404, detail="Artifact path not available")
- file_path = Path(artifact_path)
- if not file_path.exists():
+ # Relative paths are resolved against the artifacts directory, matching how
+ # uploads store them; previously this handler treated them as relative to
+ # the server's working directory, so downloads 404'd for files the inline
+ # content endpoint served fine.
+ file_path = _resolve_artifact_file(artifact_path)
+ if not file_path.is_file():
raise HTTPException(status_code=404, detail="Artifact file not found on disk")
return FileResponse(
@@ -516,16 +560,8 @@ async def get_asset_content(artifact_id: str):
if not artifact_path:
raise HTTPException(status_code=404, detail="Artifact path not available")
- # Handle relative paths for local store
- from flowyml.utils.config import get_config
-
- config = get_config()
-
- file_path = Path(artifact_path)
- if not file_path.is_absolute():
- file_path = config.artifacts_dir / file_path
-
- if not file_path.exists():
+ file_path = _resolve_artifact_file(artifact_path)
+ if not file_path.is_file():
raise HTTPException(status_code=404, detail="Artifact file not found on disk")
# Guess mime type
diff --git a/tests/test_artifact_path_security.py b/tests/test_artifact_path_security.py
new file mode 100644
index 00000000..f9eaeef8
--- /dev/null
+++ b/tests/test_artifact_path_security.py
@@ -0,0 +1,263 @@
+"""Path-traversal regression tests for the artifact endpoints.
+
+Artifact metadata records where an artifact's bytes live, and four handlers
+turn that value into a filesystem operation. Because ``POST /api/assets/``
+persisted a client's ``metadata`` dict verbatim, and the upload handler built
+its destination from client-supplied name components, that value was
+attacker-controlled — which made the API an arbitrary file read, write and
+delete primitive:
+
+* ``metadata={"path": "/etc/hostname"}`` then ``GET /{id}/download`` returned
+ the contents of ``/etc/hostname``.
+* ``metadata={"path": "/tmp/victim"}`` then ``DELETE /{id}`` removed that file.
+* ``run_id="../../.."`` or ``filename="../../../etc/cron.d/x"`` escaped the
+ artifacts directory on upload.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+from fastapi.testclient import TestClient # noqa: E402
+
+from flowyml.ui.backend.artifact_paths import ( # noqa: E402
+ ArtifactPathError,
+ resolve_within_root,
+ sanitize_path_segment,
+ strip_reserved_metadata,
+)
+
+
+# ---------------------------------------------------------------------------
+# the confinement primitives
+# ---------------------------------------------------------------------------
+
+
+class TestResolveWithinRoot:
+ def test_relative_path_resolves_under_root(self, tmp_path):
+ resolved = resolve_within_root("a/b/c.txt", tmp_path)
+ assert resolved == (tmp_path / "a" / "b" / "c.txt").resolve()
+
+ def test_parent_traversal_is_rejected(self, tmp_path):
+ with pytest.raises(ArtifactPathError):
+ resolve_within_root("../escape.txt", tmp_path)
+
+ def test_deep_traversal_is_rejected(self, tmp_path):
+ with pytest.raises(ArtifactPathError):
+ resolve_within_root("a/../../../../etc/passwd", tmp_path)
+
+ def test_absolute_path_outside_root_is_rejected(self, tmp_path):
+ """`Path("/base") / "/etc/passwd"` silently discards the base."""
+ with pytest.raises(ArtifactPathError):
+ resolve_within_root("/etc/passwd", tmp_path)
+
+ def test_absolute_path_inside_root_is_allowed(self, tmp_path):
+ target = tmp_path / "nested" / "file.txt"
+ assert resolve_within_root(str(target), tmp_path) == target.resolve()
+
+ def test_symlink_escaping_root_is_rejected(self, tmp_path):
+ outside = tmp_path.parent / "outside-secret.txt"
+ outside.write_text("secret")
+ root = tmp_path / "root"
+ root.mkdir()
+ (root / "link.txt").symlink_to(outside)
+
+ with pytest.raises(ArtifactPathError):
+ resolve_within_root("link.txt", root)
+
+ def test_nonexistent_path_still_resolves(self, tmp_path):
+ """Upload destinations do not exist yet; containment is the only check."""
+ assert resolve_within_root("not/created/yet.bin", tmp_path)
+
+ def test_root_itself_is_permitted(self, tmp_path):
+ assert resolve_within_root(".", tmp_path) == tmp_path.resolve()
+
+
+class TestSanitizePathSegment:
+ @pytest.mark.parametrize(
+ ("raw", "expected"),
+ [
+ ("normal.txt", "normal.txt"),
+ ("../../etc/passwd", "passwd"),
+ ("a/b/c.txt", "c.txt"),
+ (r"C:\Users\me\file.txt", "file.txt"),
+ ("..", "unnamed"),
+ ("", "unnamed"),
+ ("with space.txt", "with_space.txt"),
+ ("semi;colon", "semi_colon"),
+ ],
+ )
+ def test_reduces_to_one_safe_component(self, raw, expected):
+ assert sanitize_path_segment(raw) == expected
+
+ def test_result_never_introduces_nesting(self):
+ for raw in ("../../x", "a/b", "/abs/path", "..\\..\\win"):
+ assert "/" not in sanitize_path_segment(raw)
+ assert "\\" not in sanitize_path_segment(raw)
+
+ def test_custom_fallback_is_used(self):
+ assert sanitize_path_segment("", fallback="default") == "default"
+
+
+class TestStripReservedMetadata:
+ def test_path_is_removed(self):
+ assert strip_reserved_metadata({"path": "/etc/passwd", "rows": 10}) == {"rows": 10}
+
+ def test_other_keys_survive(self):
+ payload = {"rows": 10, "columns": ["a"], "nested": {"path": "kept"}}
+ assert strip_reserved_metadata(payload) == payload
+
+
+# ---------------------------------------------------------------------------
+# end-to-end through the API
+# ---------------------------------------------------------------------------
+
+
+@pytest.fixture
+def client(monkeypatch, tmp_path):
+ """API client whose artifact and metadata storage are isolated to tmp_path."""
+ monkeypatch.delenv("FLOWYML_ENV", raising=False)
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+
+ artifacts_dir = tmp_path / "artifacts"
+ artifacts_dir.mkdir()
+
+ from flowyml.utils import config as config_module
+
+ real_get_config = config_module.get_config
+
+ def _scoped_config(*args, **kwargs):
+ cfg = real_get_config(*args, **kwargs)
+ cfg.artifacts_dir = artifacts_dir
+ return cfg
+
+ monkeypatch.setattr(config_module, "get_config", _scoped_config)
+
+ from flowyml.storage.sql import SQLMetadataStore
+ from flowyml.ui.backend import dependencies
+
+ store = SQLMetadataStore(db_path=str(tmp_path / "meta.db"))
+ monkeypatch.setattr(dependencies, "_store", store)
+
+ from flowyml.ui.backend.main import app
+
+ with TestClient(app) as test_client:
+ test_client.artifacts_dir = artifacts_dir
+ test_client.outside_dir = tmp_path / "outside"
+ test_client.outside_dir.mkdir()
+ yield test_client
+
+
+def _create_asset(client, artifact_id, **overrides):
+ payload = {
+ "artifact_id": artifact_id,
+ "name": artifact_id,
+ "type": "data",
+ "run_id": "run-1",
+ "step": "step-1",
+ }
+ payload.update(overrides)
+ response = client.post("/api/assets/", json=payload)
+ assert response.status_code == 200, response.text
+ return response
+
+
+class TestArbitraryFileRead:
+ def test_client_supplied_path_is_not_persisted(self, client):
+ _create_asset(client, "poc-read", metadata={"path": "/etc/hostname"})
+
+ stored = client.get("/api/assets/poc-read").json()
+ assert stored.get("path") in (None, "")
+
+ def test_download_cannot_read_a_file_outside_the_root(self, client):
+ secret = client.outside_dir / "secret.txt"
+ secret.write_text("TOP SECRET")
+
+ _create_asset(client, "poc-read", metadata={"path": str(secret)})
+
+ response = client.get("/api/assets/poc-read/download")
+ assert response.status_code == 404
+ assert "TOP SECRET" not in response.text
+
+ def test_content_cannot_read_a_file_outside_the_root(self, client):
+ secret = client.outside_dir / "secret.txt"
+ secret.write_text("TOP SECRET")
+
+ _create_asset(client, "poc-read", metadata={"path": str(secret)})
+
+ response = client.get("/api/assets/poc-read/content")
+ assert response.status_code == 404
+ assert "TOP SECRET" not in response.text
+
+
+class TestArbitraryFileWrite:
+ def test_traversal_in_filename_stays_inside_the_root(self, client):
+ _create_asset(client, "poc-write")
+
+ response = client.post(
+ "/api/assets/poc-write/upload",
+ files={"file": ("../../../../escaped.txt", b"OWNED")},
+ )
+ assert response.status_code == 200
+
+ written = Path(client.artifacts_dir, response.json()["path"]).resolve()
+ assert client.artifacts_dir.resolve() in written.parents
+ assert not (client.outside_dir / "escaped.txt").exists()
+
+ def test_traversal_in_run_id_stays_inside_the_root(self, client):
+ _create_asset(client, "poc-write", run_id="../../../../tmp")
+
+ response = client.post(
+ "/api/assets/poc-write/upload",
+ files={"file": ("payload.txt", b"OWNED")},
+ )
+ assert response.status_code == 200
+
+ written = Path(client.artifacts_dir, response.json()["path"]).resolve()
+ assert client.artifacts_dir.resolve() in written.parents
+
+ def test_uploaded_content_round_trips(self, client):
+ """Hardening must not break the legitimate upload/download path."""
+ _create_asset(client, "good-asset")
+
+ upload = client.post(
+ "/api/assets/good-asset/upload",
+ files={"file": ("model.txt", b"weights")},
+ )
+ assert upload.status_code == 200
+
+ download = client.get("/api/assets/good-asset/download")
+ assert download.status_code == 200
+ assert download.content == b"weights"
+
+ content = client.get("/api/assets/good-asset/content")
+ assert content.status_code == 200
+ assert content.content == b"weights"
+
+
+class TestArbitraryFileDelete:
+ def test_delete_leaves_files_outside_the_root_alone(self, client):
+ victim = client.outside_dir / "victim.txt"
+ victim.write_text("important")
+
+ _create_asset(client, "poc-delete", metadata={"path": str(victim)})
+
+ response = client.delete("/api/assets/poc-delete")
+ assert response.status_code == 200
+ assert victim.exists(), "an out-of-bounds path must never be deleted"
+
+ def test_delete_still_removes_a_legitimate_artifact_file(self, client):
+ _create_asset(client, "real-asset")
+ upload = client.post(
+ "/api/assets/real-asset/upload",
+ files={"file": ("data.bin", b"payload")},
+ )
+ stored = Path(client.artifacts_dir, upload.json()["path"])
+ assert stored.exists()
+
+ assert client.delete("/api/assets/real-asset").status_code == 200
+ assert not stored.exists()
From 1d09f8f48e8d0d65cbf8c35b862ae815ce733330 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:30:51 +0000
Subject: [PATCH 04/23] fix(api): bound pagination inputs and stop leaking
internals in error responses
Resource exhaustion. Every list endpoint took an unbounded `limit`, so a single
`?limit=100000000` made the server materialise an arbitrarily large result set.
Asset lineage took an unbounded traversal `depth`, log reads an unbounded
`lines`, and step-log reads a negative `offset`. All are now declared with
explicit Query bounds, matching the pattern metrics.py already used, so an
out-of-range value is rejected as a 422 before any work starts.
Information disclosure. 46 handlers raise HTTPException(500, detail=str(e)).
For a SQLAlchemy failure that string carries the failing statement and
connection details; for a filesystem failure it carries absolute server paths.
The global handler echoed unhandled exception text too, with a comment
conceding "in prod maybe hide this". Rather than editing 46 call sites, a
single exception handler now redacts 5xx detail in production and replaces it
with a short reference id that ties the response to the full server-side log
entry. Development keeps the detail, where the operator is the developer, and
4xx messages are untouched since those are written for the caller. The
middleware's misconfiguration 503 no longer names the missing environment
variable either: that told an anonymous client the instance was running
unauthenticated. The remedy is logged instead.
Also replaces the SPA fallback, which was implemented by overriding the 404
exception handler, with a trailing catch-all route. The old approach turned
*every* unmatched path into the SPA shell, so a typo in a fetch URL returned
HTML with a 200-shaped body instead of a JSON 404, and the handler only existed
when a frontend build was present. Unknown /api and /ws paths now always answer
JSON 404s.
Adds tests/test_backend_error_handling.py: 28 cases covering redaction in
production, detail retention in development, log-correlation references, JSON
404s for unknown API paths, and the pagination bounds on every list endpoint
including the valid requests that must keep working.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/ui/backend/main.py | 106 +++++++---
flowyml/ui/backend/routers/ai_context.py | 5 +-
flowyml/ui/backend/routers/assets.py | 17 +-
flowyml/ui/backend/routers/deployments.py | 8 +-
flowyml/ui/backend/routers/evaluations.py | 7 +-
flowyml/ui/backend/routers/model_explorer.py | 11 +-
flowyml/ui/backend/routers/pipelines.py | 17 +-
flowyml/ui/backend/routers/projects.py | 13 +-
flowyml/ui/backend/routers/runs.py | 11 +-
flowyml/ui/backend/routers/traces.py | 14 +-
tests/test_backend_error_handling.py | 192 +++++++++++++++++++
tests/test_backend_security.py | 16 +-
12 files changed, 363 insertions(+), 54 deletions(-)
create mode 100644 tests/test_backend_error_handling.py
diff --git a/flowyml/ui/backend/main.py b/flowyml/ui/backend/main.py
index db9f12d9..919d483b 100644
--- a/flowyml/ui/backend/main.py
+++ b/flowyml/ui/backend/main.py
@@ -5,8 +5,13 @@
from fastapi.exceptions import RequestValidationError
import os
import traceback
+import uuid
from contextlib import asynccontextmanager
+from loguru import logger
+from starlette.exceptions import HTTPException as StarletteHTTPException
+from fastapi.exception_handlers import http_exception_handler
+
from flowyml import __version__
from flowyml.monitoring.alerts import alert_manager, AlertLevel
from flowyml.ui.backend.security import (
@@ -166,15 +171,20 @@ async def dispatch(self, request: Request, call_next):
# Fail closed. Previously this branch let the request through,
# which left the whole control plane - including remote pipeline
# execution - open to anyone who could reach the port.
+ #
+ # The specifics go to the log, not to the caller: telling an
+ # anonymous client which environment variable is missing confirms
+ # for an attacker that the instance is unauthenticated.
+ logger.error(
+ "Refusing request: FLOWYML_ENV=production but FLOWYML_API_TOKEN is "
+ "not set, so no request can be authenticated. Set FLOWYML_API_TOKEN, "
+ "or set FLOWYML_ALLOW_INSECURE=1 if a proxy enforces authentication.",
+ )
return JSONResponse(
status_code=503,
content={
"error": "Service Unavailable",
- "message": (
- "Server is misconfigured: FLOWYML_ENV=production but "
- "FLOWYML_API_TOKEN is not set, so no request can be "
- "authenticated."
- ),
+ "message": "Server is misconfigured and cannot serve requests.",
},
)
@@ -266,16 +276,18 @@ async def serve_root():
# Actually, let's try a different approach - use a custom middleware or exceptions
# The trick is to let FastAPI handle routes first, then catch 404s
- from starlette.exceptions import HTTPException as StarletteHTTPException
- from fastapi.exception_handlers import http_exception_handler
-
- @app.exception_handler(StarletteHTTPException)
- async def custom_http_exception_handler(request, exc):
- # If it's a 404 and not an API route, serve the SPA
- if exc.status_code == 404 and not request.url.path.startswith("/api"):
- return FileResponse(os.path.join(frontend_dist, "index.html"))
- # Otherwise, use the default handler
- return await http_exception_handler(request, exc)
+ @app.get("/{spa_path:path}", include_in_schema=False)
+ async def serve_spa(spa_path: str):
+ """Serve the SPA shell for client-side routes.
+
+ Registered last, so every API route and static mount takes precedence.
+ Unmatched /api paths must still 404 as JSON rather than silently
+ returning HTML, which would make a typo in a fetch URL look like a
+ successful response the client then fails to parse.
+ """
+ if spa_path.startswith(("api/", "ws/")):
+ raise StarletteHTTPException(status_code=404, detail="Not Found")
+ return FileResponse(os.path.join(frontend_dist, "index.html"))
else:
@@ -287,27 +299,71 @@ async def root():
}
+def _error_reference() -> str:
+ """A short id tying a client's error response to a server log entry."""
+ return uuid.uuid4().hex[:12]
+
+
+@app.exception_handler(StarletteHTTPException)
+async def http_exception_handler_with_redaction(request, exc):
+ """Keep 4xx details, redact 5xx details in production.
+
+ Handlers across the API raise ``HTTPException(500, detail=str(e))``. In
+ production that text reaches the client verbatim, and for SQLAlchemy errors
+ it carries the failing SQL statement and connection details, while file
+ errors carry absolute server paths. The real message is always logged; only
+ what crosses the wire is reduced.
+ """
+ if exc.status_code >= 500:
+ reference = _error_reference()
+ logger.error(
+ f"[{reference}] {request.method} {request.url.path} -> {exc.status_code}: "
+ f"{exc.detail}",
+ )
+ if is_production():
+ return JSONResponse(
+ status_code=exc.status_code,
+ content={
+ "error": "Internal Server Error",
+ "message": "The server failed to handle this request.",
+ "reference": reference,
+ },
+ headers=getattr(exc, "headers", None) or {},
+ )
+
+ return await http_exception_handler(request, exc)
+
+
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
error_msg = str(exc)
stack_trace = traceback.format_exc()
+ reference = _error_reference()
+
+ # Always record the full failure server-side, whatever we return.
+ logger.error(
+ f"[{reference}] Unhandled exception in {request.method} {request.url.path}: "
+ f"{error_msg}\n{stack_trace}",
+ )
- # Log and alert
alert_manager.send_alert(
title="Backend API Error",
message=f"Unhandled exception in {request.method} {request.url.path}: {error_msg}",
level=AlertLevel.ERROR,
- metadata={"traceback": stack_trace, "path": request.url.path},
+ metadata={"traceback": stack_trace, "path": request.url.path, "reference": reference},
)
- return JSONResponse(
- status_code=500,
- content={
- "error": "Internal Server Error",
- "message": "Something went wrong on our end. We've been notified.",
- "detail": error_msg, # In prod maybe hide this, but for now it's useful
- },
- )
+ content = {
+ "error": "Internal Server Error",
+ "message": "Something went wrong on our end. We've been notified.",
+ "reference": reference,
+ }
+ # Exception text can carry SQL, credentials and server paths, so it is only
+ # echoed outside production where the operator is the developer.
+ if not is_production():
+ content["detail"] = error_msg
+
+ return JSONResponse(status_code=500, content=content)
@app.exception_handler(RequestValidationError)
diff --git a/flowyml/ui/backend/routers/ai_context.py b/flowyml/ui/backend/routers/ai_context.py
index 264920ef..71e9ae97 100644
--- a/flowyml/ui/backend/routers/ai_context.py
+++ b/flowyml/ui/backend/routers/ai_context.py
@@ -6,7 +6,7 @@
"""
from fastapi import APIRouter, HTTPException
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
from ..dependencies import get_store
import json
from flowyml.utils.config import get_config
@@ -26,7 +26,8 @@ class AIContextRequest(BaseModel):
include_logs: bool = True
include_code: bool = True
include_metrics: bool = True
- max_log_lines: int = 100
+ # Bounded: this drives how much log text is read into memory and returned.
+ max_log_lines: int = Field(default=100, ge=1, le=5000)
class AIContextResponse(BaseModel):
diff --git a/flowyml/ui/backend/routers/assets.py b/flowyml/ui/backend/routers/assets.py
index 88e82c6a..5f0058c7 100644
--- a/flowyml/ui/backend/routers/assets.py
+++ b/flowyml/ui/backend/routers/assets.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, HTTPException, UploadFile, File
+from fastapi import APIRouter, HTTPException, UploadFile, File, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from flowyml.core.project import ProjectManager
@@ -16,6 +16,11 @@
router = APIRouter()
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
def _artifacts_root() -> Path:
"""The single directory every artifact file must live under."""
@@ -76,7 +81,7 @@ def _dedupe_assets(assets):
@router.get("/")
async def list_assets(
- limit: int = 50,
+ limit: int = Query(50, ge=1, le=MAX_PAGE_SIZE),
asset_type: str = None,
run_id: str = None,
project: str = None,
@@ -321,7 +326,11 @@ async def get_asset_stats(project: str | None = None):
@router.get("/search")
-async def search_assets(q: str, limit: int = 50, project: str | None = None):
+async def search_assets(
+ q: str = Query(..., min_length=1, max_length=200),
+ limit: int = Query(50, ge=1, le=MAX_PAGE_SIZE),
+ project: str | None = None,
+):
"""Search assets by name or properties."""
try:
combined_assets = []
@@ -364,7 +373,7 @@ async def search_assets(q: str, limit: int = 50, project: str | None = None):
async def get_asset_lineage(
asset_id: str | None = None,
project: str | None = None,
- depth: int = 3,
+ depth: int = Query(3, ge=1, le=10),
):
"""
Get lineage graph for artifacts.
diff --git a/flowyml/ui/backend/routers/deployments.py b/flowyml/ui/backend/routers/deployments.py
index ce6adc4a..e8e85947 100644
--- a/flowyml/ui/backend/routers/deployments.py
+++ b/flowyml/ui/backend/routers/deployments.py
@@ -1,6 +1,6 @@
"""Deployment management API for model serving."""
-from fastapi import APIRouter, HTTPException, BackgroundTasks
+from fastapi import APIRouter, HTTPException, BackgroundTasks, Query
from pydantic import BaseModel, Field
from datetime import datetime
from uuid import uuid4
@@ -8,6 +8,10 @@
router = APIRouter(prefix="/deployments", tags=["deployments"])
+#: Upper bound on how many log lines a single request may pull back.
+MAX_LOG_LINES = 10000
+
+
# ==================== Schemas ====================
@@ -321,7 +325,7 @@ async def stop_deployment(
@router.get("/{deployment_id}/logs")
async def get_deployment_logs(
deployment_id: str,
- lines: int = 100,
+ lines: int = Query(100, ge=1, le=MAX_LOG_LINES),
) -> dict:
"""Get deployment logs."""
if deployment_id not in _deployments:
diff --git a/flowyml/ui/backend/routers/evaluations.py b/flowyml/ui/backend/routers/evaluations.py
index 4449fc6e..ecec553d 100644
--- a/flowyml/ui/backend/routers/evaluations.py
+++ b/flowyml/ui/backend/routers/evaluations.py
@@ -13,6 +13,11 @@
router = APIRouter(prefix="/evaluations", tags=["evaluations"])
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
# ─── Request/Response Models ─────────────────────────────────────────
@@ -143,7 +148,7 @@ async def get_eval_result(eval_id: str):
@router.get("/list")
async def list_evaluations(
experiment: str | None = Query(None, description="Filter by experiment"),
- limit: int = Query(20, description="Max results"),
+ limit: int = Query(20, ge=1, le=MAX_PAGE_SIZE, description="Max results"),
):
"""List recent evaluation runs."""
try:
diff --git a/flowyml/ui/backend/routers/model_explorer.py b/flowyml/ui/backend/routers/model_explorer.py
index 373e9a5d..68c0a673 100644
--- a/flowyml/ui/backend/routers/model_explorer.py
+++ b/flowyml/ui/backend/routers/model_explorer.py
@@ -1,6 +1,6 @@
"""Model Explorer API for interactive model testing."""
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import Any
from datetime import datetime
@@ -8,6 +8,10 @@
router = APIRouter(prefix="/explorer", tags=["model-explorer"])
+#: Upper bound on how many log lines a single request may pull back.
+MAX_LOG_LINES = 10000
+
+
# ==================== Schemas ====================
@@ -552,7 +556,10 @@ async def get_model_info(deployment_id: str) -> dict:
@router.get("/logs/{deployment_id}")
-async def get_deployment_logs(deployment_id: str, lines: int = 100) -> dict:
+async def get_deployment_logs(
+ deployment_id: str,
+ lines: int = Query(100, ge=1, le=MAX_LOG_LINES),
+) -> dict:
"""Get logs from a deployed model server.
Returns recent log entries for debugging and monitoring.
diff --git a/flowyml/ui/backend/routers/pipelines.py b/flowyml/ui/backend/routers/pipelines.py
index 5db54dc0..e78b6117 100644
--- a/flowyml/ui/backend/routers/pipelines.py
+++ b/flowyml/ui/backend/routers/pipelines.py
@@ -1,10 +1,15 @@
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from flowyml.ui.backend.dependencies import get_store
from flowyml.core.project import ProjectManager
router = APIRouter()
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
def _iter_metadata_stores():
"""Yield tuples of (project_name, store) including global and project stores."""
@@ -24,7 +29,10 @@ def _iter_metadata_stores():
@router.get("/")
-async def list_pipelines(project: str | None = None, limit: int = 100):
+async def list_pipelines(
+ project: str | None = None,
+ limit: int = Query(100, ge=1, le=MAX_PAGE_SIZE),
+):
"""List all unique pipelines with details, optionally filtered by project."""
try:
pipeline_map = {} # pipeline_name -> data
@@ -74,7 +82,10 @@ async def list_pipelines(project: str | None = None, limit: int = 100):
@router.get("/{pipeline_name}/runs")
-async def list_pipeline_runs(pipeline_name: str, limit: int = 10):
+async def list_pipeline_runs(
+ pipeline_name: str,
+ limit: int = Query(10, ge=1, le=MAX_PAGE_SIZE),
+):
"""List runs for a specific pipeline."""
store = get_store()
runs = store.query(pipeline_name=pipeline_name)
diff --git a/flowyml/ui/backend/routers/projects.py b/flowyml/ui/backend/routers/projects.py
index da71bffb..15602165 100644
--- a/flowyml/ui/backend/routers/projects.py
+++ b/flowyml/ui/backend/routers/projects.py
@@ -1,10 +1,15 @@
-from fastapi import APIRouter, HTTPException, Depends
+from fastapi import APIRouter, HTTPException, Depends, Query
from flowyml.core.project import ProjectManager
from flowyml.utils.config import get_config
from pydantic import BaseModel
router = APIRouter()
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
def get_projects_manager() -> ProjectManager:
"""Instantiate a ProjectManager bound to the current config."""
@@ -93,7 +98,7 @@ async def get_project(project_name: str, manager: ProjectManager = Depends(get_p
async def get_project_runs(
project_name: str,
pipeline_name: str | None = None,
- limit: int = 100,
+ limit: int = Query(100, ge=1, le=MAX_PAGE_SIZE),
manager: ProjectManager = Depends(get_projects_manager),
):
"""Get runs for a project."""
@@ -109,7 +114,7 @@ async def get_project_runs(
async def get_project_artifacts(
project_name: str,
artifact_type: str | None = None,
- limit: int = 100,
+ limit: int = Query(100, ge=1, le=MAX_PAGE_SIZE),
manager: ProjectManager = Depends(get_projects_manager),
):
"""Get artifacts for a project."""
@@ -125,7 +130,7 @@ async def get_project_artifacts(
async def get_project_metrics(
project_name: str,
model_name: str | None = None,
- limit: int = 100,
+ limit: int = Query(100, ge=1, le=MAX_PAGE_SIZE),
manager: ProjectManager = Depends(get_projects_manager),
):
"""Get logged metrics for a project (from model_metrics table and Metrics artifacts)."""
diff --git a/flowyml/ui/backend/routers/runs.py b/flowyml/ui/backend/routers/runs.py
index e64e1140..317e27e9 100644
--- a/flowyml/ui/backend/routers/runs.py
+++ b/flowyml/ui/backend/routers/runs.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from flowyml.storage.metadata import SQLiteMetadataStore
from flowyml.core.project import ProjectManager
@@ -7,6 +7,11 @@
router = APIRouter()
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
def _iter_metadata_stores():
"""Yield tuples of (project_name, store) including global and project stores."""
@@ -47,7 +52,7 @@ def sort_key(run):
@router.get("/")
async def list_runs(
- limit: int = 20,
+ limit: int = Query(20, ge=1, le=MAX_PAGE_SIZE),
project: str | None = None,
pipeline_name: str | None = None,
status: str | None = None,
@@ -869,7 +874,7 @@ def write_log():
@router.get("/{run_id}/steps/{step_name}/logs")
-async def get_step_logs(run_id: str, step_name: str, offset: int = 0):
+async def get_step_logs(run_id: str, step_name: str, offset: int = Query(0, ge=0)):
"""Get logs for a specific step.
For local runs, reads from the local log file.
diff --git a/flowyml/ui/backend/routers/traces.py b/flowyml/ui/backend/routers/traces.py
index cef5c3ea..a6cd715c 100644
--- a/flowyml/ui/backend/routers/traces.py
+++ b/flowyml/ui/backend/routers/traces.py
@@ -1,13 +1,18 @@
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from flowyml.ui.backend.dependencies import get_store
router = APIRouter()
+#: Upper bound on any client-supplied page size. Without it a request such as
+#: `?limit=100000000` makes the server materialise an unbounded result set.
+MAX_PAGE_SIZE = 1000
+
+
@router.get("/")
async def list_traces(
- limit: int = 50,
+ limit: int = Query(50, ge=1, le=MAX_PAGE_SIZE),
trace_id: str | None = None,
event_type: str | None = None,
project: str | None = None,
@@ -73,7 +78,10 @@ async def trace_stats(project: str | None = None):
@router.get("/sessions")
-async def list_sessions(limit: int = 50, project: str | None = None):
+async def list_sessions(
+ limit: int = Query(50, ge=1, le=MAX_PAGE_SIZE),
+ project: str | None = None,
+):
"""List session-level traces (aggregated multi-turn sessions)."""
store = get_store()
# Sessions are stored as event_type='session' or 'genai_session'
diff --git a/tests/test_backend_error_handling.py b/tests/test_backend_error_handling.py
new file mode 100644
index 00000000..7b353929
--- /dev/null
+++ b/tests/test_backend_error_handling.py
@@ -0,0 +1,192 @@
+"""Error-response and input-bound tests for the UI backend.
+
+Two production concerns are covered.
+
+*Information disclosure.* Handlers across the API raise
+``HTTPException(500, detail=str(e))``. For a SQLAlchemy failure that string
+carries the failing statement and connection details; for a filesystem failure
+it carries absolute server paths. Redacting centrally covers every call site
+at once, and keeps full detail in development where the operator is the
+developer.
+
+*Resource exhaustion.* List endpoints took an unbounded ``limit``, so a single
+``?limit=100000000`` made the server materialise an arbitrarily large result
+set.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+from fastapi import HTTPException # noqa: E402
+from fastapi.routing import APIRoute # noqa: E402
+from fastapi.testclient import TestClient # noqa: E402
+
+SECRET_SQL = "SELECT * FROM users WHERE password='hunter2'"
+SECRET_DSN = "psycopg2 connect failed: password=hunter2 host=10.0.0.5"
+
+
+async def _raises_http_500():
+ raise HTTPException(status_code=500, detail=SECRET_SQL)
+
+
+async def _raises_unhandled():
+ raise RuntimeError(SECRET_DSN)
+
+
+@pytest.fixture
+def app_with_failing_routes():
+ """The real app plus two routes that fail the way real handlers do."""
+ from flowyml.ui.backend.main import app
+
+ added = [
+ APIRoute("/api/_test_http_500", _raises_http_500, methods=["GET"]),
+ APIRoute("/api/_test_unhandled", _raises_unhandled, methods=["GET"]),
+ ]
+ # Insert at the front: a catch-all SPA route is registered last and would
+ # otherwise shadow anything added after import.
+ for route in added:
+ app.router.routes.insert(0, route)
+ try:
+ yield app
+ finally:
+ for route in added:
+ app.router.routes.remove(route)
+
+
+@pytest.fixture
+def dev_env(monkeypatch):
+ monkeypatch.delenv("FLOWYML_ENV", raising=False)
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+
+
+@pytest.fixture
+def prod_env(monkeypatch):
+ monkeypatch.setenv("FLOWYML_ENV", "production")
+ monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
+ monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "secret-password")
+ return {"Authorization": "Bearer secret-token"}
+
+
+class TestProductionErrorRedaction:
+ def test_http_500_detail_is_redacted(self, prod_env, app_with_failing_routes):
+ with TestClient(app_with_failing_routes, raise_server_exceptions=False) as client:
+ response = client.get("/api/_test_http_500", headers=prod_env)
+
+ assert response.status_code == 500
+ assert "hunter2" not in response.text
+ assert "SELECT" not in response.text
+
+ def test_unhandled_exception_text_is_redacted(self, prod_env, app_with_failing_routes):
+ with TestClient(app_with_failing_routes, raise_server_exceptions=False) as client:
+ response = client.get("/api/_test_unhandled", headers=prod_env)
+
+ assert response.status_code == 500
+ assert "hunter2" not in response.text
+ assert "10.0.0.5" not in response.text
+ assert "detail" not in response.json()
+
+ def test_responses_carry_a_reference_for_log_correlation(
+ self,
+ prod_env,
+ app_with_failing_routes,
+ ):
+ with TestClient(app_with_failing_routes, raise_server_exceptions=False) as client:
+ for path in ("/api/_test_http_500", "/api/_test_unhandled"):
+ body = client.get(path, headers=prod_env).json()
+ assert body["reference"], path
+
+ def test_client_errors_keep_their_message(self, prod_env, app_with_failing_routes):
+ """Redaction applies to 5xx only; 4xx text is meant for the caller."""
+ with TestClient(app_with_failing_routes) as client:
+ response = client.get("/api/runs/nonexistent-run-id", headers=prod_env)
+
+ assert response.status_code == 404
+ assert response.json()["detail"]
+
+
+class TestDevelopmentErrorDetail:
+ def test_detail_is_preserved_for_the_developer(self, dev_env, app_with_failing_routes):
+ with TestClient(app_with_failing_routes, raise_server_exceptions=False) as client:
+ response = client.get("/api/_test_http_500")
+
+ assert response.status_code == 500
+ assert SECRET_SQL in response.text
+
+ def test_unhandled_exception_detail_is_preserved(self, dev_env, app_with_failing_routes):
+ with TestClient(app_with_failing_routes, raise_server_exceptions=False) as client:
+ body = client.get("/api/_test_unhandled").json()
+
+ assert body["detail"] == SECRET_DSN
+
+
+class TestUnknownApiRoutes:
+ def test_unknown_api_path_returns_json_not_the_spa_shell(self, dev_env):
+ """A typo'd fetch URL must not receive HTML with a 200-looking body."""
+ from flowyml.ui.backend.main import app
+
+ with TestClient(app) as client:
+ response = client.get("/api/no-such-endpoint")
+
+ assert response.status_code == 404
+ assert response.headers["content-type"].startswith("application/json")
+
+
+class TestPaginationBounds:
+ """Every list endpoint must reject an unbounded page size."""
+
+ OVERSIZED = [
+ "/api/assets/?limit=100000000",
+ "/api/runs/?limit=100000000",
+ "/api/pipelines/?limit=100000000",
+ "/api/traces/?limit=100000000",
+ "/api/projects/demo/runs?limit=100000000",
+ "/api/projects/demo/artifacts?limit=100000000",
+ "/api/projects/demo/metrics?limit=100000000",
+ "/api/assets/search?q=x&limit=100000000",
+ "/api/assets/lineage?depth=1000",
+ ]
+
+ NON_POSITIVE = [
+ "/api/assets/?limit=0",
+ "/api/runs/?limit=-1",
+ "/api/pipelines/?limit=0",
+ "/api/traces/?limit=-10",
+ ]
+
+ VALID = [
+ "/api/assets/?limit=25",
+ "/api/runs/?limit=25",
+ "/api/pipelines/?limit=25",
+ "/api/traces/?limit=25",
+ "/api/assets/search?q=model",
+ "/api/assets/lineage?depth=2",
+ ]
+
+ @pytest.fixture
+ def client(self, dev_env):
+ from flowyml.ui.backend.main import app
+
+ with TestClient(app) as test_client:
+ yield test_client
+
+ @pytest.mark.parametrize("url", OVERSIZED)
+ def test_oversized_limit_is_rejected(self, client, url):
+ assert client.get(url).status_code == 422, url
+
+ @pytest.mark.parametrize("url", NON_POSITIVE)
+ def test_non_positive_limit_is_rejected(self, client, url):
+ assert client.get(url).status_code == 422, url
+
+ @pytest.mark.parametrize("url", VALID)
+ def test_reasonable_requests_still_succeed(self, client, url):
+ assert client.get(url).status_code == 200, url
+
+ def test_negative_log_offset_is_rejected(self, client):
+ response = client.get("/api/runs/some-run/steps/some-step/logs?offset=-1")
+ assert response.status_code == 422
+
+ def test_empty_search_query_is_rejected(self, client):
+ assert client.get("/api/assets/search?q=").status_code == 422
diff --git a/tests/test_backend_security.py b/tests/test_backend_security.py
index c80bdcca..bff70514 100644
--- a/tests/test_backend_security.py
+++ b/tests/test_backend_security.py
@@ -149,7 +149,10 @@ def test_production_without_token_denies_requests(self, production_env, app):
client = TestClient(app) # not used as a context manager => no lifespan
response = client.get("/api/runs/")
assert response.status_code == 503
- assert "FLOWYML_API_TOKEN" in response.json()["message"]
+ # The remedy is logged for the operator; the caller is told nothing
+ # that would confirm the instance is running unauthenticated.
+ assert "FLOWYML_API_TOKEN" not in response.text
+ assert "misconfigured" in response.json()["message"]
def test_production_rejects_missing_token(self, production_env, monkeypatch, app):
monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
@@ -231,13 +234,14 @@ def test_production_login_refuses_without_configured_password(
"""Must never accept the publicly documented default in production."""
monkeypatch.setenv("FLOWYML_API_TOKEN", "secret-token")
- client = TestClient(app) # no lifespan: exercise the endpoint guard
+ client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/api/auth/login",
json={"username": "admin", "password": security.INSECURE_DEFAULT_PASSWORD},
)
+ # Refused, and without echoing the configuration detail to the caller.
assert response.status_code == 503
- assert "FLOWYML_ADMIN_PASSWORD" in response.json()["detail"]
+ assert "FLOWYML_ADMIN_PASSWORD" not in response.text
def test_production_login_never_issues_placeholder_token(
self,
@@ -248,13 +252,15 @@ def test_production_login_never_issues_placeholder_token(
"""The old code handed out the guessable string 'dev-token-placeholder'."""
monkeypatch.setenv("FLOWYML_ADMIN_PASSWORD", "correct-horse")
- client = TestClient(app)
+ client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/api/auth/login",
json={"username": "admin", "password": "correct-horse"},
)
assert response.status_code == 503
- assert "FLOWYML_API_TOKEN" in response.json()["detail"]
+ # Crucially, no token is issued - least of all a guessable placeholder.
+ assert "dev-token-placeholder" not in response.text
+ assert "access_token" not in response.json()
def test_successful_production_login_sets_secure_cookie(
self,
From eee6f99f01ea45951ea17a2339280ef8970a37df Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:34:50 +0000
Subject: [PATCH 05/23] fix(assets): keep artifact ids addressable and locate
model files correctly
Artifact identity. SQLMetadataStore stores artifact_id in its own column, but
list_assets() and load_artifact() returned only the metadata JSON, so any
record whose metadata did not happen to repeat the id came back without one.
Everything created through POST /api/assets/ is in that category, because the
handler builds metadata from the request body and never writes the id into it.
The UI composes download and content URLs from that field, so those assets were
listed but not retrievable, and the dedupe helper fell back to a synthetic key.
Both methods now project the column into the result, which is exactly what
query() already did for run_id on runs. A metadata-supplied id still wins, so
existing records are unaffected.
Model availability. /api/deployments/available-models tested for model files
under a hard-coded "/app/artifacts" - the path inside the project's Docker
image. Anyone running `flowyml ui` from a pip install keeps artifacts under the
configured artifacts directory, so the probe always failed and the deployments
page rendered an amber "Missing" badge for every model, making the deploy flow
look broken. Resolution now goes through the configured artifacts directory and
the shared confinement helper, so an artifact path pointing outside the root
reports False instead of probing the host filesystem. The probes also run off
the event loop rather than blocking it inside an async handler.
Repository is now lint-clean: this also fixes the three pre-existing ruff
findings (two blocking-filesystem calls in async handlers, one docstring) and
the pathlib findings in the token store added earlier.
Adds tests/test_asset_identity_and_availability.py: 11 cases covering id
projection in both store methods, the full create/upload/list/download path the
UI takes, and model availability reporting including out-of-root paths.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/storage/sql.py | 30 ++-
flowyml/ui/backend/auth.py | 7 +-
flowyml/ui/backend/routers/deployments.py | 50 +++--
tests/enterprise/test_models.py | 2 +-
tests/test_asset_identity_and_availability.py | 202 ++++++++++++++++++
5 files changed, 269 insertions(+), 22 deletions(-)
create mode 100644 tests/test_asset_identity_and_availability.py
diff --git a/flowyml/storage/sql.py b/flowyml/storage/sql.py
index 528dcca4..bab217ec 100644
--- a/flowyml/storage/sql.py
+++ b/flowyml/storage/sql.py
@@ -436,14 +436,23 @@ def save_artifact(self, artifact_id: str, metadata: dict) -> None:
conn.commit()
def load_artifact(self, artifact_id: str) -> dict | None:
- """Load artifact metadata."""
+ """Load artifact metadata.
+
+ ``artifact_id`` is always present in the result. It lives in its own
+ column, and callers that wrote metadata without repeating it there
+ (anything created through ``POST /api/assets/``, for example) would
+ otherwise get back a record the UI cannot address, since download and
+ content URLs are built from that field.
+ """
with self.engine.connect() as conn:
stmt = select(self.artifacts.c.metadata).where(
self.artifacts.c.artifact_id == artifact_id,
)
row = conn.execute(stmt).fetchone()
if row:
- return json.loads(row[0])
+ data = json.loads(row[0])
+ data.setdefault("artifact_id", artifact_id)
+ return data
return None
def delete_artifact(self, artifact_id: str) -> None:
@@ -453,9 +462,14 @@ def delete_artifact(self, artifact_id: str) -> None:
conn.commit()
def list_assets(self, limit: int | None = None, **filters) -> list[dict]:
- """List assets with optional filters."""
+ """List assets with optional filters.
+
+ Each result carries its ``artifact_id``, projected from the column
+ rather than trusted to be duplicated inside the metadata JSON - the
+ same treatment :meth:`query` gives ``run_id``.
+ """
with self.engine.connect() as conn:
- stmt = select(self.artifacts.c.metadata)
+ stmt = select(self.artifacts.c.artifact_id, self.artifacts.c.metadata)
for key, value in filters.items():
if value is not None and hasattr(self.artifacts.c, key):
@@ -466,8 +480,12 @@ def list_assets(self, limit: int | None = None, **filters) -> list[dict]:
if limit:
stmt = stmt.limit(limit)
- rows = conn.execute(stmt).fetchall()
- return [json.loads(row[0]) for row in rows]
+ assets = []
+ for artifact_id, raw_metadata in conn.execute(stmt).fetchall():
+ data = json.loads(raw_metadata)
+ data.setdefault("artifact_id", artifact_id)
+ assets.append(data)
+ return assets
def query(self, **filters) -> list[dict]:
"""Query runs with filters."""
diff --git a/flowyml/ui/backend/auth.py b/flowyml/ui/backend/auth.py
index 84d1cde1..432fb7ce 100644
--- a/flowyml/ui/backend/auth.py
+++ b/flowyml/ui/backend/auth.py
@@ -93,16 +93,17 @@ def _save_tokens(self) -> None:
prefix=self.tokens_file.name,
suffix=".tmp",
)
+ tmp = Path(tmp_path)
try:
with os.fdopen(fd, "w") as handle:
json.dump(self.tokens, handle, indent=2)
handle.flush()
os.fsync(handle.fileno())
- os.chmod(tmp_path, 0o600)
- os.replace(tmp_path, self.tokens_file)
+ tmp.chmod(0o600)
+ tmp.replace(self.tokens_file)
except BaseException:
with contextlib.suppress(OSError):
- os.unlink(tmp_path)
+ tmp.unlink()
raise
def _hash_token(self, token: str) -> str:
diff --git a/flowyml/ui/backend/routers/deployments.py b/flowyml/ui/backend/routers/deployments.py
index e8e85947..44c8b76b 100644
--- a/flowyml/ui/backend/routers/deployments.py
+++ b/flowyml/ui/backend/routers/deployments.py
@@ -3,9 +3,13 @@
from fastapi import APIRouter, HTTPException, BackgroundTasks, Query
from pydantic import BaseModel, Field
from datetime import datetime
+from pathlib import Path
from uuid import uuid4
+import anyio
import secrets
+from flowyml.ui.backend.artifact_paths import ArtifactPathError, resolve_within_root
+
router = APIRouter(prefix="/deployments", tags=["deployments"])
#: Upper bound on how many log lines a single request may pull back.
@@ -13,6 +17,32 @@
+
+def _artifact_file_exists(artifact_path: str | None) -> bool:
+ """Whether an artifact's file is present in the configured artifacts directory.
+
+ The previous implementation joined the path onto a hard-coded
+ "/app/artifacts", which is only correct inside the project's Docker image.
+ Anyone running ``flowyml ui`` from a pip install has their artifacts under
+ the configured artifacts directory, so this always returned False and the
+ deployments page labelled every model "Missing".
+
+ Resolution is confined to the artifacts root, so an artifact path pointing
+ outside it reports False instead of probing the host filesystem.
+ """
+ if not artifact_path:
+ return False
+
+ from flowyml.utils.config import get_config
+
+ try:
+ resolved = resolve_within_root(artifact_path, Path(get_config().artifacts_dir))
+ except ArtifactPathError:
+ return False
+
+ return resolved.exists()
+
+
# ==================== Schemas ====================
# Common ML framework dependencies that can be installed on-demand
@@ -166,7 +196,6 @@ async def get_available_models() -> list[dict]:
"""Get list of models available for deployment."""
# Import here to avoid circular imports
from flowyml.ui.backend.dependencies import get_store
- import os
store = get_store()
try:
@@ -206,11 +235,8 @@ async def get_available_models() -> list[dict]:
# Skip if no file path (inline values can't be deployed as model servers)
has_file = bool(path)
- file_exists = False
- if has_file:
- # Check if file exists (in container, paths are relative to artifacts dir)
- full_path = os.path.join("/app/artifacts", path)
- file_exists = os.path.exists(full_path)
+ # Filesystem probes are blocking, so they run off the event loop.
+ file_exists = await anyio.to_thread.run_sync(_artifact_file_exists, path)
if is_model:
# Generate artifact_id if not present
@@ -232,6 +258,10 @@ async def get_available_models() -> list[dict]:
# If no models found, return all artifacts with paths as potential models
if not models and artifacts:
+ fallback_exists = [
+ await anyio.to_thread.run_sync(_artifact_file_exists, a.get("path"))
+ for a in artifacts
+ ]
models = [
{
"artifact_id": a.get("artifact_id") or f"{a.get('run_id')}_{a.get('step')}_{a.get('name')}",
@@ -242,14 +272,10 @@ async def get_available_models() -> list[dict]:
"run_id": a.get("run_id"),
"project": a.get("project"),
"has_file": bool(a.get("path")),
- "file_exists": os.path.exists(
- os.path.join("/app/artifacts", a.get("path") or ""),
- )
- if a.get("path")
- else False,
+ "file_exists": fallback_exists[index],
"path": a.get("path"),
}
- for a in artifacts
+ for index, a in enumerate(artifacts)
]
return models
diff --git a/tests/enterprise/test_models.py b/tests/enterprise/test_models.py
index ee183f17..9aeeeb50 100644
--- a/tests/enterprise/test_models.py
+++ b/tests/enterprise/test_models.py
@@ -48,7 +48,7 @@ def test_stack_definition_invalid_compute_type(self, sample_stack_dict):
StackDefinition.from_dict(sample_stack_dict)
def test_stack_definition_instance_range(self, sample_stack_dict):
- """minInstances > maxInstances is rejected."""
+ """A stack whose minInstances exceeds maxInstances is rejected."""
sample_stack_dict["spec"]["compute"]["minInstances"] = 5
sample_stack_dict["spec"]["compute"]["maxInstances"] = 2
with pytest.raises(ValidationError, match="minInstances.*cannot exceed.*maxInstances"):
diff --git a/tests/test_asset_identity_and_availability.py b/tests/test_asset_identity_and_availability.py
new file mode 100644
index 00000000..8001b27c
--- /dev/null
+++ b/tests/test_asset_identity_and_availability.py
@@ -0,0 +1,202 @@
+"""Tests for artifact identity in the store and model availability reporting.
+
+Two defects that made assets look broken in the UI:
+
+*Missing identity.* ``SQLMetadataStore`` keeps ``artifact_id`` in its own
+column but ``list_assets``/``load_artifact`` returned only the metadata JSON.
+Records whose metadata did not happen to repeat the id — everything created
+through ``POST /api/assets/`` — came back without one, and the UI builds
+download and content URLs from that field. ``query()`` had always projected
+``run_id`` the same way for runs; artifacts simply never got the same
+treatment.
+
+*Wrong artifact root.* ``/api/deployments/available-models`` probed for model
+files under a hard-coded ``/app/artifacts``, the path inside the project's
+Docker image. Anyone running ``flowyml ui`` from a pip install keeps artifacts
+under the configured artifacts directory, so the probe always failed and the
+deployments page labelled every model "Missing".
+"""
+
+from __future__ import annotations
+
+import pytest
+
+pytest.importorskip("fastapi", reason="fastapi not installed")
+
+from fastapi.testclient import TestClient # noqa: E402
+
+from flowyml.storage.sql import SQLMetadataStore # noqa: E402
+
+
+@pytest.fixture
+def store(tmp_path):
+ return SQLMetadataStore(db_path=str(tmp_path / "meta.db"))
+
+
+class TestArtifactIdentity:
+ def test_list_assets_includes_the_id_when_metadata_omits_it(self, store):
+ store.save_artifact("no-id-in-metadata", {"name": "m", "type": "model"})
+
+ [asset] = store.list_assets()
+ assert asset["artifact_id"] == "no-id-in-metadata"
+
+ def test_load_artifact_includes_the_id_when_metadata_omits_it(self, store):
+ store.save_artifact("no-id-in-metadata", {"name": "m", "type": "model"})
+
+ loaded = store.load_artifact("no-id-in-metadata")
+ assert loaded["artifact_id"] == "no-id-in-metadata"
+
+ def test_metadata_id_is_not_overwritten(self, store):
+ """A metadata-supplied id wins, so existing records keep their value."""
+ store.save_artifact("column-id", {"artifact_id": "metadata-id", "name": "m"})
+
+ assert store.load_artifact("column-id")["artifact_id"] == "metadata-id"
+ assert store.list_assets()[0]["artifact_id"] == "metadata-id"
+
+ def test_every_listed_asset_is_addressable(self, store):
+ for index in range(5):
+ store.save_artifact(f"asset-{index}", {"name": f"m{index}", "type": "model"})
+
+ assets = store.list_assets()
+ assert len(assets) == 5
+ assert all(asset.get("artifact_id") for asset in assets)
+
+ def test_load_artifact_still_returns_none_for_unknown_id(self, store):
+ assert store.load_artifact("nope") is None
+
+
+@pytest.fixture
+def client(monkeypatch, tmp_path):
+ """API client with isolated artifact and metadata storage."""
+ monkeypatch.delenv("FLOWYML_ENV", raising=False)
+ monkeypatch.delenv("FLOWYML_API_TOKEN", raising=False)
+
+ artifacts_dir = tmp_path / "artifacts"
+ artifacts_dir.mkdir()
+
+ from flowyml.utils import config as config_module
+
+ real_get_config = config_module.get_config
+
+ def _scoped_config(*args, **kwargs):
+ cfg = real_get_config(*args, **kwargs)
+ cfg.artifacts_dir = artifacts_dir
+ return cfg
+
+ monkeypatch.setattr(config_module, "get_config", _scoped_config)
+
+ from flowyml.ui.backend import dependencies
+
+ monkeypatch.setattr(
+ dependencies,
+ "_store",
+ SQLMetadataStore(db_path=str(tmp_path / "meta.db")),
+ )
+
+ from flowyml.ui.backend.main import app
+
+ with TestClient(app) as test_client:
+ yield test_client
+
+
+class TestRestCreatedAssetsAreUsable:
+ """The full path the UI takes: create, upload, list, download."""
+
+ def test_asset_created_via_api_is_listed_with_an_id(self, client):
+ client.post(
+ "/api/assets/",
+ json={
+ "artifact_id": "rest-made",
+ "name": "my_model",
+ "type": "model",
+ "run_id": "run-1",
+ "step": "train",
+ },
+ )
+
+ listed = client.get("/api/assets/?limit=100").json()["assets"]
+ assert any(asset.get("artifact_id") == "rest-made" for asset in listed)
+
+ def test_asset_created_via_api_can_be_downloaded(self, client):
+ client.post(
+ "/api/assets/",
+ json={
+ "artifact_id": "rest-made",
+ "name": "my_model",
+ "type": "model",
+ "run_id": "run-1",
+ "step": "train",
+ },
+ )
+ client.post(
+ "/api/assets/rest-made/upload",
+ files={"file": ("model.pkl", b"MODELBYTES")},
+ )
+
+ response = client.get("/api/assets/rest-made/download")
+ assert response.status_code == 200
+ assert response.content == b"MODELBYTES"
+
+
+class TestModelAvailability:
+ def _create_model_with_file(self, client, artifact_id="deployable"):
+ client.post(
+ "/api/assets/",
+ json={
+ "artifact_id": artifact_id,
+ "name": "my_model",
+ "type": "model",
+ "run_id": "run-1",
+ "step": "train",
+ },
+ )
+ client.post(
+ f"/api/assets/{artifact_id}/upload",
+ files={"file": ("model.pkl", b"MODELBYTES")},
+ )
+
+ def test_model_with_a_file_reports_file_exists(self, client):
+ """Previously always False outside Docker, showing "Missing" for every model."""
+ self._create_model_with_file(client)
+
+ models = client.get("/api/deployments/available-models").json()
+ found = [m for m in models if m["artifact_id"] == "deployable"]
+
+ assert found, "model should be offered for deployment"
+ assert found[0]["has_file"] is True
+ assert found[0]["file_exists"] is True
+
+ def test_model_without_a_file_reports_file_missing(self, client):
+ client.post(
+ "/api/assets/",
+ json={
+ "artifact_id": "no-file",
+ "name": "my_model",
+ "type": "model",
+ "run_id": "run-1",
+ "step": "train",
+ },
+ )
+
+ models = client.get("/api/deployments/available-models").json()
+ found = [m for m in models if m["artifact_id"] == "no-file"]
+
+ assert found
+ assert found[0]["has_file"] is False
+ assert found[0]["file_exists"] is False
+
+ def test_path_outside_the_artifacts_root_is_not_probed(self, client, tmp_path):
+ """A path escaping the root reports False rather than probing the host."""
+ from flowyml.ui.backend.routers.deployments import _artifact_file_exists
+
+ outside = tmp_path / "outside.txt"
+ outside.write_text("data")
+
+ assert _artifact_file_exists(str(outside)) is False
+ assert _artifact_file_exists("../../../etc/hostname") is False
+
+ def test_missing_path_reports_false(self, client):
+ from flowyml.ui.backend.routers.deployments import _artifact_file_exists
+
+ assert _artifact_file_exists(None) is False
+ assert _artifact_file_exists("") is False
From 6d9391d49dace91d6febb56fc51cb3ffaa106f96 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 27 Aug 2026 14:46:02 +0000
Subject: [PATCH 06/23] perf(ui): cut initial bundle from 7.9 MB to 394 KB and
fix redirected API calls
Bundle. The frontend shipped as a single 7.9 MB chunk (2.7 MB gzipped) that
every visitor downloaded before the first paint. Three causes, all fixed:
- @mlc-ai/web-llm, the in-browser LLM runtime and by far the largest
dependency at 5.4 MB, was imported statically by an app-wide context
provider. It has exactly one call site, already inside an async function, so
it is now imported on demand when a user actually starts the assistant.
- The router imported all 24 pages eagerly, so opening the dashboard also
downloaded the charting, graph-rendering and syntax-highlighting code for
every other screen. Pages are now React.lazy with a Suspense fallback.
- AIAssistantPanel rendered unconditionally in MainLayout, pulling the markdown
renderer and syntax highlighter into every page load for a panel that starts
closed. It now mounts on first open and stays mounted so its transitions
still run.
Initial load is 394 KB across two chunks; the remaining 7.2 MB is split into 80
chunks fetched per route. manualChunks deliberately pins only React: naming the
page-specific libraries forced each into one shared chunk that the entry
preloaded even when no initial route needed it, which left 1.5 MB on the
critical path.
Redirected API calls. 26 call sites requested paths the backend declares with a
trailing slash (`/api/runs` vs `/api/runs/`), so every one cost a 307 and a
second round trip - including a POST that only survived because 307 preserves
the body, and all of which break behind a proxy that does not forward
redirects. All now request the declared path.
WebSocket. Live step logs built their URL from window.location.host, so in
remote-execution mode the socket connected to the wrong origin and silently
fell back to polling. A new getWebSocketUrl() mirrors getBaseUrl(); the run id
and step name are URL-encoded, and a socket that resolves after the effect is
torn down is now closed instead of leaked. Adds the missing /ws entry to the
Vite dev proxy, without which live logs never worked under `npm run dev`.
Also reverts the SPA catch-all route introduced in the previous commit back to
a 404 handler. A catch-all matches before Starlette's redirect_slashes logic,
which turned every trailing-slash mismatch into a 404 - a regression caught by
driving the real UI in a browser, where five pages were failing to load data.
The handler now merges the SPA fallback with 5xx redaction, since Starlette
allows one handler per exception type, and still returns JSON 404s for unknown
/api and /ws paths.
Verified by loading all 19 routes in headless Chromium: every one renders with
no console errors, no page errors, no failed requests and no 4xx/5xx API
responses. Adds a test asserting no frontend call depends on a trailing-slash
redirect - the existing segment-wise matcher normalised slashes away and could
not see this class.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4
---
flowyml/ui/backend/main.py | 35 +-
.../dist/assets/AIAssistantPanel-CdBU5Wow.js | 37 +
.../dist/assets/AssetDetailsPanel-BWc4HjZj.js | 14 +
.../ui/frontend/dist/assets/Badge-DP8qyRAF.js | 1 +
.../ui/frontend/dist/assets/Card-BBZKGAoE.js | 1 +
.../frontend/dist/assets/DataView-Hll7aI4A.js | 1 +
.../dist/assets/DatasetViewer-CQ565S_k.js | 103 ++
.../dist/assets/EmptyState-CkpoIEaq.js | 1 +
.../dist/assets/ExecutionStatus-D66pQxjD.js | 1 +
.../ui/frontend/dist/assets/Login-C_lkCwWS.js | 1 +
.../dist/assets/NavigationTree-pPdRIMDM.js | 6 +
.../dist/assets/PipelineGraph-C1Sy4sxM.js | 34 +
.../dist/assets/PipelineGraph-Fd0xVSp_.css | 1 +
.../dist/assets/ProjectSelector-B3P3u1ON.js | 7 +
.../dist/assets/arrow-left-Bk3w8Z42.js | 6 +
.../dist/assets/arrow-right-D6I50UgY.js | 6 +
.../dist/assets/arrow-up-down-6WlE3Jtk.js | 6 +
.../dist/assets/bar-chart-2-CHGh-BjM.js | 6 +
.../dist/assets/bar-chart-3-oRZUKegZ.js | 6 +
.../ui/frontend/dist/assets/check-D0tKBgRQ.js | 6 +
.../dist/assets/check-circle-2-KzKxkKkN.js | 6 +
.../ui/frontend/dist/assets/clock-D9L7Xp_x.js | 6 +
.../ui/frontend/dist/assets/cloud-Bq27gras.js | 6 +
.../ui/frontend/dist/assets/copy-BgBLzvyA.js | 6 +
.../ui/frontend/dist/assets/cpu-Dfypt6uH.js | 6 +
.../ui/frontend/dist/assets/date-BeX_axv0.js | 1 +
.../frontend/dist/assets/download-B19i2bDd.js | 6 +
.../dist/assets/evaluationsApi-DK-DNZQI.js | 1 +
.../ui/frontend/dist/assets/eye-DOnLepKL.js | 6 +
.../frontend/dist/assets/eye-off-BzFsPg4u.js | 6 +
.../dist/assets/file-text-mhv5Ydfz.js | 6 +
.../frontend/dist/assets/filter-rWfOJeAK.js | 6 +
.../frontend/dist/assets/folder-Cu-h4dxh.js | 6 +
.../frontend/dist/assets/format-CXbwdB0y.js | 1 +
.../dist/assets/git-branch-BczbGpb8.js | 6 +
.../dist/assets/git-compare-KInlJ_wi.js | 6 +
.../dist/assets/hard-drive-_3xPqRHV.js | 6 +
.../dist/assets/highlight-DAeDSD7N.js | 6 +
.../frontend/dist/assets/history-DjcC86SW.js | 6 +
.../frontend/dist/assets/index-B_TACZy4.css | 1 +
.../frontend/dist/assets/index-CNml3YfC.css | 1 -
.../ui/frontend/dist/assets/index-Dh3jADKv.js | 267 +++++
.../ui/frontend/dist/assets/index-YpiMqZ_R.js | 89 ++
.../ui/frontend/dist/assets/index-_Mg40Vrw.js | 1014 -----------------
.../frontend/dist/assets/layers-D_GBACOZ.js | 6 +
.../ui/frontend/dist/assets/list-CRPvu0WP.js | 11 +
.../frontend/dist/assets/loader-2-DzAWsyqi.js | 6 +
.../frontend/dist/assets/loader-DYj5Ts2J.js | 6 +
.../dist/assets/message-square-BXLoKqlV.js | 6 +
.../ui/frontend/dist/assets/page-BHkkq3xc.js | 21 +
.../ui/frontend/dist/assets/page-BK6nHh94.js | 1 +
.../ui/frontend/dist/assets/page-BT8Ojpug.js | 6 +
.../ui/frontend/dist/assets/page-BoXxAhwZ.js | 1 +
.../ui/frontend/dist/assets/page-C5kRTjd_.js | 6 +
.../ui/frontend/dist/assets/page-C6GrWmfX.js | 6 +
.../ui/frontend/dist/assets/page-CATx7BYg.js | 1 +
.../ui/frontend/dist/assets/page-CAbuuuxV.js | 46 +
.../ui/frontend/dist/assets/page-CENBnYm2.js | 16 +
.../ui/frontend/dist/assets/page-CN8MXBpQ.js | 11 +
.../ui/frontend/dist/assets/page-Cjt8rVCi.js | 5 +
.../ui/frontend/dist/assets/page-DAzWRJ4r.js | 17 +
.../ui/frontend/dist/assets/page-D_d-QCNL.js | 1 +
.../ui/frontend/dist/assets/page-DegCJWzu.js | 1 +
.../ui/frontend/dist/assets/page-DhBBce9B.js | 9 +
.../ui/frontend/dist/assets/page-DiQuuZ6d.js | 19 +
.../ui/frontend/dist/assets/page-Dijb20wF.js | 1 +
.../ui/frontend/dist/assets/page-DmfVboQn.js | 1 +
.../ui/frontend/dist/assets/page-Dqg992zE.js | 1 +
.../ui/frontend/dist/assets/page-DsKl1lJd.js | 9 +
.../ui/frontend/dist/assets/page-DtPL68sm.js | 11 +
.../ui/frontend/dist/assets/page-b2dlRXZO.js | 24 +
.../ui/frontend/dist/assets/page-h3nsJPQa.js | 1 +
.../dist/assets/panels-top-left-MdLksA2T.js | 6 +
.../ui/frontend/dist/assets/play-D2sR6f2R.js | 6 +
.../ui/frontend/dist/assets/plus-C6KTwy-_.js | 6 +
.../frontend/dist/assets/search-BQwteO7S.js | 6 +
.../frontend/dist/assets/shield-BP_2yyBW.js | 6 +
.../frontend/dist/assets/sortBy-BTniHmyJ.js | 6 +
.../ui/frontend/dist/assets/table-B2ryjvUR.js | 6 +
.../ui/frontend/dist/assets/tag-C3gRB1_e.js | 11 +
.../frontend/dist/assets/target-VsuVqYjD.js | 6 +
.../frontend/dist/assets/trash-2-rOyV3320.js | 6 +
.../dist/assets/trending-down-C3r_rJEM.js | 6 +
.../dist/assets/trending-up-WhlPTOUR.js | 6 +
.../dist/assets/vendor-react-CseKpsZ9.js | 60 +
.../frontend/dist/assets/x-circle-BeG6n9aL.js | 6 +
.../ui/frontend/dist/assets/zap-BRDqRZqn.js | 6 +
flowyml/ui/frontend/dist/index.html | 5 +-
flowyml/ui/frontend/src/app/assets/page.jsx | 4 +-
.../_components/ProjectExperimentsList.jsx | 2 +-
.../_components/ProjectHierarchy.jsx | 6 +-
.../_components/ProjectPipelinesList.jsx | 2 +-
.../_components/ProjectRelations.jsx | 6 +-
.../_components/ProjectRunsList.jsx | 2 +-
.../src/app/projects/[projectId]/page.jsx | 6 +-
flowyml/ui/frontend/src/app/projects/page.jsx | 4 +-
.../ui/frontend/src/app/runs/[runId]/page.jsx | 88 +-
.../ui/frontend/src/app/schedules/page.jsx | 2 +-
flowyml/ui/frontend/src/app/traces/page.jsx | 2 +-
.../src/components/AssetTreeHierarchy.jsx | 12 +-
.../src/components/RunDetailsPanel.jsx | 2 +-
.../src/contexts/AIAssistantContext.jsx | 7 +-
.../ui/frontend/src/layouts/MainLayout.jsx | 35 +-
flowyml/ui/frontend/src/router/index.jsx | 65 +-
flowyml/ui/frontend/src/utils/api.js | 25 +
flowyml/ui/frontend/vite.config.js | 31 +
tests/test_api_alignment.py | 41 +
107 files changed, 1372 insertions(+), 1124 deletions(-)
create mode 100644 flowyml/ui/frontend/dist/assets/AIAssistantPanel-CdBU5Wow.js
create mode 100644 flowyml/ui/frontend/dist/assets/AssetDetailsPanel-BWc4HjZj.js
create mode 100644 flowyml/ui/frontend/dist/assets/Badge-DP8qyRAF.js
create mode 100644 flowyml/ui/frontend/dist/assets/Card-BBZKGAoE.js
create mode 100644 flowyml/ui/frontend/dist/assets/DataView-Hll7aI4A.js
create mode 100644 flowyml/ui/frontend/dist/assets/DatasetViewer-CQ565S_k.js
create mode 100644 flowyml/ui/frontend/dist/assets/EmptyState-CkpoIEaq.js
create mode 100644 flowyml/ui/frontend/dist/assets/ExecutionStatus-D66pQxjD.js
create mode 100644 flowyml/ui/frontend/dist/assets/Login-C_lkCwWS.js
create mode 100644 flowyml/ui/frontend/dist/assets/NavigationTree-pPdRIMDM.js
create mode 100644 flowyml/ui/frontend/dist/assets/PipelineGraph-C1Sy4sxM.js
create mode 100644 flowyml/ui/frontend/dist/assets/PipelineGraph-Fd0xVSp_.css
create mode 100644 flowyml/ui/frontend/dist/assets/ProjectSelector-B3P3u1ON.js
create mode 100644 flowyml/ui/frontend/dist/assets/arrow-left-Bk3w8Z42.js
create mode 100644 flowyml/ui/frontend/dist/assets/arrow-right-D6I50UgY.js
create mode 100644 flowyml/ui/frontend/dist/assets/arrow-up-down-6WlE3Jtk.js
create mode 100644 flowyml/ui/frontend/dist/assets/bar-chart-2-CHGh-BjM.js
create mode 100644 flowyml/ui/frontend/dist/assets/bar-chart-3-oRZUKegZ.js
create mode 100644 flowyml/ui/frontend/dist/assets/check-D0tKBgRQ.js
create mode 100644 flowyml/ui/frontend/dist/assets/check-circle-2-KzKxkKkN.js
create mode 100644 flowyml/ui/frontend/dist/assets/clock-D9L7Xp_x.js
create mode 100644 flowyml/ui/frontend/dist/assets/cloud-Bq27gras.js
create mode 100644 flowyml/ui/frontend/dist/assets/copy-BgBLzvyA.js
create mode 100644 flowyml/ui/frontend/dist/assets/cpu-Dfypt6uH.js
create mode 100644 flowyml/ui/frontend/dist/assets/date-BeX_axv0.js
create mode 100644 flowyml/ui/frontend/dist/assets/download-B19i2bDd.js
create mode 100644 flowyml/ui/frontend/dist/assets/evaluationsApi-DK-DNZQI.js
create mode 100644 flowyml/ui/frontend/dist/assets/eye-DOnLepKL.js
create mode 100644 flowyml/ui/frontend/dist/assets/eye-off-BzFsPg4u.js
create mode 100644 flowyml/ui/frontend/dist/assets/file-text-mhv5Ydfz.js
create mode 100644 flowyml/ui/frontend/dist/assets/filter-rWfOJeAK.js
create mode 100644 flowyml/ui/frontend/dist/assets/folder-Cu-h4dxh.js
create mode 100644 flowyml/ui/frontend/dist/assets/format-CXbwdB0y.js
create mode 100644 flowyml/ui/frontend/dist/assets/git-branch-BczbGpb8.js
create mode 100644 flowyml/ui/frontend/dist/assets/git-compare-KInlJ_wi.js
create mode 100644 flowyml/ui/frontend/dist/assets/hard-drive-_3xPqRHV.js
create mode 100644 flowyml/ui/frontend/dist/assets/highlight-DAeDSD7N.js
create mode 100644 flowyml/ui/frontend/dist/assets/history-DjcC86SW.js
create mode 100644 flowyml/ui/frontend/dist/assets/index-B_TACZy4.css
delete mode 100644 flowyml/ui/frontend/dist/assets/index-CNml3YfC.css
create mode 100644 flowyml/ui/frontend/dist/assets/index-Dh3jADKv.js
create mode 100644 flowyml/ui/frontend/dist/assets/index-YpiMqZ_R.js
delete mode 100644 flowyml/ui/frontend/dist/assets/index-_Mg40Vrw.js
create mode 100644 flowyml/ui/frontend/dist/assets/layers-D_GBACOZ.js
create mode 100644 flowyml/ui/frontend/dist/assets/list-CRPvu0WP.js
create mode 100644 flowyml/ui/frontend/dist/assets/loader-2-DzAWsyqi.js
create mode 100644 flowyml/ui/frontend/dist/assets/loader-DYj5Ts2J.js
create mode 100644 flowyml/ui/frontend/dist/assets/message-square-BXLoKqlV.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-BHkkq3xc.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-BK6nHh94.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-BT8Ojpug.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-BoXxAhwZ.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-C5kRTjd_.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-C6GrWmfX.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-CATx7BYg.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-CAbuuuxV.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-CENBnYm2.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-CN8MXBpQ.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-Cjt8rVCi.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DAzWRJ4r.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-D_d-QCNL.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DegCJWzu.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DhBBce9B.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DiQuuZ6d.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-Dijb20wF.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DmfVboQn.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-Dqg992zE.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DsKl1lJd.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-DtPL68sm.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-b2dlRXZO.js
create mode 100644 flowyml/ui/frontend/dist/assets/page-h3nsJPQa.js
create mode 100644 flowyml/ui/frontend/dist/assets/panels-top-left-MdLksA2T.js
create mode 100644 flowyml/ui/frontend/dist/assets/play-D2sR6f2R.js
create mode 100644 flowyml/ui/frontend/dist/assets/plus-C6KTwy-_.js
create mode 100644 flowyml/ui/frontend/dist/assets/search-BQwteO7S.js
create mode 100644 flowyml/ui/frontend/dist/assets/shield-BP_2yyBW.js
create mode 100644 flowyml/ui/frontend/dist/assets/sortBy-BTniHmyJ.js
create mode 100644 flowyml/ui/frontend/dist/assets/table-B2ryjvUR.js
create mode 100644 flowyml/ui/frontend/dist/assets/tag-C3gRB1_e.js
create mode 100644 flowyml/ui/frontend/dist/assets/target-VsuVqYjD.js
create mode 100644 flowyml/ui/frontend/dist/assets/trash-2-rOyV3320.js
create mode 100644 flowyml/ui/frontend/dist/assets/trending-down-C3r_rJEM.js
create mode 100644 flowyml/ui/frontend/dist/assets/trending-up-WhlPTOUR.js
create mode 100644 flowyml/ui/frontend/dist/assets/vendor-react-CseKpsZ9.js
create mode 100644 flowyml/ui/frontend/dist/assets/x-circle-BeG6n9aL.js
create mode 100644 flowyml/ui/frontend/dist/assets/zap-BRDqRZqn.js
diff --git a/flowyml/ui/backend/main.py b/flowyml/ui/backend/main.py
index 919d483b..0ac3a961 100644
--- a/flowyml/ui/backend/main.py
+++ b/flowyml/ui/backend/main.py
@@ -275,21 +275,14 @@ async def serve_root():
# Instead, mount a StaticFiles handler for the root, but do it AFTER API routes
# Actually, let's try a different approach - use a custom middleware or exceptions
- # The trick is to let FastAPI handle routes first, then catch 404s
- @app.get("/{spa_path:path}", include_in_schema=False)
- async def serve_spa(spa_path: str):
- """Serve the SPA shell for client-side routes.
-
- Registered last, so every API route and static mount takes precedence.
- Unmatched /api paths must still 404 as JSON rather than silently
- returning HTML, which would make a typo in a fetch URL look like a
- successful response the client then fails to parse.
- """
- if spa_path.startswith(("api/", "ws/")):
- raise StarletteHTTPException(status_code=404, detail="Not Found")
- return FileResponse(os.path.join(frontend_dist, "index.html"))
+ # Client-side routes are served by the 404 handler below rather than a
+ # catch-all route. A catch-all would match *before* Starlette's
+ # redirect_slashes logic, turning every `/api/stats` (declared as
+ # `/api/stats/`) into a 404 instead of a redirect.
+ _frontend_index = os.path.join(frontend_dist, "index.html")
else:
+ _frontend_index = None
@app.get("/")
async def root():
@@ -306,7 +299,14 @@ def _error_reference() -> str:
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler_with_redaction(request, exc):
- """Keep 4xx details, redact 5xx details in production.
+ """Serve the SPA for unknown page routes; redact 5xx details in production.
+
+ A single handler covers both concerns because Starlette allows only one
+ handler per exception type.
+
+ Unknown ``/api`` and ``/ws`` paths deliberately keep their JSON 404: a typo
+ in a fetch URL must not come back as the HTML shell, which the caller would
+ then fail to parse with a confusing error far from the cause.
Handlers across the API raise ``HTTPException(500, detail=str(e))``. In
production that text reaches the client verbatim, and for SQLAlchemy errors
@@ -314,6 +314,13 @@ async def http_exception_handler_with_redaction(request, exc):
errors carry absolute server paths. The real message is always logged; only
what crosses the wire is reduced.
"""
+ if (
+ exc.status_code == 404
+ and _frontend_index is not None
+ and not request.url.path.startswith(("/api", "/ws"))
+ ):
+ return FileResponse(_frontend_index)
+
if exc.status_code >= 500:
reference = _error_reference()
logger.error(
diff --git a/flowyml/ui/frontend/dist/assets/AIAssistantPanel-CdBU5Wow.js b/flowyml/ui/frontend/dist/assets/AIAssistantPanel-CdBU5Wow.js
new file mode 100644
index 00000000..3873bd0f
--- /dev/null
+++ b/flowyml/ui/frontend/dist/assets/AIAssistantPanel-CdBU5Wow.js
@@ -0,0 +1,37 @@
+import{c as wc,j as I,e as Ac,k as _c,m as Nt,v as Ql,X as Ys,d as Jl,T as Ic}from"./index-Dh3jADKv.js";import{j as xt,g as eu,r as ve}from"./vendor-react-CseKpsZ9.js";import{C as Rc}from"./cpu-Dfypt6uH.js";import{T as Nc}from"./trash-2-rOyV3320.js";import{Z as xc}from"./zap-BRDqRZqn.js";import{E as Ks}from"./eye-DOnLepKL.js";import{E as vc}from"./eye-off-BzFsPg4u.js";import{L as Cc}from"./loader-2-DzAWsyqi.js";import{B as Oc}from"./bar-chart-2-CHGh-BjM.js";import{F as Lc}from"./file-text-mhv5Ydfz.js";import{L as Dc}from"./layers-D_GBACOZ.js";import{h as Mc}from"./highlight-DAeDSD7N.js";/**
+ * @license lucide-react v0.344.0 - ISC
+ *
+ * This source code is licensed under the ISC license.
+ * See the LICENSE file in the root directory of this source tree.
+ */const Fc=wc("Send",[["path",{d:"m22 2-7 20-4-9-9-4Z",key:"1q3vgg"}],["path",{d:"M22 2 11 13",key:"nzbqef"}]]);function Zs(e){const t=[],n=String(e||"");let r=n.indexOf(","),a=0,i=!1;for(;!i;){r===-1&&(r=n.length,i=!0);const o=n.slice(a,r).trim();(o||!i)&&t.push(o),a=r+1,r=n.indexOf(",",a)}return t}function Uc(e,t){const n={};return(e[e.length-1]===""?[...e,""]:e).join((n.padRight?" ":"")+","+(n.padLeft===!1?"":" ")).trim()}const Bc=/^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,Pc=/^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,$c={};function Xs(e,t){return($c.jsx?Pc:Bc).test(e)}const zc=/[ \t\n\f\r]/g;function Gc(e){return typeof e=="object"?e.type==="text"?Qs(e.value):!1:Qs(e)}function Qs(e){return e.replace(zc,"")===""}class ct{constructor(t,n,r){this.normal=n,this.property=t,r&&(this.space=r)}}ct.prototype.normal={};ct.prototype.property={};ct.prototype.space=void 0;function tu(e,t){const n={},r={};for(const a of e)Object.assign(n,a.property),Object.assign(r,a.normal);return new ct(n,r,t)}function ot(e){return e.toLowerCase()}class de{constructor(t,n){this.attribute=n,this.property=t}}de.prototype.attribute="";de.prototype.booleanish=!1;de.prototype.boolean=!1;de.prototype.commaOrSpaceSeparated=!1;de.prototype.commaSeparated=!1;de.prototype.defined=!1;de.prototype.mustUseProperty=!1;de.prototype.number=!1;de.prototype.overloadedBoolean=!1;de.prototype.property="";de.prototype.spaceSeparated=!1;de.prototype.space=void 0;let Hc=0;const $=De(),X=De(),pn=De(),N=De(),K=De(),ze=De(),ge=De();function De(){return 2**++Hc}const gn=Object.freeze(Object.defineProperty({__proto__:null,boolean:$,booleanish:X,commaOrSpaceSeparated:ge,commaSeparated:ze,number:N,overloadedBoolean:pn,spaceSeparated:K},Symbol.toStringTag,{value:"Module"})),Zt=Object.keys(gn);class _n extends de{constructor(t,n,r,a){let i=-1;if(super(t,n),Js(this,"space",a),typeof r=="number")for(;++i4&&n.slice(0,4)==="data"&&Yc.test(t)){if(t.charAt(4)==="-"){const i=t.slice(5).replace(el,Zc);r="data"+i.charAt(0).toUpperCase()+i.slice(1)}else{const i=t.slice(4);if(!el.test(i)){let o=i.replace(Wc,Kc);o.charAt(0)!=="-"&&(o="-"+o),t="data"+o}}a=_n}return new a(r,t)}function Kc(e){return"-"+e.toLowerCase()}function Zc(e){return e.charAt(1).toUpperCase()}const uu=tu([nu,jc,iu,ou,su],"html"),Dt=tu([nu,Vc,iu,ou,su],"svg");function tl(e){const t=String(e||"").trim();return t?t.split(/[ \t\n\r\f]+/g):[]}function Xc(e){return e.join(" ").trim()}var In={},nl=/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g,Qc=/\n/g,Jc=/^\s*/,ed=/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/,td=/^:\s*/,nd=/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/,rd=/^[;\s]*/,ad=/^\s+|\s+$/g,id=`
+`,rl="/",al="*",Oe="",od="comment",sd="declaration";function ld(e,t){if(typeof e!="string")throw new TypeError("First argument must be a string");if(!e)return[];t=t||{};var n=1,r=1;function a(m){var E=m.match(Qc);E&&(n+=E.length);var S=m.lastIndexOf(id);r=~S?m.length-S:r+m.length}function i(){var m={line:n,column:r};return function(E){return E.position=new o(m),u(),E}}function o(m){this.start=m,this.end={line:n,column:r},this.source=t.source}o.prototype.content=e;function s(m){var E=new Error(t.source+":"+n+":"+r+": "+m);if(E.reason=m,E.filename=t.source,E.line=n,E.column=r,E.source=e,!t.silent)throw E}function l(m){var E=m.exec(e);if(E){var S=E[0];return a(S),e=e.slice(S.length),E}}function u(){l(Jc)}function d(m){var E;for(m=m||[];E=c();)E!==!1&&m.push(E);return m}function c(){var m=i();if(!(rl!=e.charAt(0)||al!=e.charAt(1))){for(var E=2;Oe!=e.charAt(E)&&(al!=e.charAt(E)||rl!=e.charAt(E+1));)++E;if(E+=2,Oe===e.charAt(E-1))return s("End of comment missing");var S=e.slice(2,E-2);return r+=2,a(S),e=e.slice(E),r+=2,m({type:od,comment:S})}}function g(){var m=i(),E=l(ed);if(E){if(c(),!l(td))return s("property missing ':'");var S=l(nd),y=m({type:sd,property:il(E[0].replace(nl,Oe)),value:S?il(S[0].replace(nl,Oe)):Oe});return l(rd),y}}function p(){var m=[];d(m);for(var E;E=g();)E!==!1&&(m.push(E),d(m));return m}return u(),p()}function il(e){return e?e.replace(ad,Oe):Oe}var ud=ld,cd=xt&&xt.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(In,"__esModule",{value:!0});In.default=pd;const dd=cd(ud);function pd(e,t){let n=null;if(!e||typeof e!="string")return n;const r=(0,dd.default)(e),a=typeof t=="function";return r.forEach(i=>{if(i.type!=="declaration")return;const{property:o,value:s}=i;a?t(o,s,i):s&&(n=n||{},n[o]=s)}),n}var Mt={};Object.defineProperty(Mt,"__esModule",{value:!0});Mt.camelCase=void 0;var gd=/^--[a-zA-Z0-9_-]+$/,fd=/-([a-z])/g,md=/^[^-]+$/,bd=/^-(webkit|moz|ms|o|khtml)-/,hd=/^-(ms)-/,Ed=function(e){return!e||md.test(e)||gd.test(e)},yd=function(e,t){return t.toUpperCase()},ol=function(e,t){return"".concat(t,"-")},Sd=function(e,t){return t===void 0&&(t={}),Ed(e)?e:(e=e.toLowerCase(),t.reactCompat?e=e.replace(hd,ol):e=e.replace(bd,ol),e.replace(fd,yd))};Mt.camelCase=Sd;var kd=xt&&xt.__importDefault||function(e){return e&&e.__esModule?e:{default:e}},Td=kd(In),wd=Mt;function fn(e,t){var n={};return!e||typeof e!="string"||(0,Td.default)(e,function(r,a){r&&a&&(n[(0,wd.camelCase)(r,t)]=a)}),n}fn.default=fn;var Ad=fn;const _d=eu(Ad),cu=du("end"),Rn=du("start");function du(e){return t;function t(n){const r=n&&n.position&&n.position[e]||{};if(typeof r.line=="number"&&r.line>0&&typeof r.column=="number"&&r.column>0)return{line:r.line,column:r.column,offset:typeof r.offset=="number"&&r.offset>-1?r.offset:void 0}}}function Id(e){const t=Rn(e),n=cu(e);if(t&&n)return{start:t,end:n}}function nt(e){return!e||typeof e!="object"?"":"position"in e||"type"in e?sl(e.position):"start"in e||"end"in e?sl(e):"line"in e||"column"in e?mn(e):""}function mn(e){return ll(e&&e.line)+":"+ll(e&&e.column)}function sl(e){return mn(e&&e.start)+"-"+mn(e&&e.end)}function ll(e){return e&&typeof e=="number"?e:1}class ie extends Error{constructor(t,n,r){super(),typeof n=="string"&&(r=n,n=void 0);let a="",i={},o=!1;if(n&&("line"in n&&"column"in n?i={place:n}:"start"in n&&"end"in n?i={place:n}:"type"in n?i={ancestors:[n],place:n.position}:i={...n}),typeof t=="string"?a=t:!i.cause&&t&&(o=!0,a=t.message,i.cause=t),!i.ruleId&&!i.source&&typeof r=="string"){const l=r.indexOf(":");l===-1?i.ruleId=r:(i.source=r.slice(0,l),i.ruleId=r.slice(l+1))}if(!i.place&&i.ancestors&&i.ancestors){const l=i.ancestors[i.ancestors.length-1];l&&(i.place=l.position)}const s=i.place&&"start"in i.place?i.place.start:i.place;this.ancestors=i.ancestors||void 0,this.cause=i.cause||void 0,this.column=s?s.column:void 0,this.fatal=void 0,this.file="",this.message=a,this.line=s?s.line:void 0,this.name=nt(i.place)||"1:1",this.place=i.place||void 0,this.reason=this.message,this.ruleId=i.ruleId||void 0,this.source=i.source||void 0,this.stack=o&&i.cause&&typeof i.cause.stack=="string"?i.cause.stack:"",this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}}ie.prototype.file="";ie.prototype.name="";ie.prototype.reason="";ie.prototype.message="";ie.prototype.stack="";ie.prototype.column=void 0;ie.prototype.line=void 0;ie.prototype.ancestors=void 0;ie.prototype.cause=void 0;ie.prototype.fatal=void 0;ie.prototype.place=void 0;ie.prototype.ruleId=void 0;ie.prototype.source=void 0;const Nn={}.hasOwnProperty,Rd=new Map,Nd=/[A-Z]/g,xd=new Set(["table","tbody","thead","tfoot","tr"]),vd=new Set(["td","th"]),pu="https://github.com/syntax-tree/hast-util-to-jsx-runtime";function Cd(e,t){if(!t||t.Fragment===void 0)throw new TypeError("Expected `Fragment` in options");const n=t.filePath||void 0;let r;if(t.development){if(typeof t.jsxDEV!="function")throw new TypeError("Expected `jsxDEV` in options when `development: true`");r=Pd(n,t.jsxDEV)}else{if(typeof t.jsx!="function")throw new TypeError("Expected `jsx` in production options");if(typeof t.jsxs!="function")throw new TypeError("Expected `jsxs` in production options");r=Bd(n,t.jsx,t.jsxs)}const a={Fragment:t.Fragment,ancestors:[],components:t.components||{},create:r,elementAttributeNameCase:t.elementAttributeNameCase||"react",evaluater:t.createEvaluater?t.createEvaluater():void 0,filePath:n,ignoreInvalidStyle:t.ignoreInvalidStyle||!1,passKeys:t.passKeys!==!1,passNode:t.passNode||!1,schema:t.space==="svg"?Dt:uu,stylePropertyNameCase:t.stylePropertyNameCase||"dom",tableCellAlignToStyle:t.tableCellAlignToStyle!==!1},i=gu(a,e,void 0);return i&&typeof i!="string"?i:a.create(e,a.Fragment,{children:i||void 0},void 0)}function gu(e,t,n){if(t.type==="element")return Od(e,t,n);if(t.type==="mdxFlowExpression"||t.type==="mdxTextExpression")return Ld(e,t);if(t.type==="mdxJsxFlowElement"||t.type==="mdxJsxTextElement")return Md(e,t,n);if(t.type==="mdxjsEsm")return Dd(e,t);if(t.type==="root")return Fd(e,t,n);if(t.type==="text")return Ud(e,t)}function Od(e,t,n){const r=e.schema;let a=r;t.tagName.toLowerCase()==="svg"&&r.space==="html"&&(a=Dt,e.schema=a),e.ancestors.push(t);const i=mu(e,t.tagName,!1),o=$d(e,t);let s=vn(e,t);return xd.has(t.tagName)&&(s=s.filter(function(l){return typeof l=="string"?!Gc(l):!0})),fu(e,o,i,t),xn(o,s),e.ancestors.pop(),e.schema=r,e.create(t,i,o,n)}function Ld(e,t){if(t.data&&t.data.estree&&e.evaluater){const r=t.data.estree.body[0];return r.type,e.evaluater.evaluateExpression(r.expression)}st(e,t.position)}function Dd(e,t){if(t.data&&t.data.estree&&e.evaluater)return e.evaluater.evaluateProgram(t.data.estree);st(e,t.position)}function Md(e,t,n){const r=e.schema;let a=r;t.name==="svg"&&r.space==="html"&&(a=Dt,e.schema=a),e.ancestors.push(t);const i=t.name===null?e.Fragment:mu(e,t.name,!0),o=zd(e,t),s=vn(e,t);return fu(e,o,i,t),xn(o,s),e.ancestors.pop(),e.schema=r,e.create(t,i,o,n)}function Fd(e,t,n){const r={};return xn(r,vn(e,t)),e.create(t,e.Fragment,r,n)}function Ud(e,t){return t.value}function fu(e,t,n,r){typeof n!="string"&&n!==e.Fragment&&e.passNode&&(t.node=r)}function xn(e,t){if(t.length>0){const n=t.length>1?t:t[0];n&&(e.children=n)}}function Bd(e,t,n){return r;function r(a,i,o,s){const u=Array.isArray(o.children)?n:t;return s?u(i,o,s):u(i,o)}}function Pd(e,t){return n;function n(r,a,i,o){const s=Array.isArray(i.children),l=Rn(r);return t(a,i,o,s,{columnNumber:l?l.column-1:void 0,fileName:e,lineNumber:l?l.line:void 0},void 0)}}function $d(e,t){const n={};let r,a;for(a in t.properties)if(a!=="children"&&Nn.call(t.properties,a)){const i=Gd(e,a,t.properties[a]);if(i){const[o,s]=i;e.tableCellAlignToStyle&&o==="align"&&typeof s=="string"&&vd.has(t.tagName)?r=s:n[o]=s}}if(r){const i=n.style||(n.style={});i[e.stylePropertyNameCase==="css"?"text-align":"textAlign"]=r}return n}function zd(e,t){const n={};for(const r of t.attributes)if(r.type==="mdxJsxExpressionAttribute")if(r.data&&r.data.estree&&e.evaluater){const i=r.data.estree.body[0];i.type;const o=i.expression;o.type;const s=o.properties[0];s.type,Object.assign(n,e.evaluater.evaluateExpression(s.argument))}else st(e,t.position);else{const a=r.name;let i;if(r.value&&typeof r.value=="object")if(r.value.data&&r.value.data.estree&&e.evaluater){const s=r.value.data.estree.body[0];s.type,i=e.evaluater.evaluateExpression(s.expression)}else st(e,t.position);else i=r.value===null?!0:r.value;n[a]=i}return n}function vn(e,t){const n=[];let r=-1;const a=e.passKeys?new Map:Rd;for(;++ra?0:a+t:t=t>a?a:t,n=n>0?n:0,r.length<1e4)o=Array.from(r),o.unshift(t,n),e.splice(...o);else for(n&&e.splice(t,n);i0?(fe(e,e.length,0,t),e):t}const dl={}.hasOwnProperty;function hu(e){const t={};let n=-1;for(;++n13&&n<32||n>126&&n<160||n>55295&&n<57344||n>64975&&n<65008||(n&65535)===65535||(n&65535)===65534||n>1114111?"�":String.fromCodePoint(n)}function Ee(e){return e.replace(/[\t\n\r ]+/g," ").replace(/^ | $/g,"").toLowerCase().toUpperCase()}const le=Re(/[A-Za-z]/),ae=Re(/[\dA-Za-z]/),Xd=Re(/[#-'*+\--9=?A-Z^-~]/);function vt(e){return e!==null&&(e<32||e===127)}const bn=Re(/\d/),Qd=Re(/[\dA-Fa-f]/),Jd=Re(/[!-/:-@[-`{-~]/);function F(e){return e!==null&&e<-2}function Y(e){return e!==null&&(e<0||e===32)}function G(e){return e===-2||e===-1||e===32}const Ft=Re(new RegExp("\\p{P}|\\p{S}","u")),Le=Re(/\s/);function Re(e){return t;function t(n){return n!==null&&n>-1&&e.test(String.fromCharCode(n))}}function je(e){const t=[];let n=-1,r=0,a=0;for(;++n55295&&i<57344){const s=e.charCodeAt(n+1);i<56320&&s>56319&&s<57344?(o=String.fromCharCode(i,s),a=1):o="�"}else o=String.fromCharCode(i);o&&(t.push(e.slice(r,n),encodeURIComponent(o)),r=n+a+1,o=""),a&&(n+=a,a=0)}return t.join("")+e.slice(r)}function V(e,t,n,r){const a=r?r-1:Number.POSITIVE_INFINITY;let i=0;return o;function o(l){return G(l)?(e.enter(n),s(l)):t(l)}function s(l){return G(l)&&i++o))return;const D=t.events.length;let M=D,U,k;for(;M--;)if(t.events[M][0]==="exit"&&t.events[M][1].type==="chunkFlow"){if(U){k=t.events[M][1].end;break}U=!0}for(y(r),T=D;Tw;){const v=n[R];t.containerState=v[1],v[0].exit.call(t,e)}n.length=w}function A(){a.write([null]),i=void 0,a=void 0,t.containerState._closeFlow=void 0}}function ap(e,t,n){return V(e,e.attempt(this.parser.constructs.document,t,n),"linePrefix",this.parser.constructs.disable.null.includes("codeIndented")?void 0:4)}function Ge(e){if(e===null||Y(e)||Le(e))return 1;if(Ft(e))return 2}function Ut(e,t,n){const r=[];let a=-1;for(;++a1&&e[n][1].end.offset-e[n][1].start.offset>1?2:1;const c={...e[r][1].end},g={...e[n][1].start};gl(c,-l),gl(g,l),o={type:l>1?"strongSequence":"emphasisSequence",start:c,end:{...e[r][1].end}},s={type:l>1?"strongSequence":"emphasisSequence",start:{...e[n][1].start},end:g},i={type:l>1?"strongText":"emphasisText",start:{...e[r][1].end},end:{...e[n][1].start}},a={type:l>1?"strong":"emphasis",start:{...o.start},end:{...s.end}},e[r][1].end={...o.start},e[n][1].start={...s.end},u=[],e[r][1].end.offset-e[r][1].start.offset&&(u=me(u,[["enter",e[r][1],t],["exit",e[r][1],t]])),u=me(u,[["enter",a,t],["enter",o,t],["exit",o,t],["enter",i,t]]),u=me(u,Ut(t.parser.constructs.insideSpan.null,e.slice(r+1,n),t)),u=me(u,[["exit",i,t],["enter",s,t],["exit",s,t],["exit",a,t]]),e[n][1].end.offset-e[n][1].start.offset?(d=2,u=me(u,[["enter",e[n][1],t],["exit",e[n][1],t]])):d=0,fe(e,r-1,n-r+3,u),n=r+u.length-d-2;break}}for(n=-1;++n0&&G(T)?V(e,A,"linePrefix",i+1)(T):A(T)}function A(T){return T===null||F(T)?e.check(fl,E,R)(T):(e.enter("codeFlowValue"),w(T))}function w(T){return T===null||F(T)?(e.exit("codeFlowValue"),A(T)):(e.consume(T),w)}function R(T){return e.exit("codeFenced"),t(T)}function v(T,D,M){let U=0;return k;function k(P){return T.enter("lineEnding"),T.consume(P),T.exit("lineEnding"),C}function C(P){return T.enter("codeFencedFence"),G(P)?V(T,O,"linePrefix",r.parser.constructs.disable.null.includes("codeIndented")?void 0:4)(P):O(P)}function O(P){return P===s?(T.enter("codeFencedFenceSequence"),L(P)):M(P)}function L(P){return P===s?(U++,T.consume(P),L):U>=o?(T.exit("codeFencedFenceSequence"),G(P)?V(T,z,"whitespace")(P):z(P)):M(P)}function z(P){return P===null||F(P)?(T.exit("codeFencedFence"),D(P)):M(P)}}}function bp(e,t,n){const r=this;return a;function a(o){return o===null?n(o):(e.enter("lineEnding"),e.consume(o),e.exit("lineEnding"),i)}function i(o){return r.parser.lazy[r.now().line]?n(o):t(o)}}const Qt={name:"codeIndented",tokenize:Ep},hp={partial:!0,tokenize:yp};function Ep(e,t,n){const r=this;return a;function a(u){return e.enter("codeIndented"),V(e,i,"linePrefix",5)(u)}function i(u){const d=r.events[r.events.length-1];return d&&d[1].type==="linePrefix"&&d[2].sliceSerialize(d[1],!0).length>=4?o(u):n(u)}function o(u){return u===null?l(u):F(u)?e.attempt(hp,o,l)(u):(e.enter("codeFlowValue"),s(u))}function s(u){return u===null||F(u)?(e.exit("codeFlowValue"),o(u)):(e.consume(u),s)}function l(u){return e.exit("codeIndented"),t(u)}}function yp(e,t,n){const r=this;return a;function a(o){return r.parser.lazy[r.now().line]?n(o):F(o)?(e.enter("lineEnding"),e.consume(o),e.exit("lineEnding"),a):V(e,i,"linePrefix",5)(o)}function i(o){const s=r.events[r.events.length-1];return s&&s[1].type==="linePrefix"&&s[2].sliceSerialize(s[1],!0).length>=4?t(o):F(o)?a(o):n(o)}}const Sp={name:"codeText",previous:Tp,resolve:kp,tokenize:wp};function kp(e){let t=e.length-4,n=3,r,a;if((e[n][1].type==="lineEnding"||e[n][1].type==="space")&&(e[t][1].type==="lineEnding"||e[t][1].type==="space")){for(r=n;++r=this.left.length+this.right.length)throw new RangeError("Cannot access index `"+t+"` in a splice buffer of size `"+(this.left.length+this.right.length)+"`");return tthis.left.length?this.right.slice(this.right.length-r+this.left.length,this.right.length-t+this.left.length).reverse():this.left.slice(t).concat(this.right.slice(this.right.length-r+this.left.length).reverse())}splice(t,n,r){const a=n||0;this.setCursor(Math.trunc(t));const i=this.right.splice(this.right.length-a,Number.POSITIVE_INFINITY);return r&&et(this.left,r),i.reverse()}pop(){return this.setCursor(Number.POSITIVE_INFINITY),this.left.pop()}push(t){this.setCursor(Number.POSITIVE_INFINITY),this.left.push(t)}pushMany(t){this.setCursor(Number.POSITIVE_INFINITY),et(this.left,t)}unshift(t){this.setCursor(0),this.right.push(t)}unshiftMany(t){this.setCursor(0),et(this.right,t.reverse())}setCursor(t){if(!(t===this.left.length||t>this.left.length&&this.right.length===0||t<0&&this.left.length===0))if(t=4?t(o):e.interrupt(r.parser.constructs.flow,n,t)(o)}}function wu(e,t,n,r,a,i,o,s,l){const u=l||Number.POSITIVE_INFINITY;let d=0;return c;function c(y){return y===60?(e.enter(r),e.enter(a),e.enter(i),e.consume(y),e.exit(i),g):y===null||y===32||y===41||vt(y)?n(y):(e.enter(r),e.enter(o),e.enter(s),e.enter("chunkString",{contentType:"string"}),E(y))}function g(y){return y===62?(e.enter(i),e.consume(y),e.exit(i),e.exit(a),e.exit(r),t):(e.enter(s),e.enter("chunkString",{contentType:"string"}),p(y))}function p(y){return y===62?(e.exit("chunkString"),e.exit(s),g(y)):y===null||y===60||F(y)?n(y):(e.consume(y),y===92?m:p)}function m(y){return y===60||y===62||y===92?(e.consume(y),p):p(y)}function E(y){return!d&&(y===null||y===41||Y(y))?(e.exit("chunkString"),e.exit(s),e.exit(o),e.exit(r),t(y)):d999||p===null||p===91||p===93&&!l||p===94&&!s&&"_hiddenFootnoteSupport"in o.parser.constructs?n(p):p===93?(e.exit(i),e.enter(a),e.consume(p),e.exit(a),e.exit(r),t):F(p)?(e.enter("lineEnding"),e.consume(p),e.exit("lineEnding"),d):(e.enter("chunkString",{contentType:"string"}),c(p))}function c(p){return p===null||p===91||p===93||F(p)||s++>999?(e.exit("chunkString"),d(p)):(e.consume(p),l||(l=!G(p)),p===92?g:c)}function g(p){return p===91||p===92||p===93?(e.consume(p),s++,c):c(p)}}function _u(e,t,n,r,a,i){let o;return s;function s(g){return g===34||g===39||g===40?(e.enter(r),e.enter(a),e.consume(g),e.exit(a),o=g===40?41:g,l):n(g)}function l(g){return g===o?(e.enter(a),e.consume(g),e.exit(a),e.exit(r),t):(e.enter(i),u(g))}function u(g){return g===o?(e.exit(i),l(o)):g===null?n(g):F(g)?(e.enter("lineEnding"),e.consume(g),e.exit("lineEnding"),V(e,u,"linePrefix")):(e.enter("chunkString",{contentType:"string"}),d(g))}function d(g){return g===o||g===null||F(g)?(e.exit("chunkString"),u(g)):(e.consume(g),g===92?c:d)}function c(g){return g===o||g===92?(e.consume(g),d):d(g)}}function rt(e,t){let n;return r;function r(a){return F(a)?(e.enter("lineEnding"),e.consume(a),e.exit("lineEnding"),n=!0,r):G(a)?V(e,r,n?"linePrefix":"lineSuffix")(a):t(a)}}const Cp={name:"definition",tokenize:Lp},Op={partial:!0,tokenize:Dp};function Lp(e,t,n){const r=this;let a;return i;function i(p){return e.enter("definition"),o(p)}function o(p){return Au.call(r,e,s,n,"definitionLabel","definitionLabelMarker","definitionLabelString")(p)}function s(p){return a=Ee(r.sliceSerialize(r.events[r.events.length-1][1]).slice(1,-1)),p===58?(e.enter("definitionMarker"),e.consume(p),e.exit("definitionMarker"),l):n(p)}function l(p){return Y(p)?rt(e,u)(p):u(p)}function u(p){return wu(e,d,n,"definitionDestination","definitionDestinationLiteral","definitionDestinationLiteralMarker","definitionDestinationRaw","definitionDestinationString")(p)}function d(p){return e.attempt(Op,c,c)(p)}function c(p){return G(p)?V(e,g,"whitespace")(p):g(p)}function g(p){return p===null||F(p)?(e.exit("definition"),r.parser.defined.push(a),t(p)):n(p)}}function Dp(e,t,n){return r;function r(s){return Y(s)?rt(e,a)(s):n(s)}function a(s){return _u(e,i,n,"definitionTitle","definitionTitleMarker","definitionTitleString")(s)}function i(s){return G(s)?V(e,o,"whitespace")(s):o(s)}function o(s){return s===null||F(s)?t(s):n(s)}}const Mp={name:"hardBreakEscape",tokenize:Fp};function Fp(e,t,n){return r;function r(i){return e.enter("hardBreakEscape"),e.consume(i),a}function a(i){return F(i)?(e.exit("hardBreakEscape"),t(i)):n(i)}}const Up={name:"headingAtx",resolve:Bp,tokenize:Pp};function Bp(e,t){let n=e.length-2,r=3,a,i;return e[r][1].type==="whitespace"&&(r+=2),n-2>r&&e[n][1].type==="whitespace"&&(n-=2),e[n][1].type==="atxHeadingSequence"&&(r===n-1||n-4>r&&e[n-2][1].type==="whitespace")&&(n-=r+1===n?2:4),n>r&&(a={type:"atxHeadingText",start:e[r][1].start,end:e[n][1].end},i={type:"chunkText",start:e[r][1].start,end:e[n][1].end,contentType:"text"},fe(e,r,n-r+1,[["enter",a,t],["enter",i,t],["exit",i,t],["exit",a,t]])),e}function Pp(e,t,n){let r=0;return a;function a(d){return e.enter("atxHeading"),i(d)}function i(d){return e.enter("atxHeadingSequence"),o(d)}function o(d){return d===35&&r++<6?(e.consume(d),o):d===null||Y(d)?(e.exit("atxHeadingSequence"),s(d)):n(d)}function s(d){return d===35?(e.enter("atxHeadingSequence"),l(d)):d===null||F(d)?(e.exit("atxHeading"),t(d)):G(d)?V(e,s,"whitespace")(d):(e.enter("atxHeadingText"),u(d))}function l(d){return d===35?(e.consume(d),l):(e.exit("atxHeadingSequence"),s(d))}function u(d){return d===null||d===35||Y(d)?(e.exit("atxHeadingText"),s(d)):(e.consume(d),u)}}const $p=["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","search","section","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"],bl=["pre","script","style","textarea"],zp={concrete:!0,name:"htmlFlow",resolveTo:jp,tokenize:Vp},Gp={partial:!0,tokenize:Wp},Hp={partial:!0,tokenize:qp};function jp(e){let t=e.length;for(;t--&&!(e[t][0]==="enter"&&e[t][1].type==="htmlFlow"););return t>1&&e[t-2][1].type==="linePrefix"&&(e[t][1].start=e[t-2][1].start,e[t+1][1].start=e[t-2][1].start,e.splice(t-2,2)),e}function Vp(e,t,n){const r=this;let a,i,o,s,l;return u;function u(h){return d(h)}function d(h){return e.enter("htmlFlow"),e.enter("htmlFlowData"),e.consume(h),c}function c(h){return h===33?(e.consume(h),g):h===47?(e.consume(h),i=!0,E):h===63?(e.consume(h),a=3,r.interrupt?t:b):le(h)?(e.consume(h),o=String.fromCharCode(h),S):n(h)}function g(h){return h===45?(e.consume(h),a=2,p):h===91?(e.consume(h),a=5,s=0,m):le(h)?(e.consume(h),a=4,r.interrupt?t:b):n(h)}function p(h){return h===45?(e.consume(h),r.interrupt?t:b):n(h)}function m(h){const be="CDATA[";return h===be.charCodeAt(s++)?(e.consume(h),s===be.length?r.interrupt?t:O:m):n(h)}function E(h){return le(h)?(e.consume(h),o=String.fromCharCode(h),S):n(h)}function S(h){if(h===null||h===47||h===62||Y(h)){const be=h===47,Ne=o.toLowerCase();return!be&&!i&&bl.includes(Ne)?(a=1,r.interrupt?t(h):O(h)):$p.includes(o.toLowerCase())?(a=6,be?(e.consume(h),y):r.interrupt?t(h):O(h)):(a=7,r.interrupt&&!r.parser.lazy[r.now().line]?n(h):i?A(h):w(h))}return h===45||ae(h)?(e.consume(h),o+=String.fromCharCode(h),S):n(h)}function y(h){return h===62?(e.consume(h),r.interrupt?t:O):n(h)}function A(h){return G(h)?(e.consume(h),A):k(h)}function w(h){return h===47?(e.consume(h),k):h===58||h===95||le(h)?(e.consume(h),R):G(h)?(e.consume(h),w):k(h)}function R(h){return h===45||h===46||h===58||h===95||ae(h)?(e.consume(h),R):v(h)}function v(h){return h===61?(e.consume(h),T):G(h)?(e.consume(h),v):w(h)}function T(h){return h===null||h===60||h===61||h===62||h===96?n(h):h===34||h===39?(e.consume(h),l=h,D):G(h)?(e.consume(h),T):M(h)}function D(h){return h===l?(e.consume(h),l=null,U):h===null||F(h)?n(h):(e.consume(h),D)}function M(h){return h===null||h===34||h===39||h===47||h===60||h===61||h===62||h===96||Y(h)?v(h):(e.consume(h),M)}function U(h){return h===47||h===62||G(h)?w(h):n(h)}function k(h){return h===62?(e.consume(h),C):n(h)}function C(h){return h===null||F(h)?O(h):G(h)?(e.consume(h),C):n(h)}function O(h){return h===45&&a===2?(e.consume(h),Z):h===60&&a===1?(e.consume(h),q):h===62&&a===4?(e.consume(h),Q):h===63&&a===3?(e.consume(h),b):h===93&&a===5?(e.consume(h),re):F(h)&&(a===6||a===7)?(e.exit("htmlFlowData"),e.check(Gp,ue,L)(h)):h===null||F(h)?(e.exit("htmlFlowData"),L(h)):(e.consume(h),O)}function L(h){return e.check(Hp,z,ue)(h)}function z(h){return e.enter("lineEnding"),e.consume(h),e.exit("lineEnding"),P}function P(h){return h===null||F(h)?L(h):(e.enter("htmlFlowData"),O(h))}function Z(h){return h===45?(e.consume(h),b):O(h)}function q(h){return h===47?(e.consume(h),o="",ee):O(h)}function ee(h){if(h===62){const be=o.toLowerCase();return bl.includes(be)?(e.consume(h),Q):O(h)}return le(h)&&o.length<8?(e.consume(h),o+=String.fromCharCode(h),ee):O(h)}function re(h){return h===93?(e.consume(h),b):O(h)}function b(h){return h===62?(e.consume(h),Q):h===45&&a===2?(e.consume(h),b):O(h)}function Q(h){return h===null||F(h)?(e.exit("htmlFlowData"),ue(h)):(e.consume(h),Q)}function ue(h){return e.exit("htmlFlow"),t(h)}}function qp(e,t,n){const r=this;return a;function a(o){return F(o)?(e.enter("lineEnding"),e.consume(o),e.exit("lineEnding"),i):n(o)}function i(o){return r.parser.lazy[r.now().line]?n(o):t(o)}}function Wp(e,t,n){return r;function r(a){return e.enter("lineEnding"),e.consume(a),e.exit("lineEnding"),e.attempt(dt,t,n)}}const Yp={name:"htmlText",tokenize:Kp};function Kp(e,t,n){const r=this;let a,i,o;return s;function s(b){return e.enter("htmlText"),e.enter("htmlTextData"),e.consume(b),l}function l(b){return b===33?(e.consume(b),u):b===47?(e.consume(b),v):b===63?(e.consume(b),w):le(b)?(e.consume(b),M):n(b)}function u(b){return b===45?(e.consume(b),d):b===91?(e.consume(b),i=0,m):le(b)?(e.consume(b),A):n(b)}function d(b){return b===45?(e.consume(b),p):n(b)}function c(b){return b===null?n(b):b===45?(e.consume(b),g):F(b)?(o=c,q(b)):(e.consume(b),c)}function g(b){return b===45?(e.consume(b),p):c(b)}function p(b){return b===62?Z(b):b===45?g(b):c(b)}function m(b){const Q="CDATA[";return b===Q.charCodeAt(i++)?(e.consume(b),i===Q.length?E:m):n(b)}function E(b){return b===null?n(b):b===93?(e.consume(b),S):F(b)?(o=E,q(b)):(e.consume(b),E)}function S(b){return b===93?(e.consume(b),y):E(b)}function y(b){return b===62?Z(b):b===93?(e.consume(b),y):E(b)}function A(b){return b===null||b===62?Z(b):F(b)?(o=A,q(b)):(e.consume(b),A)}function w(b){return b===null?n(b):b===63?(e.consume(b),R):F(b)?(o=w,q(b)):(e.consume(b),w)}function R(b){return b===62?Z(b):w(b)}function v(b){return le(b)?(e.consume(b),T):n(b)}function T(b){return b===45||ae(b)?(e.consume(b),T):D(b)}function D(b){return F(b)?(o=D,q(b)):G(b)?(e.consume(b),D):Z(b)}function M(b){return b===45||ae(b)?(e.consume(b),M):b===47||b===62||Y(b)?U(b):n(b)}function U(b){return b===47?(e.consume(b),Z):b===58||b===95||le(b)?(e.consume(b),k):F(b)?(o=U,q(b)):G(b)?(e.consume(b),U):Z(b)}function k(b){return b===45||b===46||b===58||b===95||ae(b)?(e.consume(b),k):C(b)}function C(b){return b===61?(e.consume(b),O):F(b)?(o=C,q(b)):G(b)?(e.consume(b),C):U(b)}function O(b){return b===null||b===60||b===61||b===62||b===96?n(b):b===34||b===39?(e.consume(b),a=b,L):F(b)?(o=O,q(b)):G(b)?(e.consume(b),O):(e.consume(b),z)}function L(b){return b===a?(e.consume(b),a=void 0,P):b===null?n(b):F(b)?(o=L,q(b)):(e.consume(b),L)}function z(b){return b===null||b===34||b===39||b===60||b===61||b===96?n(b):b===47||b===62||Y(b)?U(b):(e.consume(b),z)}function P(b){return b===47||b===62||Y(b)?U(b):n(b)}function Z(b){return b===62?(e.consume(b),e.exit("htmlTextData"),e.exit("htmlText"),t):n(b)}function q(b){return e.exit("htmlTextData"),e.enter("lineEnding"),e.consume(b),e.exit("lineEnding"),ee}function ee(b){return G(b)?V(e,re,"linePrefix",r.parser.constructs.disable.null.includes("codeIndented")?void 0:4)(b):re(b)}function re(b){return e.enter("htmlTextData"),o(b)}}const On={name:"labelEnd",resolveAll:Jp,resolveTo:eg,tokenize:tg},Zp={tokenize:ng},Xp={tokenize:rg},Qp={tokenize:ag};function Jp(e){let t=-1;const n=[];for(;++t=3&&(u===null||F(u))?(e.exit("thematicBreak"),t(u)):n(u)}function l(u){return u===a?(e.consume(u),r++,l):(e.exit("thematicBreakSequence"),G(u)?V(e,s,"whitespace")(u):s(u))}}const ce={continuation:{tokenize:fg},exit:bg,name:"list",tokenize:gg},dg={partial:!0,tokenize:hg},pg={partial:!0,tokenize:mg};function gg(e,t,n){const r=this,a=r.events[r.events.length-1];let i=a&&a[1].type==="linePrefix"?a[2].sliceSerialize(a[1],!0).length:0,o=0;return s;function s(p){const m=r.containerState.type||(p===42||p===43||p===45?"listUnordered":"listOrdered");if(m==="listUnordered"?!r.containerState.marker||p===r.containerState.marker:bn(p)){if(r.containerState.type||(r.containerState.type=m,e.enter(m,{_container:!0})),m==="listUnordered")return e.enter("listItemPrefix"),p===42||p===45?e.check(_t,n,u)(p):u(p);if(!r.interrupt||p===49)return e.enter("listItemPrefix"),e.enter("listItemValue"),l(p)}return n(p)}function l(p){return bn(p)&&++o<10?(e.consume(p),l):(!r.interrupt||o<2)&&(r.containerState.marker?p===r.containerState.marker:p===41||p===46)?(e.exit("listItemValue"),u(p)):n(p)}function u(p){return e.enter("listItemMarker"),e.consume(p),e.exit("listItemMarker"),r.containerState.marker=r.containerState.marker||p,e.check(dt,r.interrupt?n:d,e.attempt(dg,g,c))}function d(p){return r.containerState.initialBlankLine=!0,i++,g(p)}function c(p){return G(p)?(e.enter("listItemPrefixWhitespace"),e.consume(p),e.exit("listItemPrefixWhitespace"),g):n(p)}function g(p){return r.containerState.size=i+r.sliceSerialize(e.exit("listItemPrefix"),!0).length,t(p)}}function fg(e,t,n){const r=this;return r.containerState._closeFlow=void 0,e.check(dt,a,i);function a(s){return r.containerState.furtherBlankLines=r.containerState.furtherBlankLines||r.containerState.initialBlankLine,V(e,t,"listItemIndent",r.containerState.size+1)(s)}function i(s){return r.containerState.furtherBlankLines||!G(s)?(r.containerState.furtherBlankLines=void 0,r.containerState.initialBlankLine=void 0,o(s)):(r.containerState.furtherBlankLines=void 0,r.containerState.initialBlankLine=void 0,e.attempt(pg,t,o)(s))}function o(s){return r.containerState._closeFlow=!0,r.interrupt=void 0,V(e,e.attempt(ce,t,n),"linePrefix",r.parser.constructs.disable.null.includes("codeIndented")?void 0:4)(s)}}function mg(e,t,n){const r=this;return V(e,a,"listItemIndent",r.containerState.size+1);function a(i){const o=r.events[r.events.length-1];return o&&o[1].type==="listItemIndent"&&o[2].sliceSerialize(o[1],!0).length===r.containerState.size?t(i):n(i)}}function bg(e){e.exit(this.containerState.type)}function hg(e,t,n){const r=this;return V(e,a,"listItemPrefixWhitespace",r.parser.constructs.disable.null.includes("codeIndented")?void 0:5);function a(i){const o=r.events[r.events.length-1];return!G(i)&&o&&o[1].type==="listItemPrefixWhitespace"?t(i):n(i)}}const hl={name:"setextUnderline",resolveTo:Eg,tokenize:yg};function Eg(e,t){let n=e.length,r,a,i;for(;n--;)if(e[n][0]==="enter"){if(e[n][1].type==="content"){r=n;break}e[n][1].type==="paragraph"&&(a=n)}else e[n][1].type==="content"&&e.splice(n,1),!i&&e[n][1].type==="definition"&&(i=n);const o={type:"setextHeading",start:{...e[r][1].start},end:{...e[e.length-1][1].end}};return e[a][1].type="setextHeadingText",i?(e.splice(a,0,["enter",o,t]),e.splice(i+1,0,["exit",e[r][1],t]),e[r][1].end={...e[i][1].end}):e[r][1]=o,e.push(["exit",o,t]),e}function yg(e,t,n){const r=this;let a;return i;function i(u){let d=r.events.length,c;for(;d--;)if(r.events[d][1].type!=="lineEnding"&&r.events[d][1].type!=="linePrefix"&&r.events[d][1].type!=="content"){c=r.events[d][1].type==="paragraph";break}return!r.parser.lazy[r.now().line]&&(r.interrupt||c)?(e.enter("setextHeadingLine"),a=u,o(u)):n(u)}function o(u){return e.enter("setextHeadingLineSequence"),s(u)}function s(u){return u===a?(e.consume(u),s):(e.exit("setextHeadingLineSequence"),G(u)?V(e,l,"lineSuffix")(u):l(u))}function l(u){return u===null||F(u)?(e.exit("setextHeadingLine"),t(u)):n(u)}}const Sg={tokenize:kg};function kg(e){const t=this,n=e.attempt(dt,r,e.attempt(this.parser.constructs.flowInitial,a,V(e,e.attempt(this.parser.constructs.flow,a,e.attempt(Ip,a)),"linePrefix")));return n;function r(i){if(i===null){e.consume(i);return}return e.enter("lineEndingBlank"),e.consume(i),e.exit("lineEndingBlank"),t.currentConstruct=void 0,n}function a(i){if(i===null){e.consume(i);return}return e.enter("lineEnding"),e.consume(i),e.exit("lineEnding"),t.currentConstruct=void 0,n}}const Tg={resolveAll:Ru()},wg=Iu("string"),Ag=Iu("text");function Iu(e){return{resolveAll:Ru(e==="text"?_g:void 0),tokenize:t};function t(n){const r=this,a=this.parser.constructs[e],i=n.attempt(a,o,s);return o;function o(d){return u(d)?i(d):s(d)}function s(d){if(d===null){n.consume(d);return}return n.enter("data"),n.consume(d),l}function l(d){return u(d)?(n.exit("data"),i(d)):(n.consume(d),l)}function u(d){if(d===null)return!0;const c=a[d];let g=-1;if(c)for(;++g-1){const s=o[0];typeof s=="string"?o[0]=s.slice(r):o.shift()}i>0&&o.push(e[a].slice(0,i))}return o}function Bg(e,t){let n=-1;const r=[];let a;for(;++n0){const he=B.tokenStack[B.tokenStack.length-1];(he[1]||yl).call(B,void 0,he[0])}for(x.position={start:Ie(_.length>0?_[0][1].start:{line:1,column:1,offset:0}),end:Ie(_.length>0?_[_.length-2][1].end:{line:1,column:1,offset:0})},W=-1;++W0&&(r.className=["language-"+a[0]]);let i={type:"element",tagName:"code",properties:r,children:[{type:"text",value:n}]};return t.meta&&(i.data={meta:t.meta}),e.patch(t,i),i=e.applyData(t,i),i={type:"element",tagName:"pre",properties:{},children:[i]},e.patch(t,i),i}function Qg(e,t){const n={type:"element",tagName:"del",properties:{},children:e.all(t)};return e.patch(t,n),e.applyData(t,n)}function Jg(e,t){const n={type:"element",tagName:"em",properties:{},children:e.all(t)};return e.patch(t,n),e.applyData(t,n)}function ef(e,t){const n=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",r=String(t.identifier).toUpperCase(),a=je(r.toLowerCase()),i=e.footnoteOrder.indexOf(r);let o,s=e.footnoteCounts.get(r);s===void 0?(s=0,e.footnoteOrder.push(r),o=e.footnoteOrder.length):o=i+1,s+=1,e.footnoteCounts.set(r,s);const l={type:"element",tagName:"a",properties:{href:"#"+n+"fn-"+a,id:n+"fnref-"+a+(s>1?"-"+s:""),dataFootnoteRef:!0,ariaDescribedBy:["footnote-label"]},children:[{type:"text",value:String(o)}]};e.patch(t,l);const u={type:"element",tagName:"sup",properties:{},children:[l]};return e.patch(t,u),e.applyData(t,u)}function tf(e,t){const n={type:"element",tagName:"h"+t.depth,properties:{},children:e.all(t)};return e.patch(t,n),e.applyData(t,n)}function nf(e,t){if(e.options.allowDangerousHtml){const n={type:"raw",value:t.value};return e.patch(t,n),e.applyData(t,n)}}function vu(e,t){const n=t.referenceType;let r="]";if(n==="collapsed"?r+="[]":n==="full"&&(r+="["+(t.label||t.identifier)+"]"),t.type==="imageReference")return[{type:"text",value:"!["+t.alt+r}];const a=e.all(t),i=a[0];i&&i.type==="text"?i.value="["+i.value:a.unshift({type:"text",value:"["});const o=a[a.length-1];return o&&o.type==="text"?o.value+=r:a.push({type:"text",value:r}),a}function rf(e,t){const n=String(t.identifier).toUpperCase(),r=e.definitionById.get(n);if(!r)return vu(e,t);const a={src:je(r.url||""),alt:t.alt};r.title!==null&&r.title!==void 0&&(a.title=r.title);const i={type:"element",tagName:"img",properties:a,children:[]};return e.patch(t,i),e.applyData(t,i)}function af(e,t){const n={src:je(t.url)};t.alt!==null&&t.alt!==void 0&&(n.alt=t.alt),t.title!==null&&t.title!==void 0&&(n.title=t.title);const r={type:"element",tagName:"img",properties:n,children:[]};return e.patch(t,r),e.applyData(t,r)}function of(e,t){const n={type:"text",value:t.value.replace(/\r?\n|\r/g," ")};e.patch(t,n);const r={type:"element",tagName:"code",properties:{},children:[n]};return e.patch(t,r),e.applyData(t,r)}function sf(e,t){const n=String(t.identifier).toUpperCase(),r=e.definitionById.get(n);if(!r)return vu(e,t);const a={href:je(r.url||"")};r.title!==null&&r.title!==void 0&&(a.title=r.title);const i={type:"element",tagName:"a",properties:a,children:e.all(t)};return e.patch(t,i),e.applyData(t,i)}function lf(e,t){const n={href:je(t.url)};t.title!==null&&t.title!==void 0&&(n.title=t.title);const r={type:"element",tagName:"a",properties:n,children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function uf(e,t,n){const r=e.all(t),a=n?cf(n):Cu(t),i={},o=[];if(typeof t.checked=="boolean"){const d=r[0];let c;d&&d.type==="element"&&d.tagName==="p"?c=d:(c={type:"element",tagName:"p",properties:{},children:[]},r.unshift(c)),c.children.length>0&&c.children.unshift({type:"text",value:" "}),c.children.unshift({type:"element",tagName:"input",properties:{type:"checkbox",checked:t.checked,disabled:!0},children:[]}),i.className=["task-list-item"]}let s=-1;for(;++s1}function df(e,t){const n={},r=e.all(t);let a=-1;for(typeof t.start=="number"&&t.start!==1&&(n.start=t.start);++a0){const o={type:"element",tagName:"tbody",properties:{},children:e.wrap(n,!0)},s=Rn(t.children[1]),l=cu(t.children[t.children.length-1]);s&&l&&(o.position={start:s,end:l}),a.push(o)}const i={type:"element",tagName:"table",properties:{},children:e.wrap(a,!0)};return e.patch(t,i),e.applyData(t,i)}function bf(e,t,n){const r=n?n.children:void 0,i=(r?r.indexOf(t):1)===0?"th":"td",o=n&&n.type==="table"?n.align:void 0,s=o?o.length:t.children.length;let l=-1;const u=[];for(;++l0,!0),r[0]),a=r.index+r[0].length,r=n.exec(t);return i.push(Tl(t.slice(a),a>0,!1)),i.join("")}function Tl(e,t,n){let r=0,a=e.length;if(t){let i=e.codePointAt(r);for(;i===Sl||i===kl;)r++,i=e.codePointAt(r)}if(n){let i=e.codePointAt(a-1);for(;i===Sl||i===kl;)a--,i=e.codePointAt(a-1)}return a>r?e.slice(r,a):""}function yf(e,t){const n={type:"text",value:Ef(String(t.value))};return e.patch(t,n),e.applyData(t,n)}function Sf(e,t){const n={type:"element",tagName:"hr",properties:{},children:[]};return e.patch(t,n),e.applyData(t,n)}const kf={blockquote:Kg,break:Zg,code:Xg,delete:Qg,emphasis:Jg,footnoteReference:ef,heading:tf,html:nf,imageReference:rf,image:af,inlineCode:of,linkReference:sf,link:lf,listItem:uf,list:df,paragraph:pf,root:gf,strong:ff,table:mf,tableCell:hf,tableRow:bf,text:yf,thematicBreak:Sf,toml:St,yaml:St,definition:St,footnoteDefinition:St};function St(){}const Ou=-1,Bt=0,at=1,Ct=2,Ln=3,Dn=4,Mn=5,Fn=6,Lu=7,Du=8,wl=typeof self=="object"?self:globalThis,Tf=(e,t)=>{const n=(a,i)=>(e.set(i,a),a),r=a=>{if(e.has(a))return e.get(a);const[i,o]=t[a];switch(i){case Bt:case Ou:return n(o,a);case at:{const s=n([],a);for(const l of o)s.push(r(l));return s}case Ct:{const s=n({},a);for(const[l,u]of o)s[r(l)]=r(u);return s}case Ln:return n(new Date(o),a);case Dn:{const{source:s,flags:l}=o;return n(new RegExp(s,l),a)}case Mn:{const s=n(new Map,a);for(const[l,u]of o)s.set(r(l),r(u));return s}case Fn:{const s=n(new Set,a);for(const l of o)s.add(r(l));return s}case Lu:{const{name:s,message:l}=o;return n(new wl[s](l),a)}case Du:return n(BigInt(o),a);case"BigInt":return n(Object(BigInt(o)),a);case"ArrayBuffer":return n(new Uint8Array(o).buffer,o);case"DataView":{const{buffer:s}=new Uint8Array(o);return n(new DataView(s),o)}}return n(new wl[i](o),a)};return r},Al=e=>Tf(new Map,e)(0),Pe="",{toString:wf}={},{keys:Af}=Object,tt=e=>{const t=typeof e;if(t!=="object"||!e)return[Bt,t];const n=wf.call(e).slice(8,-1);switch(n){case"Array":return[at,Pe];case"Object":return[Ct,Pe];case"Date":return[Ln,Pe];case"RegExp":return[Dn,Pe];case"Map":return[Mn,Pe];case"Set":return[Fn,Pe];case"DataView":return[at,n]}return n.includes("Array")?[at,n]:n.includes("Error")?[Lu,n]:[Ct,n]},kt=([e,t])=>e===Bt&&(t==="function"||t==="symbol"),_f=(e,t,n,r)=>{const a=(o,s)=>{const l=r.push(o)-1;return n.set(s,l),l},i=o=>{if(n.has(o))return n.get(o);let[s,l]=tt(o);switch(s){case Bt:{let d=o;switch(l){case"bigint":s=Du,d=o.toString();break;case"function":case"symbol":if(e)throw new TypeError("unable to serialize "+l);d=null;break;case"undefined":return a([Ou],o)}return a([s,d],o)}case at:{if(l){let g=o;return l==="DataView"?g=new Uint8Array(o.buffer):l==="ArrayBuffer"&&(g=new Uint8Array(o)),a([l,[...g]],o)}const d=[],c=a([s,d],o);for(const g of o)d.push(i(g));return c}case Ct:{if(l)switch(l){case"BigInt":return a([l,o.toString()],o);case"Boolean":case"Number":case"String":return a([l,o.valueOf()],o)}if(t&&"toJSON"in o)return i(o.toJSON());const d=[],c=a([s,d],o);for(const g of Af(o))(e||!kt(tt(o[g])))&&d.push([i(g),i(o[g])]);return c}case Ln:return a([s,o.toISOString()],o);case Dn:{const{source:d,flags:c}=o;return a([s,{source:d,flags:c}],o)}case Mn:{const d=[],c=a([s,d],o);for(const[g,p]of o)(e||!(kt(tt(g))||kt(tt(p))))&&d.push([i(g),i(p)]);return c}case Fn:{const d=[],c=a([s,d],o);for(const g of o)(e||!kt(tt(g)))&&d.push(i(g));return c}}const{message:u}=o;return a([s,{name:l,message:u}],o)};return i},_l=(e,{json:t,lossy:n}={})=>{const r=[];return _f(!(t||n),!!t,new Map,r)(e),r},Ot=typeof structuredClone=="function"?(e,t)=>t&&("json"in t||"lossy"in t)?Al(_l(e,t)):structuredClone(e):(e,t)=>Al(_l(e,t));function If(e,t){const n=[{type:"text",value:"↩"}];return t>1&&n.push({type:"element",tagName:"sup",properties:{},children:[{type:"text",value:String(t)}]}),n}function Rf(e,t){return"Back to reference "+(e+1)+(t>1?"-"+t:"")}function Nf(e){const t=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",n=e.options.footnoteBackContent||If,r=e.options.footnoteBackLabel||Rf,a=e.options.footnoteLabel||"Footnotes",i=e.options.footnoteLabelTagName||"h2",o=e.options.footnoteLabelProperties||{className:["sr-only"]},s=[];let l=-1;for(;++l0&&m.push({type:"text",value:" "});let A=typeof n=="string"?n:n(l,p);typeof A=="string"&&(A={type:"text",value:A}),m.push({type:"element",tagName:"a",properties:{href:"#"+t+"fnref-"+g+(p>1?"-"+p:""),dataFootnoteBackref:"",ariaLabel:typeof r=="string"?r:r(l,p),className:["data-footnote-backref"]},children:Array.isArray(A)?A:[A]})}const S=d[d.length-1];if(S&&S.type==="element"&&S.tagName==="p"){const A=S.children[S.children.length-1];A&&A.type==="text"?A.value+=" ":S.children.push({type:"text",value:" "}),S.children.push(...m)}else d.push(...m);const y={type:"element",tagName:"li",properties:{id:t+"fn-"+g},children:e.wrap(d,!0)};e.patch(u,y),s.push(y)}if(s.length!==0)return{type:"element",tagName:"section",properties:{dataFootnotes:!0,className:["footnotes"]},children:[{type:"element",tagName:i,properties:{...Ot(o),id:"footnote-label"},children:[{type:"text",value:a}]},{type:"text",value:`
+`},{type:"element",tagName:"ol",properties:{},children:e.wrap(s,!0)},{type:"text",value:`
+`}]}}const Pt=function(e){if(e==null)return Of;if(typeof e=="function")return $t(e);if(typeof e=="object")return Array.isArray(e)?xf(e):vf(e);if(typeof e=="string")return Cf(e);throw new Error("Expected function, string, or object as test")};function xf(e){const t=[];let n=-1;for(;++n":""))+")"})}return g;function g(){let p=Mu,m,E,S;if((!t||i(l,u,d[d.length-1]||void 0))&&(p=Ff(n(l,d)),p[0]===En))return p;if("children"in l&&l.children){const y=l;if(y.children&&p[0]!==Mf)for(E=(r?y.children.length:-1)+o,S=d.concat(y);E>-1&&E0&&n.push({type:"text",value:`
+`}),n}function Il(e){let t=0,n=e.charCodeAt(t);for(;n===9||n===32;)t++,n=e.charCodeAt(t);return e.slice(t)}function Rl(e,t){const n=Bf(e,t),r=n.one(e,void 0),a=Nf(n),i=Array.isArray(r)?{type:"root",children:r}:r||{type:"root",children:[]};return a&&i.children.push({type:"text",value:`
+`},a),i}function Hf(e,t){return e&&"run"in e?async function(n,r){const a=Rl(n,{file:r,...t});await e.run(a,r)}:function(n,r){return Rl(n,{file:r,...e||t})}}function Nl(e){if(e)throw e}var It=Object.prototype.hasOwnProperty,Uu=Object.prototype.toString,xl=Object.defineProperty,vl=Object.getOwnPropertyDescriptor,Cl=function(t){return typeof Array.isArray=="function"?Array.isArray(t):Uu.call(t)==="[object Array]"},Ol=function(t){if(!t||Uu.call(t)!=="[object Object]")return!1;var n=It.call(t,"constructor"),r=t.constructor&&t.constructor.prototype&&It.call(t.constructor.prototype,"isPrototypeOf");if(t.constructor&&!n&&!r)return!1;var a;for(a in t);return typeof a>"u"||It.call(t,a)},Ll=function(t,n){xl&&n.name==="__proto__"?xl(t,n.name,{enumerable:!0,configurable:!0,value:n.newValue,writable:!0}):t[n.name]=n.newValue},Dl=function(t,n){if(n==="__proto__")if(It.call(t,n)){if(vl)return vl(t,n).value}else return;return t[n]},jf=function e(){var t,n,r,a,i,o,s=arguments[0],l=1,u=arguments.length,d=!1;for(typeof s=="boolean"&&(d=s,s=arguments[1]||{},l=2),(s==null||typeof s!="object"&&typeof s!="function")&&(s={});lo.length;let l;s&&o.push(a);try{l=e.apply(this,o)}catch(u){const d=u;if(s&&n)throw d;return a(d)}s||(l&&l.then&&typeof l.then=="function"?l.then(i,a):l instanceof Error?a(l):i(l))}function a(o,...s){n||(n=!0,t(o,...s))}function i(o){a(null,o)}}const ye={basename:Wf,dirname:Yf,extname:Kf,join:Zf,sep:"/"};function Wf(e,t){if(t!==void 0&&typeof t!="string")throw new TypeError('"ext" argument must be a string');pt(e);let n=0,r=-1,a=e.length,i;if(t===void 0||t.length===0||t.length>e.length){for(;a--;)if(e.codePointAt(a)===47){if(i){n=a+1;break}}else r<0&&(i=!0,r=a+1);return r<0?"":e.slice(n,r)}if(t===e)return"";let o=-1,s=t.length-1;for(;a--;)if(e.codePointAt(a)===47){if(i){n=a+1;break}}else o<0&&(i=!0,o=a+1),s>-1&&(e.codePointAt(a)===t.codePointAt(s--)?s<0&&(r=a):(s=-1,r=o));return n===r?r=o:r<0&&(r=e.length),e.slice(n,r)}function Yf(e){if(pt(e),e.length===0)return".";let t=-1,n=e.length,r;for(;--n;)if(e.codePointAt(n)===47){if(r){t=n;break}}else r||(r=!0);return t<0?e.codePointAt(0)===47?"/":".":t===1&&e.codePointAt(0)===47?"//":e.slice(0,t)}function Kf(e){pt(e);let t=e.length,n=-1,r=0,a=-1,i=0,o;for(;t--;){const s=e.codePointAt(t);if(s===47){if(o){r=t+1;break}continue}n<0&&(o=!0,n=t+1),s===46?a<0?a=t:i!==1&&(i=1):a>-1&&(i=-1)}return a<0||n<0||i===0||i===1&&a===n-1&&a===r+1?"":e.slice(a,n)}function Zf(...e){let t=-1,n;for(;++t0&&e.codePointAt(e.length-1)===47&&(n+="/"),t?"/"+n:n}function Qf(e,t){let n="",r=0,a=-1,i=0,o=-1,s,l;for(;++o<=e.length;){if(o2){if(l=n.lastIndexOf("/"),l!==n.length-1){l<0?(n="",r=0):(n=n.slice(0,l),r=n.length-1-n.lastIndexOf("/")),a=o,i=0;continue}}else if(n.length>0){n="",r=0,a=o,i=0;continue}}t&&(n=n.length>0?n+"/..":"..",r=2)}else n.length>0?n+="/"+e.slice(a+1,o):n=e.slice(a+1,o),r=o-a-1;a=o,i=0}else s===46&&i>-1?i++:i=-1}return n}function pt(e){if(typeof e!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}const Jf={cwd:em};function em(){return"/"}function kn(e){return!!(e!==null&&typeof e=="object"&&"href"in e&&e.href&&"protocol"in e&&e.protocol&&e.auth===void 0)}function tm(e){if(typeof e=="string")e=new URL(e);else if(!kn(e)){const t=new TypeError('The "path" argument must be of type string or an instance of URL. Received `'+e+"`");throw t.code="ERR_INVALID_ARG_TYPE",t}if(e.protocol!=="file:"){const t=new TypeError("The URL must be of scheme file");throw t.code="ERR_INVALID_URL_SCHEME",t}return nm(e)}function nm(e){if(e.hostname!==""){const r=new TypeError('File URL host must be "localhost" or empty on darwin');throw r.code="ERR_INVALID_FILE_URL_HOST",r}const t=e.pathname;let n=-1;for(;++n0){let[p,...m]=d;const E=r[g][1];Sn(E)&&Sn(p)&&(p=en(!0,E,p)),r[g]=[u,p,...m]}}}}const om=new Bn().freeze();function an(e,t){if(typeof t!="function")throw new TypeError("Cannot `"+e+"` without `parser`")}function on(e,t){if(typeof t!="function")throw new TypeError("Cannot `"+e+"` without `compiler`")}function sn(e,t){if(t)throw new Error("Cannot call `"+e+"` on a frozen processor.\nCreate a new processor first, by calling it: use `processor()` instead of `processor`.")}function Fl(e){if(!Sn(e)||typeof e.type!="string")throw new TypeError("Expected node, got `"+e+"`")}function Ul(e,t,n){if(!n)throw new Error("`"+e+"` finished async. Use `"+t+"` instead")}function Tt(e){return sm(e)?e:new Bu(e)}function sm(e){return!!(e&&typeof e=="object"&&"message"in e&&"messages"in e)}function lm(e){return typeof e=="string"||um(e)}function um(e){return!!(e&&typeof e=="object"&&"byteLength"in e&&"byteOffset"in e)}const cm="https://github.com/remarkjs/react-markdown/blob/main/changelog.md",Bl=[],Pl={allowDangerousHtml:!0},dm=/^(https?|ircs?|mailto|xmpp)$/i,pm=[{from:"astPlugins",id:"remove-buggy-html-in-markdown-parser"},{from:"allowDangerousHtml",id:"remove-buggy-html-in-markdown-parser"},{from:"allowNode",id:"replace-allownode-allowedtypes-and-disallowedtypes",to:"allowElement"},{from:"allowedTypes",id:"replace-allownode-allowedtypes-and-disallowedtypes",to:"allowedElements"},{from:"className",id:"remove-classname"},{from:"disallowedTypes",id:"replace-allownode-allowedtypes-and-disallowedtypes",to:"disallowedElements"},{from:"escapeHtml",id:"remove-buggy-html-in-markdown-parser"},{from:"includeElementIndex",id:"#remove-includeelementindex"},{from:"includeNodeIndex",id:"change-includenodeindex-to-includeelementindex"},{from:"linkTarget",id:"remove-linktarget"},{from:"plugins",id:"change-plugins-to-remarkplugins",to:"remarkPlugins"},{from:"rawSourcePos",id:"#remove-rawsourcepos"},{from:"renderers",id:"change-renderers-to-components",to:"components"},{from:"source",id:"change-source-to-children",to:"children"},{from:"sourcePos",id:"#remove-sourcepos"},{from:"transformImageUri",id:"#add-urltransform",to:"urlTransform"},{from:"transformLinkUri",id:"#add-urltransform",to:"urlTransform"}];function gm(e){const t=fm(e),n=mm(e);return bm(t.runSync(t.parse(n),n),e)}function fm(e){const t=e.rehypePlugins||Bl,n=e.remarkPlugins||Bl,r=e.remarkRehypeOptions?{...e.remarkRehypeOptions,...Pl}:Pl;return om().use(Yg).use(n).use(Hf,r).use(t)}function mm(e){const t=e.children||"",n=new Bu;return typeof t=="string"&&(n.value=t),n}function bm(e,t){const n=t.allowedElements,r=t.allowElement,a=t.components,i=t.disallowedElements,o=t.skipHtml,s=t.unwrapDisallowed,l=t.urlTransform||hm;for(const d of pm)Object.hasOwn(t,d.from)&&(""+d.from+(d.to?"use `"+d.to+"` instead":"remove it")+cm+d.id,void 0);return Un(e,u),Cd(e,{Fragment:I.Fragment,components:a,ignoreInvalidStyle:!0,jsx:I.jsx,jsxs:I.jsxs,passKeys:!0,passNode:!0});function u(d,c,g){if(d.type==="raw"&&g&&typeof c=="number")return o?g.children.splice(c,1):g.children[c]={type:"text",value:d.value},c;if(d.type==="element"){let p;for(p in Xt)if(Object.hasOwn(Xt,p)&&Object.hasOwn(d.properties,p)){const m=d.properties[p],E=Xt[p];(E===null||E.includes(d.tagName))&&(d.properties[p]=l(String(m||""),p,d))}}if(d.type==="element"){let p=n?!n.includes(d.tagName):i?i.includes(d.tagName):!1;if(!p&&r&&typeof c=="number"&&(p=!r(d,c,g)),p&&g&&typeof c=="number")return s&&d.children?g.children.splice(c,1,...d.children):g.children.splice(c,1),c}}}function hm(e){const t=e.indexOf(":"),n=e.indexOf("?"),r=e.indexOf("#"),a=e.indexOf("/");return t===-1||a!==-1&&t>a||n!==-1&&t>n||r!==-1&&t>r||dm.test(e.slice(0,t))?e:""}function $l(e,t){const n=String(e);if(typeof t!="string")throw new TypeError("Expected character");let r=0,a=n.indexOf(t);for(;a!==-1;)r++,a=n.indexOf(t,a+t.length);return r}function Em(e){if(typeof e!="string")throw new TypeError("Expected a string");return e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d")}function ym(e,t,n){const a=Pt((n||{}).ignore||[]),i=Sm(t);let o=-1;for(;++o0?{type:"text",value:T}:void 0),T===!1?g.lastIndex=R+1:(m!==R&&A.push({type:"text",value:u.value.slice(m,R)}),Array.isArray(T)?A.push(...T):T&&A.push(T),m=R+w[0].length,y=!0),!g.global)break;w=g.exec(u.value)}return y?(m?\]}]+$/.exec(e);if(!t)return[e,void 0];e=e.slice(0,t.index);let n=t[0],r=n.indexOf(")");const a=$l(e,"(");let i=$l(e,")");for(;r!==-1&&a>i;)e+=n.slice(0,r+1),n=n.slice(r+1),r=n.indexOf(")"),i++;return[e,n]}function Pu(e,t){const n=e.input.charCodeAt(e.index-1);return(e.index===0||Le(n)||Ft(n))&&(!t||n!==47)}$u.peek=Hm;function Mm(){this.buffer()}function Fm(e){this.enter({type:"footnoteReference",identifier:"",label:""},e)}function Um(){this.buffer()}function Bm(e){this.enter({type:"footnoteDefinition",identifier:"",label:"",children:[]},e)}function Pm(e){const t=this.resume(),n=this.stack[this.stack.length-1];n.type,n.identifier=Ee(this.sliceSerialize(e)).toLowerCase(),n.label=t}function $m(e){this.exit(e)}function zm(e){const t=this.resume(),n=this.stack[this.stack.length-1];n.type,n.identifier=Ee(this.sliceSerialize(e)).toLowerCase(),n.label=t}function Gm(e){this.exit(e)}function Hm(){return"["}function $u(e,t,n,r){const a=n.createTracker(r);let i=a.move("[^");const o=n.enter("footnoteReference"),s=n.enter("reference");return i+=a.move(n.safe(n.associationId(e),{after:"]",before:i})),s(),o(),i+=a.move("]"),i}function jm(){return{enter:{gfmFootnoteCallString:Mm,gfmFootnoteCall:Fm,gfmFootnoteDefinitionLabelString:Um,gfmFootnoteDefinition:Bm},exit:{gfmFootnoteCallString:Pm,gfmFootnoteCall:$m,gfmFootnoteDefinitionLabelString:zm,gfmFootnoteDefinition:Gm}}}function Vm(e){let t=!1;return e&&e.firstLineBlank&&(t=!0),{handlers:{footnoteDefinition:n,footnoteReference:$u},unsafe:[{character:"[",inConstruct:["label","phrasing","reference"]}]};function n(r,a,i,o){const s=i.createTracker(o);let l=s.move("[^");const u=i.enter("footnoteDefinition"),d=i.enter("label");return l+=s.move(i.safe(i.associationId(r),{before:l,after:"]"})),d(),l+=s.move("]:"),r.children&&r.children.length>0&&(s.shift(4),l+=s.move((t?`
+`:" ")+i.indentLines(i.containerFlow(r,s.current()),t?zu:qm))),u(),l}}function qm(e,t,n){return t===0?e:zu(e,t,n)}function zu(e,t,n){return(n?"":" ")+e}const Wm=["autolink","destinationLiteral","destinationRaw","reference","titleQuote","titleApostrophe"];Gu.peek=Qm;function Ym(){return{canContainEols:["delete"],enter:{strikethrough:Zm},exit:{strikethrough:Xm}}}function Km(){return{unsafe:[{character:"~",inConstruct:"phrasing",notInConstruct:Wm}],handlers:{delete:Gu}}}function Zm(e){this.enter({type:"delete",children:[]},e)}function Xm(e){this.exit(e)}function Gu(e,t,n,r){const a=n.createTracker(r),i=n.enter("strikethrough");let o=a.move("~~");return o+=n.containerPhrasing(e,{...a.current(),before:o,after:"~"}),o+=a.move("~~"),i(),o}function Qm(){return"~"}function Jm(e){return e.length}function eb(e,t){const n=t||{},r=(n.align||[]).concat(),a=n.stringLength||Jm,i=[],o=[],s=[],l=[];let u=0,d=-1;for(;++du&&(u=e[d].length);++yl[y])&&(l[y]=w)}E.push(A)}o[d]=E,s[d]=S}let c=-1;if(typeof r=="object"&&"length"in r)for(;++cl[c]&&(l[c]=A),p[c]=A),g[c]=w}o.splice(1,0,g),s.splice(1,0,p),d=-1;const m=[];for(;++d "),i.shift(2);const o=n.indentLines(n.containerFlow(e,i.current()),rb);return a(),o}function rb(e,t,n){return">"+(n?"":" ")+e}function ab(e,t){return Gl(e,t.inConstruct,!0)&&!Gl(e,t.notInConstruct,!1)}function Gl(e,t,n){if(typeof t=="string"&&(t=[t]),!t||t.length===0)return n;let r=-1;for(;++ro&&(o=i):i=1,a=r+t.length,r=n.indexOf(t,a);return o}function ob(e,t){return!!(t.options.fences===!1&&e.value&&!e.lang&&/[^ \r\n]/.test(e.value)&&!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(e.value))}function sb(e){const t=e.options.fence||"`";if(t!=="`"&&t!=="~")throw new Error("Cannot serialize code with `"+t+"` for `options.fence`, expected `` ` `` or `~`");return t}function lb(e,t,n,r){const a=sb(n),i=e.value||"",o=a==="`"?"GraveAccent":"Tilde";if(ob(e,n)){const c=n.enter("codeIndented"),g=n.indentLines(i,ub);return c(),g}const s=n.createTracker(r),l=a.repeat(Math.max(ib(i,a)+1,3)),u=n.enter("codeFenced");let d=s.move(l);if(e.lang){const c=n.enter(`codeFencedLang${o}`);d+=s.move(n.safe(e.lang,{before:d,after:" ",encode:["`"],...s.current()})),c()}if(e.lang&&e.meta){const c=n.enter(`codeFencedMeta${o}`);d+=s.move(" "),d+=s.move(n.safe(e.meta,{before:d,after:`
+`,encode:["`"],...s.current()})),c()}return d+=s.move(`
+`),i&&(d+=s.move(i+`
+`)),d+=s.move(l),u(),d}function ub(e,t,n){return(n?"":" ")+e}function Pn(e){const t=e.options.quote||'"';if(t!=='"'&&t!=="'")throw new Error("Cannot serialize title with `"+t+"` for `options.quote`, expected `\"`, or `'`");return t}function cb(e,t,n,r){const a=Pn(n),i=a==='"'?"Quote":"Apostrophe",o=n.enter("definition");let s=n.enter("label");const l=n.createTracker(r);let u=l.move("[");return u+=l.move(n.safe(n.associationId(e),{before:u,after:"]",...l.current()})),u+=l.move("]: "),s(),!e.url||/[\0- \u007F]/.test(e.url)?(s=n.enter("destinationLiteral"),u+=l.move("<"),u+=l.move(n.safe(e.url,{before:u,after:">",...l.current()})),u+=l.move(">")):(s=n.enter("destinationRaw"),u+=l.move(n.safe(e.url,{before:u,after:e.title?" ":`
+`,...l.current()}))),s(),e.title&&(s=n.enter(`title${i}`),u+=l.move(" "+a),u+=l.move(n.safe(e.title,{before:u,after:a,...l.current()})),u+=l.move(a),s()),o(),u}function db(e){const t=e.options.emphasis||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize emphasis with `"+t+"` for `options.emphasis`, expected `*`, or `_`");return t}function ut(e){return""+e.toString(16).toUpperCase()+";"}function Lt(e,t,n){const r=Ge(e),a=Ge(t);return r===void 0?a===void 0?n==="_"?{inside:!0,outside:!0}:{inside:!1,outside:!1}:a===1?{inside:!0,outside:!0}:{inside:!1,outside:!0}:r===1?a===void 0?{inside:!1,outside:!1}:a===1?{inside:!0,outside:!0}:{inside:!1,outside:!1}:a===void 0?{inside:!1,outside:!1}:a===1?{inside:!0,outside:!1}:{inside:!1,outside:!1}}Hu.peek=pb;function Hu(e,t,n,r){const a=db(n),i=n.enter("emphasis"),o=n.createTracker(r),s=o.move(a);let l=o.move(n.containerPhrasing(e,{after:a,before:s,...o.current()}));const u=l.charCodeAt(0),d=Lt(r.before.charCodeAt(r.before.length-1),u,a);d.inside&&(l=ut(u)+l.slice(1));const c=l.charCodeAt(l.length-1),g=Lt(r.after.charCodeAt(0),c,a);g.inside&&(l=l.slice(0,-1)+ut(c));const p=o.move(a);return i(),n.attentionEncodeSurroundingInfo={after:g.outside,before:d.outside},s+l+p}function pb(e,t,n){return n.options.emphasis||"*"}function gb(e,t){let n=!1;return Un(e,function(r){if("value"in r&&/\r?\n|\r/.test(r.value)||r.type==="break")return n=!0,En}),!!((!e.depth||e.depth<3)&&Cn(e)&&(t.options.setext||n))}function fb(e,t,n,r){const a=Math.max(Math.min(6,e.depth||1),1),i=n.createTracker(r);if(gb(e,n)){const d=n.enter("headingSetext"),c=n.enter("phrasing"),g=n.containerPhrasing(e,{...i.current(),before:`
+`,after:`
+`});return c(),d(),g+`
+`+(a===1?"=":"-").repeat(g.length-(Math.max(g.lastIndexOf("\r"),g.lastIndexOf(`
+`))+1))}const o="#".repeat(a),s=n.enter("headingAtx"),l=n.enter("phrasing");i.move(o+" ");let u=n.containerPhrasing(e,{before:"# ",after:`
+`,...i.current()});return/^[\t ]/.test(u)&&(u=ut(u.charCodeAt(0))+u.slice(1)),u=u?o+" "+u:o,n.options.closeAtx&&(u+=" "+o),l(),s(),u}ju.peek=mb;function ju(e){return e.value||""}function mb(){return"<"}Vu.peek=bb;function Vu(e,t,n,r){const a=Pn(n),i=a==='"'?"Quote":"Apostrophe",o=n.enter("image");let s=n.enter("label");const l=n.createTracker(r);let u=l.move("![");return u+=l.move(n.safe(e.alt,{before:u,after:"]",...l.current()})),u+=l.move("]("),s(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(s=n.enter("destinationLiteral"),u+=l.move("<"),u+=l.move(n.safe(e.url,{before:u,after:">",...l.current()})),u+=l.move(">")):(s=n.enter("destinationRaw"),u+=l.move(n.safe(e.url,{before:u,after:e.title?" ":")",...l.current()}))),s(),e.title&&(s=n.enter(`title${i}`),u+=l.move(" "+a),u+=l.move(n.safe(e.title,{before:u,after:a,...l.current()})),u+=l.move(a),s()),u+=l.move(")"),o(),u}function bb(){return"!"}qu.peek=hb;function qu(e,t,n,r){const a=e.referenceType,i=n.enter("imageReference");let o=n.enter("label");const s=n.createTracker(r);let l=s.move("![");const u=n.safe(e.alt,{before:l,after:"]",...s.current()});l+=s.move(u+"]["),o();const d=n.stack;n.stack=[],o=n.enter("reference");const c=n.safe(n.associationId(e),{before:l,after:"]",...s.current()});return o(),n.stack=d,i(),a==="full"||!u||u!==c?l+=s.move(c+"]"):a==="shortcut"?l=l.slice(0,-1):l+=s.move("]"),l}function hb(){return"!"}Wu.peek=Eb;function Wu(e,t,n){let r=e.value||"",a="`",i=-1;for(;new RegExp("(^|[^`])"+a+"([^`]|$)").test(r);)a+="`";for(/[^ \r\n]/.test(r)&&(/^[ \r\n]/.test(r)&&/[ \r\n]$/.test(r)||/^`|`$/.test(r))&&(r=" "+r+" ");++i\u007F]/.test(e.url))}Ku.peek=yb;function Ku(e,t,n,r){const a=Pn(n),i=a==='"'?"Quote":"Apostrophe",o=n.createTracker(r);let s,l;if(Yu(e,n)){const d=n.stack;n.stack=[],s=n.enter("autolink");let c=o.move("<");return c+=o.move(n.containerPhrasing(e,{before:c,after:">",...o.current()})),c+=o.move(">"),s(),n.stack=d,c}s=n.enter("link"),l=n.enter("label");let u=o.move("[");return u+=o.move(n.containerPhrasing(e,{before:u,after:"](",...o.current()})),u+=o.move("]("),l(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(l=n.enter("destinationLiteral"),u+=o.move("<"),u+=o.move(n.safe(e.url,{before:u,after:">",...o.current()})),u+=o.move(">")):(l=n.enter("destinationRaw"),u+=o.move(n.safe(e.url,{before:u,after:e.title?" ":")",...o.current()}))),l(),e.title&&(l=n.enter(`title${i}`),u+=o.move(" "+a),u+=o.move(n.safe(e.title,{before:u,after:a,...o.current()})),u+=o.move(a),l()),u+=o.move(")"),s(),u}function yb(e,t,n){return Yu(e,n)?"<":"["}Zu.peek=Sb;function Zu(e,t,n,r){const a=e.referenceType,i=n.enter("linkReference");let o=n.enter("label");const s=n.createTracker(r);let l=s.move("[");const u=n.containerPhrasing(e,{before:l,after:"]",...s.current()});l+=s.move(u+"]["),o();const d=n.stack;n.stack=[],o=n.enter("reference");const c=n.safe(n.associationId(e),{before:l,after:"]",...s.current()});return o(),n.stack=d,i(),a==="full"||!u||u!==c?l+=s.move(c+"]"):a==="shortcut"?l=l.slice(0,-1):l+=s.move("]"),l}function Sb(){return"["}function $n(e){const t=e.options.bullet||"*";if(t!=="*"&&t!=="+"&&t!=="-")throw new Error("Cannot serialize items with `"+t+"` for `options.bullet`, expected `*`, `+`, or `-`");return t}function kb(e){const t=$n(e),n=e.options.bulletOther;if(!n)return t==="*"?"-":"*";if(n!=="*"&&n!=="+"&&n!=="-")throw new Error("Cannot serialize items with `"+n+"` for `options.bulletOther`, expected `*`, `+`, or `-`");if(n===t)throw new Error("Expected `bullet` (`"+t+"`) and `bulletOther` (`"+n+"`) to be different");return n}function Tb(e){const t=e.options.bulletOrdered||".";if(t!=="."&&t!==")")throw new Error("Cannot serialize items with `"+t+"` for `options.bulletOrdered`, expected `.` or `)`");return t}function Xu(e){const t=e.options.rule||"*";if(t!=="*"&&t!=="-"&&t!=="_")throw new Error("Cannot serialize rules with `"+t+"` for `options.rule`, expected `*`, `-`, or `_`");return t}function wb(e,t,n,r){const a=n.enter("list"),i=n.bulletCurrent;let o=e.ordered?Tb(n):$n(n);const s=e.ordered?o==="."?")":".":kb(n);let l=t&&n.bulletLastUsed?o===n.bulletLastUsed:!1;if(!e.ordered){const d=e.children?e.children[0]:void 0;if((o==="*"||o==="-")&&d&&(!d.children||!d.children[0])&&n.stack[n.stack.length-1]==="list"&&n.stack[n.stack.length-2]==="listItem"&&n.stack[n.stack.length-3]==="list"&&n.stack[n.stack.length-4]==="listItem"&&n.indexStack[n.indexStack.length-1]===0&&n.indexStack[n.indexStack.length-2]===0&&n.indexStack[n.indexStack.length-3]===0&&(l=!0),Xu(n)===o&&d){let c=-1;for(;++c-1?t.start:1)+(n.options.incrementListMarker===!1?0:t.children.indexOf(e))+i);let o=i.length+1;(a==="tab"||a==="mixed"&&(t&&t.type==="list"&&t.spread||e.spread))&&(o=Math.ceil(o/4)*4);const s=n.createTracker(r);s.move(i+" ".repeat(o-i.length)),s.shift(o);const l=n.enter("listItem"),u=n.indentLines(n.containerFlow(e,s.current()),d);return l(),u;function d(c,g,p){return g?(p?"":" ".repeat(o))+c:(p?i:i+" ".repeat(o-i.length))+c}}function Ib(e,t,n,r){const a=n.enter("paragraph"),i=n.enter("phrasing"),o=n.containerPhrasing(e,r);return i(),a(),o}const Rb=Pt(["break","delete","emphasis","footnote","footnoteReference","image","imageReference","inlineCode","inlineMath","link","linkReference","mdxJsxTextElement","mdxTextExpression","strong","text","textDirective"]);function Nb(e,t,n,r){return(e.children.some(function(o){return Rb(o)})?n.containerPhrasing:n.containerFlow).call(n,e,r)}function xb(e){const t=e.options.strong||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize strong with `"+t+"` for `options.strong`, expected `*`, or `_`");return t}Qu.peek=vb;function Qu(e,t,n,r){const a=xb(n),i=n.enter("strong"),o=n.createTracker(r),s=o.move(a+a);let l=o.move(n.containerPhrasing(e,{after:a,before:s,...o.current()}));const u=l.charCodeAt(0),d=Lt(r.before.charCodeAt(r.before.length-1),u,a);d.inside&&(l=ut(u)+l.slice(1));const c=l.charCodeAt(l.length-1),g=Lt(r.after.charCodeAt(0),c,a);g.inside&&(l=l.slice(0,-1)+ut(c));const p=o.move(a+a);return i(),n.attentionEncodeSurroundingInfo={after:g.outside,before:d.outside},s+l+p}function vb(e,t,n){return n.options.strong||"*"}function Cb(e,t,n,r){return n.safe(e.value,r)}function Ob(e){const t=e.options.ruleRepetition||3;if(t<3)throw new Error("Cannot serialize rules with repetition `"+t+"` for `options.ruleRepetition`, expected `3` or more");return t}function Lb(e,t,n){const r=(Xu(n)+(n.options.ruleSpaces?" ":"")).repeat(Ob(n));return n.options.ruleSpaces?r.slice(0,-1):r}const Ju={blockquote:nb,break:Hl,code:lb,definition:cb,emphasis:Hu,hardBreak:Hl,heading:fb,html:ju,image:Vu,imageReference:qu,inlineCode:Wu,link:Ku,linkReference:Zu,list:wb,listItem:_b,paragraph:Ib,root:Nb,strong:Qu,text:Cb,thematicBreak:Lb};function Db(){return{enter:{table:Mb,tableData:jl,tableHeader:jl,tableRow:Ub},exit:{codeText:Bb,table:Fb,tableData:dn,tableHeader:dn,tableRow:dn}}}function Mb(e){const t=e._align;this.enter({type:"table",align:t.map(function(n){return n==="none"?null:n}),children:[]},e),this.data.inTable=!0}function Fb(e){this.exit(e),this.data.inTable=void 0}function Ub(e){this.enter({type:"tableRow",children:[]},e)}function dn(e){this.exit(e)}function jl(e){this.enter({type:"tableCell",children:[]},e)}function Bb(e){let t=this.resume();this.data.inTable&&(t=t.replace(/\\([\\|])/g,Pb));const n=this.stack[this.stack.length-1];n.type,n.value=t,this.exit(e)}function Pb(e,t){return t==="|"?t:e}function $b(e){const t=e||{},n=t.tableCellPadding,r=t.tablePipeAlign,a=t.stringLength,i=n?" ":"|";return{unsafe:[{character:"\r",inConstruct:"tableCell"},{character:`
+`,inConstruct:"tableCell"},{atBreak:!0,character:"|",after:"[ :-]"},{character:"|",inConstruct:"tableCell"},{atBreak:!0,character:":",after:"-"},{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{inlineCode:g,table:o,tableCell:l,tableRow:s}};function o(p,m,E,S){return u(d(p,E,S),p.align)}function s(p,m,E,S){const y=c(p,E,S),A=u([y]);return A.slice(0,A.indexOf(`
+`))}function l(p,m,E,S){const y=E.enter("tableCell"),A=E.enter("phrasing"),w=E.containerPhrasing(p,{...S,before:i,after:i});return A(),y(),w}function u(p,m){return eb(p,{align:m,alignDelimiters:r,padding:n,stringLength:a})}function d(p,m,E){const S=p.children;let y=-1;const A=[],w=m.enter("table");for(;++y0&&!n&&(e[e.length-1][1]._gfmAutolinkLiteralWalkedInto=!0),n}const ah={tokenize:ph,partial:!0};function ih(){return{document:{91:{name:"gfmFootnoteDefinition",tokenize:uh,continuation:{tokenize:ch},exit:dh}},text:{91:{name:"gfmFootnoteCall",tokenize:lh},93:{name:"gfmPotentialFootnoteCall",add:"after",tokenize:oh,resolveTo:sh}}}}function oh(e,t,n){const r=this;let a=r.events.length;const i=r.parser.gfmFootnotes||(r.parser.gfmFootnotes=[]);let o;for(;a--;){const l=r.events[a][1];if(l.type==="labelImage"){o=l;break}if(l.type==="gfmFootnoteCall"||l.type==="labelLink"||l.type==="label"||l.type==="image"||l.type==="link")break}return s;function s(l){if(!o||!o._balanced)return n(l);const u=Ee(r.sliceSerialize({start:o.end,end:r.now()}));return u.codePointAt(0)!==94||!i.includes(u.slice(1))?n(l):(e.enter("gfmFootnoteCallLabelMarker"),e.consume(l),e.exit("gfmFootnoteCallLabelMarker"),t(l))}}function sh(e,t){let n=e.length;for(;n--;)if(e[n][1].type==="labelImage"&&e[n][0]==="enter"){e[n][1];break}e[n+1][1].type="data",e[n+3][1].type="gfmFootnoteCallLabelMarker";const r={type:"gfmFootnoteCall",start:Object.assign({},e[n+3][1].start),end:Object.assign({},e[e.length-1][1].end)},a={type:"gfmFootnoteCallMarker",start:Object.assign({},e[n+3][1].end),end:Object.assign({},e[n+3][1].end)};a.end.column++,a.end.offset++,a.end._bufferIndex++;const i={type:"gfmFootnoteCallString",start:Object.assign({},a.end),end:Object.assign({},e[e.length-1][1].start)},o={type:"chunkString",contentType:"string",start:Object.assign({},i.start),end:Object.assign({},i.end)},s=[e[n+1],e[n+2],["enter",r,t],e[n+3],e[n+4],["enter",a,t],["exit",a,t],["enter",i,t],["enter",o,t],["exit",o,t],["exit",i,t],e[e.length-2],e[e.length-1],["exit",r,t]];return e.splice(n,e.length-n+1,...s),e}function lh(e,t,n){const r=this,a=r.parser.gfmFootnotes||(r.parser.gfmFootnotes=[]);let i=0,o;return s;function s(c){return e.enter("gfmFootnoteCall"),e.enter("gfmFootnoteCallLabelMarker"),e.consume(c),e.exit("gfmFootnoteCallLabelMarker"),l}function l(c){return c!==94?n(c):(e.enter("gfmFootnoteCallMarker"),e.consume(c),e.exit("gfmFootnoteCallMarker"),e.enter("gfmFootnoteCallString"),e.enter("chunkString").contentType="string",u)}function u(c){if(i>999||c===93&&!o||c===null||c===91||Y(c))return n(c);if(c===93){e.exit("chunkString");const g=e.exit("gfmFootnoteCallString");return a.includes(Ee(r.sliceSerialize(g)))?(e.enter("gfmFootnoteCallLabelMarker"),e.consume(c),e.exit("gfmFootnoteCallLabelMarker"),e.exit("gfmFootnoteCall"),t):n(c)}return Y(c)||(o=!0),i++,e.consume(c),c===92?d:u}function d(c){return c===91||c===92||c===93?(e.consume(c),i++,u):u(c)}}function uh(e,t,n){const r=this,a=r.parser.gfmFootnotes||(r.parser.gfmFootnotes=[]);let i,o=0,s;return l;function l(m){return e.enter("gfmFootnoteDefinition")._container=!0,e.enter("gfmFootnoteDefinitionLabel"),e.enter("gfmFootnoteDefinitionLabelMarker"),e.consume(m),e.exit("gfmFootnoteDefinitionLabelMarker"),u}function u(m){return m===94?(e.enter("gfmFootnoteDefinitionMarker"),e.consume(m),e.exit("gfmFootnoteDefinitionMarker"),e.enter("gfmFootnoteDefinitionLabelString"),e.enter("chunkString").contentType="string",d):n(m)}function d(m){if(o>999||m===93&&!s||m===null||m===91||Y(m))return n(m);if(m===93){e.exit("chunkString");const E=e.exit("gfmFootnoteDefinitionLabelString");return i=Ee(r.sliceSerialize(E)),e.enter("gfmFootnoteDefinitionLabelMarker"),e.consume(m),e.exit("gfmFootnoteDefinitionLabelMarker"),e.exit("gfmFootnoteDefinitionLabel"),g}return Y(m)||(s=!0),o++,e.consume(m),m===92?c:d}function c(m){return m===91||m===92||m===93?(e.consume(m),o++,d):d(m)}function g(m){return m===58?(e.enter("definitionMarker"),e.consume(m),e.exit("definitionMarker"),a.includes(i)||a.push(i),V(e,p,"gfmFootnoteDefinitionWhitespace")):n(m)}function p(m){return t(m)}}function ch(e,t,n){return e.check(dt,t,e.attempt(ah,t,n))}function dh(e){e.exit("gfmFootnoteDefinition")}function ph(e,t,n){const r=this;return V(e,a,"gfmFootnoteDefinitionIndent",5);function a(i){const o=r.events[r.events.length-1];return o&&o[1].type==="gfmFootnoteDefinitionIndent"&&o[2].sliceSerialize(o[1],!0).length===4?t(i):n(i)}}function gh(e){let n=(e||{}).singleTilde;const r={name:"strikethrough",tokenize:i,resolveAll:a};return n==null&&(n=!0),{text:{126:r},insideSpan:{null:[r]},attentionMarkers:{null:[126]}};function a(o,s){let l=-1;for(;++l1?l(m):(o.consume(m),c++,p);if(c<2&&!n)return l(m);const S=o.exit("strikethroughSequenceTemporary"),y=Ge(m);return S._open=!y||y===2&&!!E,S._close=!E||E===2&&!!y,s(m)}}}class fh{constructor(){this.map=[]}add(t,n,r){mh(this,t,n,r)}consume(t){if(this.map.sort(function(i,o){return i[0]-o[0]}),this.map.length===0)return;let n=this.map.length;const r=[];for(;n>0;)n-=1,r.push(t.slice(this.map[n][0]+this.map[n][1]),this.map[n][2]),t.length=this.map[n][0];r.push(t.slice()),t.length=0;let a=r.pop();for(;a;){for(const i of a)t.push(i);a=r.pop()}this.map.length=0}}function mh(e,t,n,r){let a=0;if(!(n===0&&r.length===0)){for(;a-1;){const z=r.events[C][1].type;if(z==="lineEnding"||z==="linePrefix")C--;else break}const O=C>-1?r.events[C][1].type:null,L=O==="tableHead"||O==="tableRow"?T:l;return L===T&&r.parser.lazy[r.now().line]?n(k):L(k)}function l(k){return e.enter("tableHead"),e.enter("tableRow"),u(k)}function u(k){return k===124||(o=!0,i+=1),d(k)}function d(k){return k===null?n(k):F(k)?i>1?(i=0,r.interrupt=!0,e.exit("tableRow"),e.enter("lineEnding"),e.consume(k),e.exit("lineEnding"),p):n(k):G(k)?V(e,d,"whitespace")(k):(i+=1,o&&(o=!1,a+=1),k===124?(e.enter("tableCellDivider"),e.consume(k),e.exit("tableCellDivider"),o=!0,d):(e.enter("data"),c(k)))}function c(k){return k===null||k===124||Y(k)?(e.exit("data"),d(k)):(e.consume(k),k===92?g:c)}function g(k){return k===92||k===124?(e.consume(k),c):c(k)}function p(k){return r.interrupt=!1,r.parser.lazy[r.now().line]?n(k):(e.enter("tableDelimiterRow"),o=!1,G(k)?V(e,m,"linePrefix",r.parser.constructs.disable.null.includes("codeIndented")?void 0:4)(k):m(k))}function m(k){return k===45||k===58?S(k):k===124?(o=!0,e.enter("tableCellDivider"),e.consume(k),e.exit("tableCellDivider"),E):v(k)}function E(k){return G(k)?V(e,S,"whitespace")(k):S(k)}function S(k){return k===58?(i+=1,o=!0,e.enter("tableDelimiterMarker"),e.consume(k),e.exit("tableDelimiterMarker"),y):k===45?(i+=1,y(k)):k===null||F(k)?R(k):v(k)}function y(k){return k===45?(e.enter("tableDelimiterFiller"),A(k)):v(k)}function A(k){return k===45?(e.consume(k),A):k===58?(o=!0,e.exit("tableDelimiterFiller"),e.enter("tableDelimiterMarker"),e.consume(k),e.exit("tableDelimiterMarker"),w):(e.exit("tableDelimiterFiller"),w(k))}function w(k){return G(k)?V(e,R,"whitespace")(k):R(k)}function R(k){return k===124?m(k):k===null||F(k)?!o||a!==i?v(k):(e.exit("tableDelimiterRow"),e.exit("tableHead"),t(k)):v(k)}function v(k){return n(k)}function T(k){return e.enter("tableRow"),D(k)}function D(k){return k===124?(e.enter("tableCellDivider"),e.consume(k),e.exit("tableCellDivider"),D):k===null||F(k)?(e.exit("tableRow"),t(k)):G(k)?V(e,D,"whitespace")(k):(e.enter("data"),M(k))}function M(k){return k===null||k===124||Y(k)?(e.exit("data"),D(k)):(e.consume(k),k===92?U:M)}function U(k){return k===92||k===124?(e.consume(k),M):M(k)}}function yh(e,t){let n=-1,r=!0,a=0,i=[0,0,0,0],o=[0,0,0,0],s=!1,l=0,u,d,c;const g=new fh;for(;++nn[2]+1){const m=n[2]+1,E=n[3]-n[2]-1;e.add(m,E,[])}}e.add(n[3]+1,0,[["exit",c,t]])}return a!==void 0&&(i.end=Object.assign({},$e(t.events,a)),e.add(a,0,[["exit",i,t]]),i=void 0),i}function ql(e,t,n,r,a){const i=[],o=$e(t.events,n);a&&(a.end=Object.assign({},o),i.push(["exit",a,t])),r.end=Object.assign({},o),i.push(["exit",r,t]),e.add(n+1,0,i)}function $e(e,t){const n=e[t],r=n[0]==="enter"?"start":"end";return n[1][r]}const Sh={name:"tasklistCheck",tokenize:Th};function kh(){return{text:{91:Sh}}}function Th(e,t,n){const r=this;return a;function a(l){return r.previous!==null||!r._gfmTasklistFirstContentOfListItem?n(l):(e.enter("taskListCheck"),e.enter("taskListCheckMarker"),e.consume(l),e.exit("taskListCheckMarker"),i)}function i(l){return Y(l)?(e.enter("taskListCheckValueUnchecked"),e.consume(l),e.exit("taskListCheckValueUnchecked"),o):l===88||l===120?(e.enter("taskListCheckValueChecked"),e.consume(l),e.exit("taskListCheckValueChecked"),o):n(l)}function o(l){return l===93?(e.enter("taskListCheckMarker"),e.consume(l),e.exit("taskListCheckMarker"),e.exit("taskListCheck"),s):n(l)}function s(l){return F(l)?t(l):G(l)?e.check({tokenize:wh},t,n)(l):n(l)}}function wh(e,t,n){return V(e,r,"whitespace");function r(a){return a===null?n(a):t(a)}}function Ah(e){return hu([Kb(),ih(),gh(e),hh(),kh()])}const _h={};function Ih(e){const t=this,n=e||_h,r=t.data(),a=r.micromarkExtensions||(r.micromarkExtensions=[]),i=r.fromMarkdownExtensions||(r.fromMarkdownExtensions=[]),o=r.toMarkdownExtensions||(r.toMarkdownExtensions=[]);a.push(Ah(n)),i.push(Vb()),o.push(qb(n))}const Rh=["abap","abnf","actionscript","ada","agda","al","antlr4","apacheconf","apex","apl","applescript","aql","arduino","arff","armasm","arturo","asciidoc","asm6502","asmatmel","aspnet","autohotkey","autoit","avisynth","avro-idl","awk","bash","basic","batch","bbcode","bbj","bicep","birb","bison","bnf","bqn","brainfuck","brightscript","bro","bsl","c","cfscript","chaiscript","cil","cilkc","cilkcpp","clike","clojure","cmake","cobol","coffeescript","concurnas","cooklang","coq","cpp","crystal","csharp","cshtml","csp","css-extras","css","csv","cue","cypher","d","dart","dataweave","dax","dhall","diff","django","dns-zone-file","docker","dot","ebnf","editorconfig","eiffel","ejs","elixir","elm","erb","erlang","etlua","excel-formula","factor","false","firestore-security-rules","flow","fortran","fsharp","ftl","gap","gcode","gdscript","gedcom","gettext","gherkin","git","glsl","gml","gn","go-module","go","gradle","graphql","groovy","haml","handlebars","haskell","haxe","hcl","hlsl","hoon","hpkp","hsts","http","ichigojam","icon","icu-message-format","idris","iecst","ignore","inform7","ini","io","j","java","javadoc","javadoclike","javascript","javastacktrace","jexl","jolie","jq","js-extras","js-templates","jsdoc","json","json5","jsonp","jsstacktrace","jsx","julia","keepalived","keyman","kotlin","kumir","kusto","latex","latte","less","lilypond","linker-script","liquid","lisp","livescript","llvm","log","lolcode","lua","magma","makefile","markdown","markup-templating","markup","mata","matlab","maxscript","mel","mermaid","metafont","mizar","mongodb","monkey","moonscript","n1ql","n4js","nand2tetris-hdl","naniscript","nasm","neon","nevod","nginx","nim","nix","nsis","objectivec","ocaml","odin","opencl","openqasm","oz","parigp","parser","pascal","pascaligo","pcaxis","peoplecode","perl","php-extras","php","phpdoc","plant-uml","plsql","powerquery","powershell","processing","prolog","promql","properties","protobuf","psl","pug","puppet","pure","purebasic","purescript","python","q","qml","qore","qsharp","r","racket","reason","regex","rego","renpy","rescript","rest","rip","roboconf","robotframework","ruby","rust","sas","sass","scala","scheme","scss","shell-session","smali","smalltalk","smarty","sml","solidity","solution-file","soy","sparql","splunk-spl","sqf","sql","squirrel","stan","stata","stylus","supercollider","swift","systemd","t4-cs","t4-templating","t4-vb","tap","tcl","textile","toml","tremor","tsx","tt2","turtle","twig","typescript","typoscript","unrealscript","uorazor","uri","v","vala","vbnet","velocity","verilog","vhdl","vim","visual-basic","warpscript","wasm","web-idl","wgsl","wiki","wolfram","wren","xeora","xml-doc","xojo","xquery","yaml","yang","zig"],Wl=/[#.]/g;function Nh(e,t){const n=e||"",r={};let a=0,i,o;for(;a=48&&t<=57}function Fh(e){const t=typeof e=="string"?e.charCodeAt(0):e;return t>=97&&t<=102||t>=65&&t<=70||t>=48&&t<=57}function Uh(e){const t=typeof e=="string"?e.charCodeAt(0):e;return t>=97&&t<=122||t>=65&&t<=90}function Zl(e){return Uh(e)||uc(e)}const Bh=["","Named character references must be terminated by a semicolon","Numeric character references must be terminated by a semicolon","Named character references cannot be empty","Numeric character references cannot be empty","Named character references must be known","Numeric character references cannot be disallowed","Numeric character references cannot be outside the permissible Unicode range"];function Ph(e,t){const n={},r=typeof n.additional=="string"?n.additional.charCodeAt(0):n.additional,a=[];let i=0,o=-1,s="",l,u;n.position&&("start"in n.position||"indent"in n.position?(u=n.position.indent,l=n.position.start):l=n.position);let d=(l?l.line:0)||1,c=(l?l.column:0)||1,g=m(),p;for(i--;++i<=e.length;)if(p===10&&(c=(u?u[o]:0)||1),p=e.charCodeAt(i),p===38){const y=e.charCodeAt(i+1);if(y===9||y===10||y===12||y===32||y===38||y===60||Number.isNaN(y)||r&&y===r){s+=String.fromCharCode(p),c++;continue}const A=i+1;let w=A,R=A,v;if(y===35){R=++w;const L=e.charCodeAt(R);L===88||L===120?(v="hexadecimal",R=++w):v="decimal"}else v="named";let T="",D="",M="";const U=v==="named"?Zl:v==="decimal"?uc:Fh;for(R--;++R<=e.length;){const L=e.charCodeAt(R);if(!U(L))break;M+=String.fromCharCode(L),v==="named"&&Mh.includes(M)&&(T=M,D=lt(M))}let k=e.charCodeAt(R)===59;if(k){R++;const L=v==="named"?lt(M):!1;L&&(T=M,D=L)}let C=1+R-A,O="";if(!(!k&&n.nonTerminated===!1))if(!M)v!=="named"&&E(4,C);else if(v==="named"){if(k&&!D)E(5,1);else if(T!==M&&(R=w+T.length,C=1+R-w,k=!1),!k){const L=T?1:3;if(n.attribute){const z=e.charCodeAt(R);z===61?(E(L,C),D=""):Zl(z)?D="":E(L,C)}else E(L,C)}O=D}else{k||E(2,C);let L=Number.parseInt(M,v==="hexadecimal"?16:10);if($h(L))E(7,C),O="�";else if(L in Kl)E(6,C),O=Kl[L];else{let z="";zh(L)&&E(6,C),L>65535&&(L-=65536,z+=String.fromCharCode(L>>>10|55296),L=56320|L&1023),O=z+String.fromCharCode(L)}}if(O){S(),g=m(),i=R-1,c+=R-A+1,a.push(O);const L=m();L.offset++,n.reference&&n.reference.call(n.referenceContext||void 0,O,{start:g,end:L},e.slice(A-1,R)),g=L}else M=e.slice(A-1,R),s+=M,c+=M.length,i=R-1}else p===10&&(d++,o++,c=0),Number.isNaN(p)?S():(s+=String.fromCharCode(p),c++);return a.join("");function m(){return{line:d,column:c,offset:i+((l?l.offset:0)||0)}}function E(y,A){let w;n.warning&&(w=m(),w.column+=A,w.offset+=A,n.warning.call(n.warningContext||void 0,Bh[y],w,y))}function S(){s&&(a.push(s),n.text&&n.text.call(n.textContext||void 0,s,{start:g,end:m()}),s="")}}function $h(e){return e>=55296&&e<=57343||e>1114111}function zh(e){return e>=1&&e<=8||e===11||e>=13&&e<=31||e>=127&&e<=159||e>=64976&&e<=65007||(e&65535)===65535||(e&65535)===65534}var Gh=0,At={},J={util:{type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++Gh}),e.__id},clone:function e(t,n){n=n||{};var r,a;switch(J.util.type(t)){case"Object":if(a=J.util.objId(t),n[a])return n[a];r={},n[a]=r;for(var i in t)t.hasOwnProperty(i)&&(r[i]=e(t[i],n));return r;case"Array":return a=J.util.objId(t),n[a]?n[a]:(r=[],n[a]=r,t.forEach(function(o,s){r[s]=e(o,n)}),r);default:return t}}},languages:{plain:At,plaintext:At,text:At,txt:At,extend:function(e,t){var n=J.util.clone(J.languages[e]);for(var r in t)n[r]=t[r];return n},insertBefore:function(e,t,n,r){r=r||J.languages;var a=r[e],i={};for(var o in a)if(a.hasOwnProperty(o)){if(o==t)for(var s in n)n.hasOwnProperty(s)&&(i[s]=n[s]);n.hasOwnProperty(o)||(i[o]=a[o])}var l=r[e];return r[e]=i,J.languages.DFS(J.languages,function(u,d){d===l&&u!=e&&(this[u]=i)}),i},DFS:function e(t,n,r,a){a=a||{};var i=J.util.objId;for(var o in t)if(t.hasOwnProperty(o)){n.call(t,o,t[o],r||o);var s=t[o],l=J.util.type(s);l==="Object"&&!a[i(s)]?(a[i(s)]=!0,e(s,n,null,a)):l==="Array"&&!a[i(s)]&&(a[i(s)]=!0,e(s,n,o,a))}}},plugins:{},highlight:function(e,t,n){var r={code:e,grammar:t,language:n};if(J.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=J.tokenize(r.code,r.grammar),J.hooks.run("after-tokenize",r),it.stringify(J.util.encode(r.tokens),r.language)},tokenize:function(e,t){var n=t.rest;if(n){for(var r in n)t[r]=n[r];delete t.rest}var a=new Hh;return Rt(a,a.head,e),cc(e,a,t,a.head,0),Vh(a)},hooks:{all:{},add:function(e,t){var n=J.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=J.hooks.all[e];if(!(!n||!n.length))for(var r=0,a;a=n[r++];)a(t)}},Token:it};function it(e,t,n,r){this.type=e,this.content=t,this.alias=n,this.length=(r||"").length|0}function Xl(e,t,n,r){e.lastIndex=t;var a=e.exec(n);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function cc(e,t,n,r,a,i){for(var o in n)if(!(!n.hasOwnProperty(o)||!n[o])){var s=n[o];s=Array.isArray(s)?s:[s];for(var l=0;l=i.reach);y+=S.value.length,S=S.next){var A=S.value;if(t.length>e.length)return;if(!(A instanceof it)){var w=1,R;if(g){if(R=Xl(E,y,e,c),!R||R.index>=e.length)break;var M=R.index,v=R.index+R[0].length,T=y;for(T+=S.value.length;M>=T;)S=S.next,T+=S.value.length;if(T-=S.value.length,y=T,S.value instanceof it)continue;for(var D=S;D!==t.tail&&(Ti.reach&&(i.reach=O);var L=S.prev;k&&(L=Rt(t,L,k),y+=k.length),jh(t,L,w);var z=new it(o,d?J.tokenize(U,d):U,p,U);if(S=Rt(t,L,z),C&&Rt(t,S,C),w>1){var P={cause:o+","+l,reach:O};cc(e,t,n,S.prev,y,P),i&&P.reach>i.reach&&(i.reach=P.reach)}}}}}}function Hh(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function Rt(e,t,n){var r=t.next,a={value:n,prev:t,next:r};return t.next=a,r.prev=a,e.length++,a}function jh(e,t,n){for(var r=t.next,a=0;a code[class*="language-"]':{background:"#f5f2f0",padding:".1em",borderRadius:".3em",whiteSpace:"normal"},comment:{color:"slategray"},prolog:{color:"slategray"},doctype:{color:"slategray"},cdata:{color:"slategray"},punctuation:{color:"#999"},namespace:{Opacity:".7"},property:{color:"#905"},tag:{color:"#905"},boolean:{color:"#905"},number:{color:"#905"},constant:{color:"#905"},symbol:{color:"#905"},deleted:{color:"#905"},selector:{color:"#690"},"attr-name":{color:"#690"},string:{color:"#690"},char:{color:"#690"},builtin:{color:"#690"},inserted:{color:"#690"},operator:{color:"#9a6e3a",background:"hsla(0, 0%, 100%, .5)"},entity:{color:"#9a6e3a",background:"hsla(0, 0%, 100%, .5)",cursor:"help"},url:{color:"#9a6e3a",background:"hsla(0, 0%, 100%, .5)"},".language-css .token.string":{color:"#9a6e3a",background:"hsla(0, 0%, 100%, .5)"},".style .token.string":{color:"#9a6e3a",background:"hsla(0, 0%, 100%, .5)"},atrule:{color:"#07a"},"attr-value":{color:"#07a"},keyword:{color:"#07a"},function:{color:"#DD4A68"},"class-name":{color:"#DD4A68"},regex:{color:"#e90"},important:{color:"#e90",fontWeight:"bold"},variable:{color:"#e90"},bold:{fontWeight:"bold"},italic:{fontStyle:"italic"}};ne.displayName="markup";ne.aliases=["atom","html","mathml","rss","ssml","svg","xml"];function ne(e){e.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},e.languages.markup.tag.inside["attr-value"].inside.entity=e.languages.markup.entity,e.languages.markup.doctype.inside["internal-subset"].inside=e.languages.markup,e.hooks.add("wrap",function(t){t.type==="entity"&&(t.attributes.title=t.content.value.replace(/&/,"&"))}),Object.defineProperty(e.languages.markup.tag,"addInlined",{value:function(n,r){var a={};a["language-"+r]={pattern:/(^$)/i,lookbehind:!0,inside:e.languages[r]},a.cdata=/^$/i;var i={"included-cdata":{pattern://i,inside:a}};i["language-"+r]={pattern:/[\s\S]+/,inside:e.languages[r]};var o={};o[n]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return n}),"i"),lookbehind:!0,greedy:!0,inside:i},e.languages.insertBefore("markup","cdata",o)}}),Object.defineProperty(e.languages.markup.tag,"addAttribute",{value:function(t,n){e.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+t+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:e.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),e.languages.html=e.languages.markup,e.languages.mathml=e.languages.markup,e.languages.svg=e.languages.markup,e.languages.xml=e.languages.extend("markup",{}),e.languages.ssml=e.languages.xml,e.languages.atom=e.languages.xml,e.languages.rss=e.languages.xml}Me.displayName="css";Me.aliases=[];function Me(e){(function(t){var n=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;t.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+n.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+n.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+n.source+"$"),alias:"url"}}},selector:{pattern:RegExp(`(^|[{}\\s])[^{}\\s](?:[^{};"'\\s]|\\s+(?![\\s{])|`+n.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:n,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},t.languages.css.atrule.inside.rest=t.languages.css;var r=t.languages.markup;r&&(r.tag.addInlined("style","css"),r.tag.addAttribute("style","css"))})(e)}H.displayName="clike";H.aliases=[];function H(e){e.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}}Gn.displayName="regex";Gn.aliases=[];function Gn(e){(function(t){var n={pattern:/\\[\\(){}[\]^$+*?|.]/,alias:"escape"},r=/\\(?:x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]+\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)/,a={pattern:/\.|\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},i={pattern:/\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},o="(?:[^\\\\-]|"+r.source+")",s=RegExp(o+"-"+o),l={pattern:/(<|')[^<>']+(?=[>']$)/,lookbehind:!0,alias:"variable"};t.languages.regex={"char-class":{pattern:/((?:^|[^\\])(?:\\\\)*)\[(?:[^\\\]]|\\[\s\S])*\]/,lookbehind:!0,inside:{"char-class-negation":{pattern:/(^\[)\^/,lookbehind:!0,alias:"operator"},"char-class-punctuation":{pattern:/^\[|\]$/,alias:"punctuation"},range:{pattern:s,inside:{escape:r,"range-punctuation":{pattern:/-/,alias:"operator"}}},"special-escape":n,"char-set":i,escape:r}},"special-escape":n,"char-set":a,backreference:[{pattern:/\\(?![123][0-7]{2})[1-9]/,alias:"keyword"},{pattern:/\\k<[^<>']+>/,alias:"keyword",inside:{"group-name":l}}],anchor:{pattern:/[$^]|\\[ABbGZz]/,alias:"function"},escape:r,group:[{pattern:/\((?:\?(?:<[^<>']+>|'[^<>']+'|[>:]|[=!]|[idmnsuxU]+(?:-[idmnsuxU]+)?:?))?/,alias:"punctuation",inside:{"group-name":l}},{pattern:/\)/,alias:"punctuation"}],quantifier:{pattern:/(?:[+*?]|\{\d+(?:,\d*)?\})[?+]?/,alias:"number"},alternation:{pattern:/\|/,alias:"keyword"}}})(e)}oe.displayName="javascript";oe.aliases=["js"];function oe(e){e.register(H),e.languages.javascript=e.languages.extend("clike",{"class-name":[e.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+(/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source)+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),e.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,e.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:e.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:e.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:e.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:e.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:e.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),e.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:e.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),e.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),e.languages.markup&&(e.languages.markup.tag.addInlined("script","javascript"),e.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),e.languages.js=e.languages.javascript}Hn.displayName="abap";Hn.aliases=[];function Hn(e){e.languages.abap={comment:/^\*.*/m,string:/(`|')(?:\\.|(?!\1)[^\\\r\n])*\1/,"string-template":{pattern:/([|}])(?:\\.|[^\\|{\r\n])*(?=[|{])/,lookbehind:!0,alias:"string"},"eol-comment":{pattern:/(^|\s)".*/m,lookbehind:!0,alias:"comment"},keyword:{pattern:/(\s|\.|^)(?:\*-INPUT|\?TO|ABAP-SOURCE|ABBREVIATED|ABS|ABSTRACT|ACCEPT|ACCEPTING|ACCESSPOLICY|ACCORDING|ACOS|ACTIVATION|ACTUAL|ADD|ADD-CORRESPONDING|ADJACENT|AFTER|ALIAS|ALIASES|ALIGN|ALL|ALLOCATE|ALPHA|ANALYSIS|ANALYZER|AND|ANY|APPEND|APPENDAGE|APPENDING|APPLICATION|ARCHIVE|AREA|ARITHMETIC|AS|ASCENDING|ASIN|ASPECT|ASSERT|ASSIGN|ASSIGNED|ASSIGNING|ASSOCIATION|ASYNCHRONOUS|AT|ATAN|ATTRIBUTES|AUTHORITY|AUTHORITY-CHECK|AVG|BACK|BACKGROUND|BACKUP|BACKWARD|BADI|BASE|BEFORE|BEGIN|BETWEEN|BIG|BINARY|BINDING|BIT|BIT-AND|BIT-NOT|BIT-OR|BIT-XOR|BLACK|BLANK|BLANKS|BLOB|BLOCK|BLOCKS|BLUE|BOUND|BOUNDARIES|BOUNDS|BOXED|BREAK-POINT|BT|BUFFER|BY|BYPASSING|BYTE|BYTE-CA|BYTE-CN|BYTE-CO|BYTE-CS|BYTE-NA|BYTE-NS|BYTE-ORDER|C|CA|CALL|CALLING|CASE|CAST|CASTING|CATCH|CEIL|CENTER|CENTERED|CHAIN|CHAIN-INPUT|CHAIN-REQUEST|CHANGE|CHANGING|CHANNELS|CHAR-TO-HEX|CHARACTER|CHARLEN|CHECK|CHECKBOX|CIRCULAR|CI_|CLASS|CLASS-CODING|CLASS-DATA|CLASS-EVENTS|CLASS-METHODS|CLASS-POOL|CLEANUP|CLEAR|CLIENT|CLOB|CLOCK|CLOSE|CN|CNT|CO|COALESCE|CODE|CODING|COLLECT|COLOR|COLUMN|COLUMNS|COL_BACKGROUND|COL_GROUP|COL_HEADING|COL_KEY|COL_NEGATIVE|COL_NORMAL|COL_POSITIVE|COL_TOTAL|COMMENT|COMMENTS|COMMIT|COMMON|COMMUNICATION|COMPARING|COMPONENT|COMPONENTS|COMPRESSION|COMPUTE|CONCAT|CONCATENATE|COND|CONDENSE|CONDITION|CONNECT|CONNECTION|CONSTANTS|CONTEXT|CONTEXTS|CONTINUE|CONTROL|CONTROLS|CONV|CONVERSION|CONVERT|COPIES|COPY|CORRESPONDING|COS|COSH|COUNT|COUNTRY|COVER|CP|CPI|CREATE|CREATING|CRITICAL|CS|CURRENCY|CURRENCY_CONVERSION|CURRENT|CURSOR|CURSOR-SELECTION|CUSTOMER|CUSTOMER-FUNCTION|DANGEROUS|DATA|DATABASE|DATAINFO|DATASET|DATE|DAYLIGHT|DBMAXLEN|DD\/MM\/YY|DD\/MM\/YYYY|DDMMYY|DEALLOCATE|DECIMALS|DECIMAL_SHIFT|DECLARATIONS|DEEP|DEFAULT|DEFERRED|DEFINE|DEFINING|DEFINITION|DELETE|DELETING|DEMAND|DEPARTMENT|DESCENDING|DESCRIBE|DESTINATION|DETAIL|DIALOG|DIRECTORY|DISCONNECT|DISPLAY|DISPLAY-MODE|DISTANCE|DISTINCT|DIV|DIVIDE|DIVIDE-CORRESPONDING|DIVISION|DO|DUMMY|DUPLICATE|DUPLICATES|DURATION|DURING|DYNAMIC|DYNPRO|E|EACH|EDIT|EDITOR-CALL|ELSE|ELSEIF|EMPTY|ENABLED|ENABLING|ENCODING|END|END-ENHANCEMENT-SECTION|END-LINES|END-OF-DEFINITION|END-OF-FILE|END-OF-PAGE|END-OF-SELECTION|ENDAT|ENDCASE|ENDCATCH|ENDCHAIN|ENDCLASS|ENDDO|ENDENHANCEMENT|ENDEXEC|ENDFOR|ENDFORM|ENDFUNCTION|ENDIAN|ENDIF|ENDING|ENDINTERFACE|ENDLOOP|ENDMETHOD|ENDMODULE|ENDON|ENDPROVIDE|ENDSELECT|ENDTRY|ENDWHILE|ENGINEERING|ENHANCEMENT|ENHANCEMENT-POINT|ENHANCEMENT-SECTION|ENHANCEMENTS|ENTRIES|ENTRY|ENVIRONMENT|EQ|EQUAL|EQUIV|ERRORMESSAGE|ERRORS|ESCAPE|ESCAPING|EVENT|EVENTS|EXACT|EXCEPT|EXCEPTION|EXCEPTION-TABLE|EXCEPTIONS|EXCLUDE|EXCLUDING|EXEC|EXECUTE|EXISTS|EXIT|EXIT-COMMAND|EXP|EXPAND|EXPANDING|EXPIRATION|EXPLICIT|EXPONENT|EXPORT|EXPORTING|EXTEND|EXTENDED|EXTENSION|EXTRACT|FAIL|FETCH|FIELD|FIELD-GROUPS|FIELD-SYMBOL|FIELD-SYMBOLS|FIELDS|FILE|FILTER|FILTER-TABLE|FILTERS|FINAL|FIND|FIRST|FIRST-LINE|FIXED-POINT|FKEQ|FKGE|FLOOR|FLUSH|FONT|FOR|FORM|FORMAT|FORWARD|FOUND|FRAC|FRAME|FRAMES|FREE|FRIENDS|FROM|FUNCTION|FUNCTION-POOL|FUNCTIONALITY|FURTHER|GAPS|GE|GENERATE|GET|GIVING|GKEQ|GKGE|GLOBAL|GRANT|GREATER|GREEN|GROUP|GROUPS|GT|HANDLE|HANDLER|HARMLESS|HASHED|HAVING|HDB|HEAD-LINES|HEADER|HEADERS|HEADING|HELP-ID|HELP-REQUEST|HIDE|HIGH|HINT|HOLD|HOTSPOT|I|ICON|ID|IDENTIFICATION|IDENTIFIER|IDS|IF|IGNORE|IGNORING|IMMEDIATELY|IMPLEMENTATION|IMPLEMENTATIONS|IMPLEMENTED|IMPLICIT|IMPORT|IMPORTING|IN|INACTIVE|INCL|INCLUDE|INCLUDES|INCLUDING|INCREMENT|INDEX|INDEX-LINE|INFOTYPES|INHERITING|INIT|INITIAL|INITIALIZATION|INNER|INOUT|INPUT|INSERT|INSTANCES|INTENSIFIED|INTERFACE|INTERFACE-POOL|INTERFACES|INTERNAL|INTERVALS|INTO|INVERSE|INVERTED-DATE|IS|ISO|ITERATOR|ITNO|JOB|JOIN|KEEP|KEEPING|KERNEL|KEY|KEYS|KEYWORDS|KIND|LANGUAGE|LAST|LATE|LAYOUT|LE|LEADING|LEAVE|LEFT|LEFT-JUSTIFIED|LEFTPLUS|LEFTSPACE|LEGACY|LENGTH|LESS|LET|LEVEL|LEVELS|LIKE|LINE|LINE-COUNT|LINE-SELECTION|LINE-SIZE|LINEFEED|LINES|LIST|LIST-PROCESSING|LISTBOX|LITTLE|LLANG|LOAD|LOAD-OF-PROGRAM|LOB|LOCAL|LOCALE|LOCATOR|LOG|LOG-POINT|LOG10|LOGFILE|LOGICAL|LONG|LOOP|LOW|LOWER|LPAD|LPI|LT|M|MAIL|MAIN|MAJOR-ID|MAPPING|MARGIN|MARK|MASK|MATCH|MATCHCODE|MAX|MAXIMUM|MEDIUM|MEMBERS|MEMORY|MESH|MESSAGE|MESSAGE-ID|MESSAGES|MESSAGING|METHOD|METHODS|MIN|MINIMUM|MINOR-ID|MM\/DD\/YY|MM\/DD\/YYYY|MMDDYY|MOD|MODE|MODIF|MODIFIER|MODIFY|MODULE|MOVE|MOVE-CORRESPONDING|MULTIPLY|MULTIPLY-CORRESPONDING|NA|NAME|NAMETAB|NATIVE|NB|NE|NESTED|NESTING|NEW|NEW-LINE|NEW-PAGE|NEW-SECTION|NEXT|NO|NO-DISPLAY|NO-EXTENSION|NO-GAP|NO-GAPS|NO-GROUPING|NO-HEADING|NO-SCROLLING|NO-SIGN|NO-TITLE|NO-TOPOFPAGE|NO-ZERO|NODE|NODES|NON-UNICODE|NON-UNIQUE|NOT|NP|NS|NULL|NUMBER|NUMOFCHAR|O|OBJECT|OBJECTS|OBLIGATORY|OCCURRENCE|OCCURRENCES|OCCURS|OF|OFF|OFFSET|OLE|ON|ONLY|OPEN|OPTION|OPTIONAL|OPTIONS|OR|ORDER|OTHER|OTHERS|OUT|OUTER|OUTPUT|OUTPUT-LENGTH|OVERFLOW|OVERLAY|PACK|PACKAGE|PAD|PADDING|PAGE|PAGES|PARAMETER|PARAMETER-TABLE|PARAMETERS|PART|PARTIALLY|PATTERN|PERCENTAGE|PERFORM|PERFORMING|PERSON|PF|PF-STATUS|PINK|PLACES|POOL|POSITION|POS_HIGH|POS_LOW|PRAGMAS|PRECOMPILED|PREFERRED|PRESERVING|PRIMARY|PRINT|PRINT-CONTROL|PRIORITY|PRIVATE|PROCEDURE|PROCESS|PROGRAM|PROPERTY|PROTECTED|PROVIDE|PUBLIC|PUSHBUTTON|PUT|QUEUE-ONLY|QUICKINFO|RADIOBUTTON|RAISE|RAISING|RANGE|RANGES|RAW|READ|READ-ONLY|READER|RECEIVE|RECEIVED|RECEIVER|RECEIVING|RED|REDEFINITION|REDUCE|REDUCED|REF|REFERENCE|REFRESH|REGEX|REJECT|REMOTE|RENAMING|REPLACE|REPLACEMENT|REPLACING|REPORT|REQUEST|REQUESTED|RESERVE|RESET|RESOLUTION|RESPECTING|RESPONSIBLE|RESULT|RESULTS|RESUMABLE|RESUME|RETRY|RETURN|RETURNCODE|RETURNING|RIGHT|RIGHT-JUSTIFIED|RIGHTPLUS|RIGHTSPACE|RISK|RMC_COMMUNICATION_FAILURE|RMC_INVALID_STATUS|RMC_SYSTEM_FAILURE|ROLE|ROLLBACK|ROUND|ROWS|RTTI|RUN|SAP|SAP-SPOOL|SAVING|SCALE_PRESERVING|SCALE_PRESERVING_SCIENTIFIC|SCAN|SCIENTIFIC|SCIENTIFIC_WITH_LEADING_ZERO|SCREEN|SCROLL|SCROLL-BOUNDARY|SCROLLING|SEARCH|SECONDARY|SECONDS|SECTION|SELECT|SELECT-OPTIONS|SELECTION|SELECTION-SCREEN|SELECTION-SET|SELECTION-SETS|SELECTION-TABLE|SELECTIONS|SELECTOR|SEND|SEPARATE|SEPARATED|SET|SHARED|SHIFT|SHORT|SHORTDUMP-ID|SIGN|SIGN_AS_POSTFIX|SIMPLE|SIN|SINGLE|SINH|SIZE|SKIP|SKIPPING|SMART|SOME|SORT|SORTABLE|SORTED|SOURCE|SPACE|SPECIFIED|SPLIT|SPOOL|SPOTS|SQL|SQLSCRIPT|SQRT|STABLE|STAMP|STANDARD|START-OF-SELECTION|STARTING|STATE|STATEMENT|STATEMENTS|STATIC|STATICS|STATUSINFO|STEP-LOOP|STOP|STRLEN|STRUCTURE|STRUCTURES|STYLE|SUBKEY|SUBMATCHES|SUBMIT|SUBROUTINE|SUBSCREEN|SUBSTRING|SUBTRACT|SUBTRACT-CORRESPONDING|SUFFIX|SUM|SUMMARY|SUMMING|SUPPLIED|SUPPLY|SUPPRESS|SWITCH|SWITCHSTATES|SYMBOL|SYNCPOINTS|SYNTAX|SYNTAX-CHECK|SYNTAX-TRACE|SYSTEM-CALL|SYSTEM-EXCEPTIONS|SYSTEM-EXIT|TAB|TABBED|TABLE|TABLES|TABLEVIEW|TABSTRIP|TAN|TANH|TARGET|TASK|TASKS|TEST|TESTING|TEXT|TEXTPOOL|THEN|THROW|TIME|TIMES|TIMESTAMP|TIMEZONE|TITLE|TITLE-LINES|TITLEBAR|TO|TOKENIZATION|TOKENS|TOP-LINES|TOP-OF-PAGE|TRACE-FILE|TRACE-TABLE|TRAILING|TRANSACTION|TRANSFER|TRANSFORMATION|TRANSLATE|TRANSPORTING|TRMAC|TRUNC|TRUNCATE|TRUNCATION|TRY|TYPE|TYPE-POOL|TYPE-POOLS|TYPES|ULINE|UNASSIGN|UNDER|UNICODE|UNION|UNIQUE|UNIT|UNIT_CONVERSION|UNIX|UNPACK|UNTIL|UNWIND|UP|UPDATE|UPPER|USER|USER-COMMAND|USING|UTF-8|VALID|VALUE|VALUE-REQUEST|VALUES|VARY|VARYING|VERIFICATION-MESSAGE|VERSION|VIA|VIEW|VISIBLE|WAIT|WARNING|WHEN|WHENEVER|WHERE|WHILE|WIDTH|WINDOW|WINDOWS|WITH|WITH-HEADING|WITH-TITLE|WITHOUT|WORD|WORK|WRITE|WRITER|X|XML|XOR|XSD|XSTRLEN|YELLOW|YES|YYMMDD|Z|ZERO|ZONE)(?![\w-])/i,lookbehind:!0},number:/\b\d+\b/,operator:{pattern:/(\s)(?:\*\*?|<[=>]?|>=?|\?=|[-+\/=])(?=\s)/,lookbehind:!0},"string-operator":{pattern:/(\s)&&?(?=\s)/,lookbehind:!0,alias:"keyword"},"token-operator":[{pattern:/(\w)(?:->?|=>|[~|{}])(?=\w)/,lookbehind:!0,alias:"punctuation"},{pattern:/[|{}]/,alias:"punctuation"}],punctuation:/[,.:()]/}}jn.displayName="abnf";jn.aliases=[];function jn(e){(function(t){var n="(?:ALPHA|BIT|CHAR|CR|CRLF|CTL|DIGIT|DQUOTE|HEXDIG|HTAB|LF|LWSP|OCTET|SP|VCHAR|WSP)";t.languages.abnf={comment:/;.*/,string:{pattern:/(?:%[is])?"[^"\n\r]*"/,greedy:!0,inside:{punctuation:/^%[is]/}},range:{pattern:/%(?:b[01]+-[01]+|d\d+-\d+|x[A-F\d]+-[A-F\d]+)/i,alias:"number"},terminal:{pattern:/%(?:b[01]+(?:\.[01]+)*|d\d+(?:\.\d+)*|x[A-F\d]+(?:\.[A-F\d]+)*)/i,alias:"number"},repetition:{pattern:/(^|[^\w-])(?:\d*\*\d*|\d+)/,lookbehind:!0,alias:"operator"},definition:{pattern:/(^[ \t]*)(?:[a-z][\w-]*|<[^<>\r\n]*>)(?=\s*=)/m,lookbehind:!0,alias:"keyword",inside:{punctuation:/<|>/}},"core-rule":{pattern:RegExp("(?:(^|[^<\\w-])"+n+"|<"+n+">)(?![\\w-])","i"),lookbehind:!0,alias:["rule","constant"],inside:{punctuation:/<|>/}},rule:{pattern:/(^|[^<\w-])[a-z][\w-]*|<[^<>\r\n]*>/i,lookbehind:!0,inside:{punctuation:/<|>/}},operator:/=\/?|\//,punctuation:/[()\[\]]/}})(e)}Vn.displayName="actionscript";Vn.aliases=[];function Vn(e){e.register(oe),e.languages.actionscript=e.languages.extend("javascript",{keyword:/\b(?:as|break|case|catch|class|const|default|delete|do|dynamic|each|else|extends|final|finally|for|function|get|if|implements|import|in|include|instanceof|interface|internal|is|namespace|native|new|null|override|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|use|var|void|while|with)\b/,operator:/\+\+|--|(?:[+\-*\/%^]|&&?|\|\|?|<|>>?>?|[!=]=?)=?|[~?@]/}),e.languages.actionscript["class-name"].alias="function",delete e.languages.actionscript.parameter,delete e.languages.actionscript["literal-property"],e.languages.markup&&e.languages.insertBefore("actionscript","string",{xml:{pattern:/(^|[^.])<\/?\w+(?:\s+[^\s>\/=]+=("|')(?:\\[\s\S]|(?!\2)[^\\])*\2)*\s*\/?>/,lookbehind:!0,inside:e.languages.markup}})}qn.displayName="ada";qn.aliases=[];function qn(e){e.languages.ada={comment:/--.*/,string:/"(?:""|[^"\r\f\n])*"/,number:[{pattern:/\b\d(?:_?\d)*#[\dA-F](?:_?[\dA-F])*(?:\.[\dA-F](?:_?[\dA-F])*)?#(?:E[+-]?\d(?:_?\d)*)?/i},{pattern:/\b\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:E[+-]?\d(?:_?\d)*)?\b/i}],attribute:{pattern:/\b'\w+/,alias:"attr-name"},keyword:/\b(?:abort|abs|abstract|accept|access|aliased|all|and|array|at|begin|body|case|constant|declare|delay|delta|digits|do|else|elsif|end|entry|exception|exit|for|function|generic|goto|if|in|interface|is|limited|loop|mod|new|not|null|of|or|others|out|overriding|package|pragma|private|procedure|protected|raise|range|record|rem|renames|requeue|return|reverse|select|separate|some|subtype|synchronized|tagged|task|terminate|then|type|until|use|when|while|with|xor)\b/i,boolean:/\b(?:false|true)\b/i,operator:/<[=>]?|>=?|=>?|:=|\/=?|\*\*?|[&+-]/,punctuation:/\.\.?|[,;():]/,char:/'.'/,variable:/\b[a-z](?:\w)*\b/i}}Wn.displayName="agda";Wn.aliases=[];function Wn(e){(function(t){t.languages.agda={comment:/\{-[\s\S]*?(?:-\}|$)|--.*/,string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},punctuation:/[(){}⦃⦄.;@]/,"class-name":{pattern:/((?:data|record) +)\S+/,lookbehind:!0},function:{pattern:/(^[ \t]*)(?!\s)[^:\r\n]+(?=:)/m,lookbehind:!0},operator:{pattern:/(^\s*|\s)(?:[=|:∀→λ\\?_]|->)(?=\s)/,lookbehind:!0},keyword:/\b(?:Set|abstract|constructor|data|eta-equality|field|forall|hiding|import|in|inductive|infix|infixl|infixr|instance|let|macro|module|mutual|no-eta-equality|open|overlap|pattern|postulate|primitive|private|public|quote|quoteContext|quoteGoal|quoteTerm|record|renaming|rewrite|syntax|tactic|unquote|unquoteDecl|unquoteDef|using|variable|where|with)\b/}})(e)}Yn.displayName="al";Yn.aliases=[];function Yn(e){e.languages.al={comment:/\/\/.*|\/\*[\s\S]*?\*\//,string:{pattern:/'(?:''|[^'\r\n])*'(?!')|"(?:""|[^"\r\n])*"(?!")/,greedy:!0},function:{pattern:/(\b(?:event|procedure|trigger)\s+|(?:^|[^.])\.\s*)[a-z_]\w*(?=\s*\()/i,lookbehind:!0},keyword:[/\b(?:array|asserterror|begin|break|case|do|downto|else|end|event|exit|for|foreach|function|if|implements|in|indataset|interface|internal|local|of|procedure|program|protected|repeat|runonclient|securityfiltering|suppressdispose|temporary|then|to|trigger|until|var|while|with|withevents)\b/i,/\b(?:action|actions|addafter|addbefore|addfirst|addlast|area|assembly|chartpart|codeunit|column|controladdin|cuegroup|customizes|dataitem|dataset|dotnet|elements|enum|enumextension|extends|field|fieldattribute|fieldelement|fieldgroup|fieldgroups|fields|filter|fixed|grid|group|key|keys|label|labels|layout|modify|moveafter|movebefore|movefirst|movelast|page|pagecustomization|pageextension|part|profile|query|repeater|report|requestpage|schema|separator|systempart|table|tableelement|tableextension|textattribute|textelement|type|usercontrol|value|xmlport)\b/i],number:/\b(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(?:F|LL?|U(?:LL?)?)?\b/i,boolean:/\b(?:false|true)\b/i,variable:/\b(?:Curr(?:FieldNo|Page|Report)|x?Rec|RequestOptionsPage)\b/,"class-name":/\b(?:automation|biginteger|bigtext|blob|boolean|byte|char|clienttype|code|completiontriggererrorlevel|connectiontype|database|dataclassification|datascope|date|dateformula|datetime|decimal|defaultlayout|dialog|dictionary|dotnetassembly|dotnettypedeclaration|duration|errorinfo|errortype|executioncontext|executionmode|fieldclass|fieldref|fieldtype|file|filterpagebuilder|guid|httpclient|httpcontent|httpheaders|httprequestmessage|httpresponsemessage|instream|integer|joker|jsonarray|jsonobject|jsontoken|jsonvalue|keyref|list|moduledependencyinfo|moduleinfo|none|notification|notificationscope|objecttype|option|outstream|pageresult|record|recordid|recordref|reportformat|securityfilter|sessionsettings|tableconnectiontype|tablefilter|testaction|testfield|testfilterfield|testpage|testpermissions|testrequestpage|text|textbuilder|textconst|textencoding|time|transactionmodel|transactiontype|variant|verbosity|version|view|views|webserviceactioncontext|webserviceactionresultcode|xmlattribute|xmlattributecollection|xmlcdata|xmlcomment|xmldeclaration|xmldocument|xmldocumenttype|xmlelement|xmlnamespacemanager|xmlnametable|xmlnode|xmlnodelist|xmlprocessinginstruction|xmlreadoptions|xmltext|xmlwriteoptions)\b/i,operator:/\.\.|:[=:]|[-+*/]=?|<>|[<>]=?|=|\b(?:and|div|mod|not|or|xor)\b/i,punctuation:/[()\[\]{}:.;,]/}}Kn.displayName="antlr4";Kn.aliases=["g4"];function Kn(e){e.languages.antlr4={comment:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,string:{pattern:/'(?:\\.|[^\\'\r\n])*'/,greedy:!0},"character-class":{pattern:/\[(?:\\.|[^\\\]\r\n])*\]/,greedy:!0,alias:"regex",inside:{range:{pattern:/([^[]|(?:^|[^\\])(?:\\\\)*\\\[)-(?!\])/,lookbehind:!0,alias:"punctuation"},escape:/\\(?:u(?:[a-fA-F\d]{4}|\{[a-fA-F\d]+\})|[pP]\{[=\w-]+\}|[^\r\nupP])/,punctuation:/[\[\]]/}},action:{pattern:/\{(?:[^{}]|\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})*\}/,greedy:!0,inside:{content:{pattern:/(\{)[\s\S]+(?=\})/,lookbehind:!0},punctuation:/[{}]/}},command:{pattern:/(->\s*(?!\s))(?:\s*(?:,\s*)?\b[a-z]\w*(?:\s*\([^()\r\n]*\))?)+(?=\s*;)/i,lookbehind:!0,inside:{function:/\b\w+(?=\s*(?:[,(]|$))/,punctuation:/[,()]/}},annotation:{pattern:/@\w+(?:::\w+)*/,alias:"keyword"},label:{pattern:/#[ \t]*\w+/,alias:"punctuation"},keyword:/\b(?:catch|channels|finally|fragment|grammar|import|lexer|locals|mode|options|parser|returns|throws|tokens)\b/,definition:[{pattern:/\b[a-z]\w*(?=\s*:)/,alias:["rule","class-name"]},{pattern:/\b[A-Z]\w*(?=\s*:)/,alias:["token","constant"]}],constant:/\b[A-Z][A-Z_]*\b/,operator:/\.\.|->|[|~]|[*+?]\??/,punctuation:/[;:()=]/},e.languages.g4=e.languages.antlr4}Zn.displayName="apacheconf";Zn.aliases=[];function Zn(e){e.languages.apacheconf={comment:/#.*/,"directive-inline":{pattern:/(^[\t ]*)\b(?:AcceptFilter|AcceptPathInfo|AccessFileName|Action|Add(?:Alt|AltByEncoding|AltByType|Charset|DefaultCharset|Description|Encoding|Handler|Icon|IconByEncoding|IconByType|InputFilter|Language|ModuleInfo|OutputFilter|OutputFilterByType|Type)|Alias|AliasMatch|Allow(?:CONNECT|EncodedSlashes|Methods|Override|OverrideList)?|Anonymous(?:_LogEmail|_MustGiveEmail|_NoUserID|_VerifyEmail)?|AsyncRequestWorkerFactor|Auth(?:BasicAuthoritative|BasicFake|BasicProvider|BasicUseDigestAlgorithm|DBDUserPWQuery|DBDUserRealmQuery|DBMGroupFile|DBMType|DBMUserFile|Digest(?:Algorithm|Domain|NonceLifetime|Provider|Qop|ShmemSize)|Form(?:Authoritative|Body|DisableNoStore|FakeBasicAuth|Location|LoginRequiredLocation|LoginSuccessLocation|LogoutLocation|Method|Mimetype|Password|Provider|SitePassphrase|Size|Username)|GroupFile|LDAP(?:AuthorizePrefix|BindAuthoritative|BindDN|BindPassword|CharsetConfig|CompareAsUser|CompareDNOnServer|DereferenceAliases|GroupAttribute|GroupAttributeIsDN|InitialBindAsUser|InitialBindPattern|MaxSubGroupDepth|RemoteUserAttribute|RemoteUserIsDN|SearchAsUser|SubGroupAttribute|SubGroupClass|Url)|Merging|Name|nCache(?:Context|Enable|ProvideFor|SOCache|Timeout)|nzFcgiCheckAuthnProvider|nzFcgiDefineProvider|Type|UserFile|zDBDLoginToReferer|zDBDQuery|zDBDRedirectQuery|zDBMType|zSendForbiddenOnFailure)|BalancerGrowth|BalancerInherit|BalancerMember|BalancerPersist|BrowserMatch|BrowserMatchNoCase|BufferedLogs|BufferSize|Cache(?:DefaultExpire|DetailHeader|DirLength|DirLevels|Disable|Enable|File|Header|IgnoreCacheControl|IgnoreHeaders|IgnoreNoLastMod|IgnoreQueryString|IgnoreURLSessionIdentifiers|KeyBaseURL|LastModifiedFactor|Lock|LockMaxAge|LockPath|MaxExpire|MaxFileSize|MinExpire|MinFileSize|NegotiatedDocs|QuickHandler|ReadSize|ReadTime|Root|Socache(?:MaxSize|MaxTime|MinTime|ReadSize|ReadTime)?|StaleOnError|StoreExpired|StoreNoStore|StorePrivate)|CGIDScriptTimeout|CGIMapExtension|CharsetDefault|CharsetOptions|CharsetSourceEnc|CheckCaseOnly|CheckSpelling|ChrootDir|ContentDigest|CookieDomain|CookieExpires|CookieName|CookieStyle|CookieTracking|CoreDumpDirectory|CustomLog|Dav|DavDepthInfinity|DavGenericLockDB|DavLockDB|DavMinTimeout|DBDExptime|DBDInitSQL|DBDKeep|DBDMax|DBDMin|DBDParams|DBDPersist|DBDPrepareSQL|DBDriver|DefaultIcon|DefaultLanguage|DefaultRuntimeDir|DefaultType|Define|Deflate(?:BufferSize|CompressionLevel|FilterNote|InflateLimitRequestBody|InflateRatio(?:Burst|Limit)|MemLevel|WindowSize)|Deny|DirectoryCheckHandler|DirectoryIndex|DirectoryIndexRedirect|DirectorySlash|DocumentRoot|DTracePrivileges|DumpIOInput|DumpIOOutput|EnableExceptionHook|EnableMMAP|EnableSendfile|Error|ErrorDocument|ErrorLog|ErrorLogFormat|Example|ExpiresActive|ExpiresByType|ExpiresDefault|ExtendedStatus|ExtFilterDefine|ExtFilterOptions|FallbackResource|FileETag|FilterChain|FilterDeclare|FilterProtocol|FilterProvider|FilterTrace|ForceLanguagePriority|ForceType|ForensicLog|GprofDir|GracefulShutdownTimeout|Group|Header|HeaderName|Heartbeat(?:Address|Listen|MaxServers|Storage)|HostnameLookups|IdentityCheck|IdentityCheckTimeout|ImapBase|ImapDefault|ImapMenu|Include|IncludeOptional|Index(?:HeadInsert|Ignore|IgnoreReset|Options|OrderDefault|StyleSheet)|InputSed|ISAPI(?:AppendLogToErrors|AppendLogToQuery|CacheFile|FakeAsync|LogNotSupported|ReadAheadBuffer)|KeepAlive|KeepAliveTimeout|KeptBodySize|LanguagePriority|LDAP(?:CacheEntries|CacheTTL|ConnectionPoolTTL|ConnectionTimeout|LibraryDebug|OpCacheEntries|OpCacheTTL|ReferralHopLimit|Referrals|Retries|RetryDelay|SharedCacheFile|SharedCacheSize|Timeout|TrustedClientCert|TrustedGlobalCert|TrustedMode|VerifyServerCert)|Limit(?:InternalRecursion|Request(?:Body|Fields|FieldSize|Line)|XMLRequestBody)|Listen|ListenBackLog|LoadFile|LoadModule|LogFormat|LogLevel|LogMessage|LuaAuthzProvider|LuaCodeCache|Lua(?:Hook(?:AccessChecker|AuthChecker|CheckUserID|Fixups|InsertFilter|Log|MapToStorage|TranslateName|TypeChecker)|Inherit|InputFilter|MapHandler|OutputFilter|PackageCPath|PackagePath|QuickHandler|Root|Scope)|Max(?:ConnectionsPerChild|KeepAliveRequests|MemFree|RangeOverlaps|RangeReversals|Ranges|RequestWorkers|SpareServers|SpareThreads|Threads)|MergeTrailers|MetaDir|MetaFiles|MetaSuffix|MimeMagicFile|MinSpareServers|MinSpareThreads|MMapFile|ModemStandard|ModMimeUsePathInfo|MultiviewsMatch|Mutex|NameVirtualHost|NoProxy|NWSSLTrustedCerts|NWSSLUpgradeable|Options|Order|OutputSed|PassEnv|PidFile|PrivilegesMode|Protocol|ProtocolEcho|Proxy(?:AddHeaders|BadHeader|Block|Domain|ErrorOverride|ExpressDBMFile|ExpressDBMType|ExpressEnable|FtpDirCharset|FtpEscapeWildcards|FtpListOnWildcard|HTML(?:BufSize|CharsetOut|DocType|Enable|Events|Extended|Fixups|Interp|Links|Meta|StripComments|URLMap)|IOBufferSize|MaxForwards|Pass(?:Inherit|InterpolateEnv|Match|Reverse|ReverseCookieDomain|ReverseCookiePath)?|PreserveHost|ReceiveBufferSize|Remote|RemoteMatch|Requests|SCGIInternalRedirect|SCGISendfile|Set|SourceAddress|Status|Timeout|Via)|ReadmeName|ReceiveBufferSize|Redirect|RedirectMatch|RedirectPermanent|RedirectTemp|ReflectorHeader|RemoteIP(?:Header|InternalProxy|InternalProxyList|ProxiesHeader|TrustedProxy|TrustedProxyList)|RemoveCharset|RemoveEncoding|RemoveHandler|RemoveInputFilter|RemoveLanguage|RemoveOutputFilter|RemoveType|RequestHeader|RequestReadTimeout|Require|Rewrite(?:Base|Cond|Engine|Map|Options|Rule)|RLimitCPU|RLimitMEM|RLimitNPROC|Satisfy|ScoreBoardFile|Script(?:Alias|AliasMatch|InterpreterSource|Log|LogBuffer|LogLength|Sock)?|SecureListen|SeeRequestTail|SendBufferSize|Server(?:Admin|Alias|Limit|Name|Path|Root|Signature|Tokens)|Session(?:Cookie(?:Name|Name2|Remove)|Crypto(?:Cipher|Driver|Passphrase|PassphraseFile)|DBD(?:CookieName|CookieName2|CookieRemove|DeleteLabel|InsertLabel|PerUser|SelectLabel|UpdateLabel)|Env|Exclude|Header|Include|MaxAge)?|SetEnv|SetEnvIf|SetEnvIfExpr|SetEnvIfNoCase|SetHandler|SetInputFilter|SetOutputFilter|SSIEndTag|SSIErrorMsg|SSIETag|SSILastModified|SSILegacyExprParser|SSIStartTag|SSITimeFormat|SSIUndefinedEcho|SSL(?:CACertificateFile|CACertificatePath|CADNRequestFile|CADNRequestPath|CARevocationCheck|CARevocationFile|CARevocationPath|CertificateChainFile|CertificateFile|CertificateKeyFile|CipherSuite|Compression|CryptoDevice|Engine|FIPS|HonorCipherOrder|InsecureRenegotiation|OCSP(?:DefaultResponder|Enable|OverrideResponder|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|UseRequestNonce)|OpenSSLConfCmd|Options|PassPhraseDialog|Protocol|Proxy(?:CACertificateFile|CACertificatePath|CARevocation(?:Check|File|Path)|CheckPeer(?:CN|Expire|Name)|CipherSuite|Engine|MachineCertificate(?:ChainFile|File|Path)|Protocol|Verify|VerifyDepth)|RandomSeed|RenegBufferSize|Require|RequireSSL|Session(?:Cache|CacheTimeout|TicketKeyFile|Tickets)|SRPUnknownUserSeed|SRPVerifierFile|Stapling(?:Cache|ErrorCacheTimeout|FakeTryLater|ForceURL|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|ReturnResponderErrors|StandardCacheTimeout)|StrictSNIVHostCheck|UserName|UseStapling|VerifyClient|VerifyDepth)|StartServers|StartThreads|Substitute|Suexec|SuexecUserGroup|ThreadLimit|ThreadsPerChild|ThreadStackSize|TimeOut|TraceEnable|TransferLog|TypesConfig|UnDefine|UndefMacro|UnsetEnv|Use|UseCanonicalName|UseCanonicalPhysicalPort|User|UserDir|VHostCGIMode|VHostCGIPrivs|VHostGroup|VHostPrivs|VHostSecure|VHostUser|Virtual(?:DocumentRoot|ScriptAlias)(?:IP)?|WatchdogInterval|XBitHack|xml2EncAlias|xml2EncDefault|xml2StartParse)\b/im,lookbehind:!0,alias:"property"},"directive-block":{pattern:/<\/?\b(?:Auth[nz]ProviderAlias|Directory|DirectoryMatch|Else|ElseIf|Files|FilesMatch|If|IfDefine|IfModule|IfVersion|Limit|LimitExcept|Location|LocationMatch|Macro|Proxy|Require(?:All|Any|None)|VirtualHost)\b.*>/i,inside:{"directive-block":{pattern:/^<\/?\w+/,inside:{punctuation:/^<\/?/},alias:"tag"},"directive-block-parameter":{pattern:/.*[^>]/,inside:{punctuation:/:/,string:{pattern:/("|').*\1/,inside:{variable:/[$%]\{?(?:\w\.?[-+:]?)+\}?/}}},alias:"attr-value"},punctuation:/>/},alias:"tag"},"directive-flags":{pattern:/\[(?:[\w=],?)+\]/,alias:"keyword"},string:{pattern:/("|').*\1/,inside:{variable:/[$%]\{?(?:\w\.?[-+:]?)+\}?/}},variable:/[$%]\{?(?:\w\.?[-+:]?)+\}?/,regex:/\^?.*\$|\^.*\$?/}}gt.displayName="sql";gt.aliases=[];function gt(e){e.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:FALSE|NULL|TRUE)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/}}Xn.displayName="apex";Xn.aliases=[];function Xn(e){e.register(H),e.register(gt),function(t){var n=/\b(?:(?:after|before)(?=\s+[a-z])|abstract|activate|and|any|array|as|asc|autonomous|begin|bigdecimal|blob|boolean|break|bulk|by|byte|case|cast|catch|char|class|collect|commit|const|continue|currency|date|datetime|decimal|default|delete|desc|do|double|else|end|enum|exception|exit|export|extends|final|finally|float|for|from|get(?=\s*[{};])|global|goto|group|having|hint|if|implements|import|in|inner|insert|instanceof|int|integer|interface|into|join|like|limit|list|long|loop|map|merge|new|not|null|nulls|number|object|of|on|or|outer|override|package|parallel|pragma|private|protected|public|retrieve|return|rollback|select|set|short|sObject|sort|static|string|super|switch|synchronized|system|testmethod|then|this|throw|time|transaction|transient|trigger|try|undelete|update|upsert|using|virtual|void|webservice|when|where|while|(?:inherited|with|without)\s+sharing)\b/i,r=/\b(?:(?=[a-z_]\w*\s*[<\[])|(?!))[A-Z_]\w*(?:\s*\.\s*[A-Z_]\w*)*\b(?:\s*(?:\[\s*\]|<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>))*/.source.replace(//g,function(){return n.source});function a(o){return RegExp(o.replace(//g,function(){return r}),"i")}var i={keyword:n,punctuation:/[()\[\]{};,:.<>]/};t.languages.apex={comment:t.languages.clike.comment,string:t.languages.clike.string,sql:{pattern:/((?:[=,({:]|\breturn)\s*)\[[^\[\]]*\]/i,lookbehind:!0,greedy:!0,alias:"language-sql",inside:t.languages.sql},annotation:{pattern:/@\w+\b/,alias:"punctuation"},"class-name":[{pattern:a(/(\b(?:class|enum|extends|implements|instanceof|interface|new|trigger\s+\w+\s+on)\s+)/.source),lookbehind:!0,inside:i},{pattern:a(/(\(\s*)(?=\s*\)\s*[\w(])/.source),lookbehind:!0,inside:i},{pattern:a(/(?=\s*\w+\s*[;=,(){:])/.source),inside:i}],trigger:{pattern:/(\btrigger\s+)\w+\b/i,lookbehind:!0,alias:"class-name"},keyword:n,function:/\b[a-z_]\w*(?=\s*\()/i,boolean:/\b(?:false|true)\b/i,number:/(?:\B\.\d+|\b\d+(?:\.\d+|L)?)\b/i,operator:/[!=](?:==?)?|\?\.?|&&|\|\||--|\+\+|[-+*/^&|]=?|:|<=?|>{1,3}=?/,punctuation:/[()\[\]{};,.]/}}(e)}Qn.displayName="apl";Qn.aliases=[];function Qn(e){e.languages.apl={comment:/(?:⍝|#[! ]).*$/m,string:{pattern:/'(?:[^'\r\n]|'')*'/,greedy:!0},number:/¯?(?:\d*\.?\b\d+(?:e[+¯]?\d+)?|¯|∞)(?:j¯?(?:(?:\d+(?:\.\d+)?|\.\d+)(?:e[+¯]?\d+)?|¯|∞))?/i,statement:/:[A-Z][a-z][A-Za-z]*\b/,"system-function":{pattern:/⎕[A-Z]+/i,alias:"function"},constant:/[⍬⌾#⎕⍞]/,function:/[-+×÷⌈⌊∣|⍳⍸?*⍟○!⌹<≤=>≥≠≡≢∊⍷∪∩~∨∧⍱⍲⍴,⍪⌽⊖⍉↑↓⊂⊃⊆⊇⌷⍋⍒⊤⊥⍕⍎⊣⊢⍁⍂≈⍯↗¤→]/,"monadic-operator":{pattern:/[\\\/⌿⍀¨⍨⌶&∥]/,alias:"operator"},"dyadic-operator":{pattern:/[.⍣⍠⍤∘⌸@⌺⍥]/,alias:"operator"},assignment:{pattern:/←/,alias:"keyword"},punctuation:/[\[;\]()◇⋄]/,dfn:{pattern:/[{}⍺⍵⍶⍹∇⍫:]/,alias:"builtin"}}}Jn.displayName="applescript";Jn.aliases=[];function Jn(e){e.languages.applescript={comment:[/\(\*(?:\(\*(?:[^*]|\*(?!\)))*\*\)|(?!\(\*)[\s\S])*?\*\)/,/--.+/,/#.+/],string:/"(?:\\.|[^"\\\r\n])*"/,number:/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e-?\d+)?\b/i,operator:[/[&=≠≤≥*+\-\/÷^]|[<>]=?/,/\b(?:(?:begin|end|start)s? with|(?:contains?|(?:does not|doesn't) contain)|(?:is|isn't|is not) (?:contained by|in)|(?:(?:is|isn't|is not) )?(?:greater|less) than(?: or equal)?(?: to)?|(?:comes|(?:does not|doesn't) come) (?:after|before)|(?:is|isn't|is not) equal(?: to)?|(?:(?:does not|doesn't) equal|equal to|equals|is not|isn't)|(?:a )?(?:ref(?: to)?|reference to)|(?:and|as|div|mod|not|or))\b/],keyword:/\b(?:about|above|after|against|apart from|around|aside from|at|back|before|beginning|behind|below|beneath|beside|between|but|by|considering|continue|copy|does|eighth|else|end|equal|error|every|exit|false|fifth|first|for|fourth|from|front|get|given|global|if|ignoring|in|instead of|into|is|it|its|last|local|me|middle|my|ninth|of|on|onto|out of|over|prop|property|put|repeat|return|returning|second|set|seventh|since|sixth|some|tell|tenth|that|the|then|third|through|thru|timeout|times|to|transaction|true|try|until|where|while|whose|with|without)\b/,"class-name":/\b(?:POSIX file|RGB color|alias|application|boolean|centimeters|centimetres|class|constant|cubic centimeters|cubic centimetres|cubic feet|cubic inches|cubic meters|cubic metres|cubic yards|date|degrees Celsius|degrees Fahrenheit|degrees Kelvin|feet|file|gallons|grams|inches|integer|kilograms|kilometers|kilometres|list|liters|litres|meters|metres|miles|number|ounces|pounds|quarts|real|record|reference|script|square feet|square kilometers|square kilometres|square meters|square metres|square miles|square yards|text|yards)\b/,punctuation:/[{}():,¬«»《》]/}}er.displayName="aql";er.aliases=[];function er(e){e.languages.aql={comment:/\/\/.*|\/\*[\s\S]*?\*\//,property:{pattern:/([{,]\s*)(?:(?!\d)\w+|(["'´`])(?:(?!\2)[^\\\r\n]|\\.)*\2)(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\.)*\1/,greedy:!0},identifier:{pattern:/([´`])(?:(?!\1)[^\\\r\n]|\\.)*\1/,greedy:!0},variable:/@@?\w+/,keyword:[{pattern:/(\bWITH\s+)COUNT(?=\s+INTO\b)/i,lookbehind:!0},/\b(?:AGGREGATE|ALL|AND|ANY|ASC|COLLECT|DESC|DISTINCT|FILTER|FOR|GRAPH|IN|INBOUND|INSERT|INTO|K_PATHS|K_SHORTEST_PATHS|LET|LIKE|LIMIT|NONE|NOT|NULL|OR|OUTBOUND|REMOVE|REPLACE|RETURN|SHORTEST_PATH|SORT|UPDATE|UPSERT|WINDOW|WITH)\b/i,{pattern:/(^|[^\w.[])(?:KEEP|PRUNE|SEARCH|TO)\b/i,lookbehind:!0},{pattern:/(^|[^\w.[])(?:CURRENT|NEW|OLD)\b/,lookbehind:!0},{pattern:/\bOPTIONS(?=\s*\{)/i}],function:/\b(?!\d)\w+(?=\s*\()/,boolean:/\b(?:false|true)\b/i,range:{pattern:/\.\./,alias:"operator"},number:[/\b0b[01]+/i,/\b0x[0-9a-f]+/i,/(?:\B\.\d+|\b(?:0|[1-9]\d*)(?:\.\d+)?)(?:e[+-]?\d+)?/i],operator:/\*{2,}|[=!]~|[!=<>]=?|&&|\|\||[-+*/%]/,punctuation:/::|[?.:,;()[\]{}]/}}ke.displayName="c";ke.aliases=[];function ke(e){e.register(H),e.languages.c=e.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),e.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),e.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},e.languages.c.string],char:e.languages.c.char,comment:e.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:e.languages.c}}}}),e.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete e.languages.c.boolean}Ve.displayName="cpp";Ve.aliases=[];function Ve(e){e.register(ke),function(t){var n=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,r=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,function(){return n.source});t.languages.cpp=t.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,function(){return n.source})),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:n,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),t.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,function(){return r})+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),t.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t.languages.cpp}}}}),t.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),t.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:t.languages.extend("cpp",{})}}),t.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},t.languages.cpp["base-clause"])}(e)}tr.displayName="arduino";tr.aliases=["ino"];function tr(e){e.register(Ve),e.languages.arduino=e.languages.extend("cpp",{keyword:/\b(?:String|array|bool|boolean|break|byte|case|catch|continue|default|do|double|else|finally|for|function|goto|if|in|instanceof|int|integer|long|loop|new|null|return|setup|string|switch|throw|try|void|while|word)\b/,constant:/\b(?:ANALOG_MESSAGE|DEFAULT|DIGITAL_MESSAGE|EXTERNAL|FIRMATA_STRING|HIGH|INPUT|INPUT_PULLUP|INTERNAL|INTERNAL1V1|INTERNAL2V56|LED_BUILTIN|LOW|OUTPUT|REPORT_ANALOG|REPORT_DIGITAL|SET_PIN_MODE|SYSEX_START|SYSTEM_RESET)\b/,builtin:/\b(?:Audio|BSSID|Bridge|Client|Console|EEPROM|Esplora|EsploraTFT|Ethernet|EthernetClient|EthernetServer|EthernetUDP|File|FileIO|FileSystem|Firmata|GPRS|GSM|GSMBand|GSMClient|GSMModem|GSMPIN|GSMScanner|GSMServer|GSMVoiceCall|GSM_SMS|HttpClient|IPAddress|IRread|Keyboard|KeyboardController|LiquidCrystal|LiquidCrystal_I2C|Mailbox|Mouse|MouseController|PImage|Process|RSSI|RobotControl|RobotMotor|SD|SPI|SSID|Scheduler|Serial|Server|Servo|SoftwareSerial|Stepper|Stream|TFT|Task|USBHost|WiFi|WiFiClient|WiFiServer|WiFiUDP|Wire|YunClient|YunServer|abs|addParameter|analogRead|analogReadResolution|analogReference|analogWrite|analogWriteResolution|answerCall|attach|attachGPRS|attachInterrupt|attached|autoscroll|available|background|beep|begin|beginPacket|beginSD|beginSMS|beginSpeaker|beginTFT|beginTransmission|beginWrite|bit|bitClear|bitRead|bitSet|bitWrite|blink|blinkVersion|buffer|changePIN|checkPIN|checkPUK|checkReg|circle|cityNameRead|cityNameWrite|clear|clearScreen|click|close|compassRead|config|connect|connected|constrain|cos|countryNameRead|countryNameWrite|createChar|cursor|debugPrint|delay|delayMicroseconds|detach|detachInterrupt|digitalRead|digitalWrite|disconnect|display|displayLogos|drawBMP|drawCompass|encryptionType|end|endPacket|endSMS|endTransmission|endWrite|exists|exitValue|fill|find|findUntil|flush|gatewayIP|get|getAsynchronously|getBand|getButton|getCurrentCarrier|getIMEI|getKey|getModifiers|getOemKey|getPINUsed|getResult|getSignalStrength|getSocket|getVoiceCallStatus|getXChange|getYChange|hangCall|height|highByte|home|image|interrupts|isActionDone|isDirectory|isListening|isPIN|isPressed|isValid|keyPressed|keyReleased|keyboardRead|knobRead|leftToRight|line|lineFollowConfig|listen|listenOnLocalhost|loadImage|localIP|lowByte|macAddress|maintain|map|max|messageAvailable|micros|millis|min|mkdir|motorsStop|motorsWrite|mouseDragged|mouseMoved|mousePressed|mouseReleased|move|noAutoscroll|noBlink|noBuffer|noCursor|noDisplay|noFill|noInterrupts|noListenOnLocalhost|noStroke|noTone|onReceive|onRequest|open|openNextFile|overflow|parseCommand|parseFloat|parseInt|parsePacket|pauseMode|peek|pinMode|playFile|playMelody|point|pointTo|position|pow|prepare|press|print|printFirmwareVersion|printVersion|println|process|processInput|pulseIn|put|random|randomSeed|read|readAccelerometer|readBlue|readButton|readBytes|readBytesUntil|readGreen|readJoystickButton|readJoystickSwitch|readJoystickX|readJoystickY|readLightSensor|readMessage|readMicrophone|readNetworks|readRed|readSlider|readString|readStringUntil|readTemperature|ready|rect|release|releaseAll|remoteIP|remoteNumber|remotePort|remove|requestFrom|retrieveCallingNumber|rewindDirectory|rightToLeft|rmdir|robotNameRead|robotNameWrite|run|runAsynchronously|runShellCommand|runShellCommandAsynchronously|running|scanNetworks|scrollDisplayLeft|scrollDisplayRight|seek|sendAnalog|sendDigitalPortPair|sendDigitalPorts|sendString|sendSysex|serialEvent|setBand|setBitOrder|setClockDivider|setCursor|setDNS|setDataMode|setFirmwareVersion|setMode|setPINUsed|setSpeed|setTextSize|setTimeout|shiftIn|shiftOut|shutdown|sin|size|sqrt|startLoop|step|stop|stroke|subnetMask|switchPIN|tan|tempoWrite|text|tone|transfer|tuneWrite|turn|updateIR|userNameRead|userNameWrite|voiceCall|waitContinue|width|write|writeBlue|writeGreen|writeJSON|writeMessage|writeMicroseconds|writeRGB|writeRed|yield)\b/}),e.languages.ino=e.languages.arduino}nr.displayName="arff";nr.aliases=[];function nr(e){e.languages.arff={comment:/%.*/,string:{pattern:/(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},keyword:/@(?:attribute|data|end|relation)\b/i,number:/\b\d+(?:\.\d+)?\b/,punctuation:/[{},]/}}rr.displayName="armasm";rr.aliases=["arm-asm"];function rr(e){e.languages.armasm={comment:{pattern:/;.*/,greedy:!0},string:{pattern:/"(?:[^"\r\n]|"")*"/,greedy:!0,inside:{variable:{pattern:/((?:^|[^$])(?:\${2})*)\$\w+/,lookbehind:!0}}},char:{pattern:/'(?:[^'\r\n]{0,4}|'')'/,greedy:!0},"version-symbol":{pattern:/\|[\w@]+\|/,greedy:!0,alias:"property"},boolean:/\b(?:FALSE|TRUE)\b/,directive:{pattern:/\b(?:ALIAS|ALIGN|AREA|ARM|ASSERT|ATTR|CN|CODE|CODE16|CODE32|COMMON|CP|DATA|DCB|DCD|DCDO|DCDU|DCFD|DCFDU|DCI|DCQ|DCQU|DCW|DCWU|DN|ELIF|ELSE|END|ENDFUNC|ENDIF|ENDP|ENTRY|EQU|EXPORT|EXPORTAS|EXTERN|FIELD|FILL|FN|FUNCTION|GBLA|GBLL|GBLS|GET|GLOBAL|IF|IMPORT|INCBIN|INCLUDE|INFO|KEEP|LCLA|LCLL|LCLS|LTORG|MACRO|MAP|MEND|MEXIT|NOFP|OPT|PRESERVE8|PROC|QN|READONLY|RELOC|REQUIRE|REQUIRE8|RLIST|ROUT|SETA|SETL|SETS|SN|SPACE|SUBT|THUMB|THUMBX|TTL|WEND|WHILE)\b/,alias:"property"},instruction:{pattern:/((?:^|(?:^|[^\\])(?:\r\n?|\n))[ \t]*(?:(?:[A-Z][A-Z0-9_]*[a-z]\w*|[a-z]\w*|\d+)[ \t]+)?)\b[A-Z.]+\b/,lookbehind:!0,alias:"keyword"},variable:/\$\w+/,number:/(?:\b[2-9]_\d+|(?:\b\d+(?:\.\d+)?|\B\.\d+)(?:e-?\d+)?|\b0(?:[fd]_|x)[0-9a-f]+|&[0-9a-f]+)\b/i,register:{pattern:/\b(?:r\d|lr)\b/,alias:"symbol"},operator:/<>|<<|>>|&&|\|\||[=!<>/]=?|[+\-*%#?&|^]|:[A-Z]+:/,punctuation:/[()[\],]/},e.languages["arm-asm"]=e.languages.armasm}zt.displayName="bash";zt.aliases=["sh","shell"];function zt(e){(function(t){var n="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",r={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},a={bash:r,environment:{pattern:RegExp("\\$"+n),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+n),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};t.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+n),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:a},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:r}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:a},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:a.entity}}],environment:{pattern:RegExp("\\$?"+n),alias:"constant"},variable:a.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},r.inside=t.languages.bash;for(var i=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=a.variable[1].inside,s=0;s|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,a="(?:"+r.source+"(?:[ ]+"+n.source+")?|"+n.source+"(?:[ ]+"+r.source+")?)",i=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source}),o=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function s(l,u){u=(u||"").replace(/m/g,"")+"m";var d=/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,function(){return a}).replace(/<>/g,function(){return l});return RegExp(d,u)}t.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,function(){return a})),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,function(){return a}).replace(/<>/g,function(){return"(?:"+i+"|"+o+")"})),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:s(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:s(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:s(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:s(o),lookbehind:!0,greedy:!0},number:{pattern:s(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},t.languages.yml=t.languages.yaml})(e)}ar.displayName="markdown";ar.aliases=["md"];function ar(e){e.register(ne),function(t){var n=/(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source;function r(s){return s=s.replace(//g,function(){return n}),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+s+")")}var a=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,i=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,function(){return a}),o=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source;t.languages.markdown=t.languages.extend("markup",{}),t.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:t.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+i+o+"(?:"+i+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+i+o+")(?:"+i+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(a),inside:t.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+i+")"+o+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+i+"$"),inside:{"table-header":{pattern:RegExp(a),alias:"important",inside:t.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:r(/\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:r(/\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:r(/(~~?)(?:(?!~))+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:r(/!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach(function(s){["url","bold","italic","strike","code-snippet"].forEach(function(l){s!==l&&(t.languages.markdown[s].inside.content.inside[l]=t.languages.markdown[l])})}),t.hooks.add("after-tokenize",function(s){if(s.language!=="markdown"&&s.language!=="md")return;function l(u){if(!(!u||typeof u=="string"))for(var d=0,c=u.length;d|=>|\||::/,alias:"operator"},punctuation:/[()[\],]/,symbol:{pattern:/<:|-:|ø|@|#|\+|\||\*|\$|---|-|%|\/|\.\.|\^|~|=|<|>|\\/},boolean:{pattern:/\b(?:false|maybe|true)\b/}},t.languages.art=t.languages.arturo})(e)}or.displayName="asciidoc";or.aliases=["adoc"];function or(e){(function(t){var n={pattern:/(^[ \t]*)\[(?!\[)(?:(["'$`])(?:(?!\2)[^\\]|\\.)*\2|\[(?:[^\[\]\\]|\\.)*\]|[^\[\]\\"'$`]|\\.)*\]/m,lookbehind:!0,inside:{quoted:{pattern:/([$`])(?:(?!\1)[^\\]|\\.)*\1/,inside:{punctuation:/^[$`]|[$`]$/}},interpreted:{pattern:/'(?:[^'\\]|\\.)*'/,inside:{punctuation:/^'|'$/}},string:/"(?:[^"\\]|\\.)*"/,variable:/\w+(?==)/,punctuation:/^\[|\]$|,/,operator:/=/,"attr-value":/(?!^\s+$).+/}},r=t.languages.asciidoc={"comment-block":{pattern:/^(\/{4,})$[\s\S]*?^\1/m,alias:"comment"},table:{pattern:/^\|={3,}(?:(?:\r?\n|\r(?!\n)).*)*?(?:\r?\n|\r)\|={3,}$/m,inside:{specifiers:{pattern:/(?:(?:(?:\d+(?:\.\d+)?|\.\d+)[+*](?:[<^>](?:\.[<^>])?|\.[<^>])?|[<^>](?:\.[<^>])?|\.[<^>])[a-z]*|[a-z]+)(?=\|)/,alias:"attr-value"},punctuation:{pattern:/(^|[^\\])[|!]=*/,lookbehind:!0}}},"passthrough-block":{pattern:/^(\+{4,})$[\s\S]*?^\1$/m,inside:{punctuation:/^\++|\++$/}},"literal-block":{pattern:/^(-{4,}|\.{4,})$[\s\S]*?^\1$/m,inside:{punctuation:/^(?:-+|\.+)|(?:-+|\.+)$/}},"other-block":{pattern:/^(--|\*{4,}|_{4,}|={4,})$[\s\S]*?^\1$/m,inside:{punctuation:/^(?:-+|\*+|_+|=+)|(?:-+|\*+|_+|=+)$/}},"list-punctuation":{pattern:/(^[ \t]*)(?:-|\*{1,5}|\.{1,5}|(?:[a-z]|\d+)\.|[xvi]+\))(?= )/im,lookbehind:!0,alias:"punctuation"},"list-label":{pattern:/(^[ \t]*)[a-z\d].+(?::{2,4}|;;)(?=\s)/im,lookbehind:!0,alias:"symbol"},"indented-block":{pattern:/((\r?\n|\r)\2)([ \t]+)\S.*(?:(?:\r?\n|\r)\3.+)*(?=\2{2}|$)/,lookbehind:!0},comment:/^\/\/.*/m,title:{pattern:/^.+(?:\r?\n|\r)(?:={3,}|-{3,}|~{3,}|\^{3,}|\+{3,})$|^={1,5} .+|^\.(?![\s.]).*/m,alias:"important",inside:{punctuation:/^(?:\.|=+)|(?:=+|-+|~+|\^+|\++)$/}},"attribute-entry":{pattern:/^:[^:\r\n]+:(?: .*?(?: \+(?:\r?\n|\r).*?)*)?$/m,alias:"tag"},attributes:n,hr:{pattern:/^'{3,}$/m,alias:"punctuation"},"page-break":{pattern:/^<{3,}$/m,alias:"punctuation"},admonition:{pattern:/^(?:CAUTION|IMPORTANT|NOTE|TIP|WARNING):/m,alias:"keyword"},callout:[{pattern:/(^[ \t]*)\d*>/m,lookbehind:!0,alias:"symbol"},{pattern:/<\d+>/,alias:"symbol"}],macro:{pattern:/\b[a-z\d][a-z\d-]*::?(?:[^\s\[\]]*\[(?:[^\]\\"']|(["'])(?:(?!\1)[^\\]|\\.)*\1|\\.)*\])/,inside:{function:/^[a-z\d-]+(?=:)/,punctuation:/^::?/,attributes:{pattern:/(?:\[(?:[^\]\\"']|(["'])(?:(?!\1)[^\\]|\\.)*\1|\\.)*\])/,inside:n.inside}}},inline:{pattern:/(^|[^\\])(?:(?:\B\[(?:[^\]\\"']|(["'])(?:(?!\2)[^\\]|\\.)*\2|\\.)*\])?(?:\b_(?!\s)(?: _|[^_\\\r\n]|\\.)+(?:(?:\r?\n|\r)(?: _|[^_\\\r\n]|\\.)+)*_\b|\B``(?!\s).+?(?:(?:\r?\n|\r).+?)*''\B|\B`(?!\s)(?:[^`'\s]|\s+\S)+['`]\B|\B(['*+#])(?!\s)(?: \3|(?!\3)[^\\\r\n]|\\.)+(?:(?:\r?\n|\r)(?: \3|(?!\3)[^\\\r\n]|\\.)+)*\3\B)|(?:\[(?:[^\]\\"']|(["'])(?:(?!\4)[^\\]|\\.)*\4|\\.)*\])?(?:(__|\*\*|\+\+\+?|##|\$\$|[~^]).+?(?:(?:\r?\n|\r).+?)*\5|\{[^}\r\n]+\}|\[\[\[?.+?(?:(?:\r?\n|\r).+?)*\]?\]\]|<<.+?(?:(?:\r?\n|\r).+?)*>>|\(\(\(?.+?(?:(?:\r?\n|\r).+?)*\)?\)\)))/m,lookbehind:!0,inside:{attributes:n,url:{pattern:/^(?:\[\[\[?.+?\]?\]\]|<<.+?>>)$/,inside:{punctuation:/^(?:\[\[\[?|<<)|(?:\]\]\]?|>>)$/}},"attribute-ref":{pattern:/^\{.+\}$/,inside:{variable:{pattern:/(^\{)[a-z\d,+_-]+/,lookbehind:!0},operator:/^[=?!#%@$]|!(?=[:}])/,punctuation:/^\{|\}$|::?/}},italic:{pattern:/^(['_])[\s\S]+\1$/,inside:{punctuation:/^(?:''?|__?)|(?:''?|__?)$/}},bold:{pattern:/^\*[\s\S]+\*$/,inside:{punctuation:/^\*\*?|\*\*?$/}},punctuation:/^(?:``?|\+{1,3}|##?|\$\$|[~^]|\(\(\(?)|(?:''?|\+{1,3}|##?|\$\$|[~^`]|\)?\)\))$/}},replacement:{pattern:/\((?:C|R|TM)\)/,alias:"builtin"},entity:/?[\da-z]{1,8};/i,"line-continuation":{pattern:/(^| )\+$/m,lookbehind:!0,alias:"punctuation"}};function a(i){i=i.split(" ");for(var o={},s=0,l=i.length;s>/g,function(ue,h){return"(?:"+Q[+h]+")"})}function r(b,Q,ue){return RegExp(n(b,Q),"")}function a(b,Q){for(var ue=0;ue>/g,function(){return"(?:"+b+")"});return b.replace(/<>/g,"[^\\s\\S]")}var i={type:"bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",typeDeclaration:"class enum interface record struct",contextual:"add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",other:"abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield"};function o(b){return"\\b(?:"+b.trim().replace(/ /g,"|")+")\\b"}var s=o(i.typeDeclaration),l=RegExp(o(i.type+" "+i.typeDeclaration+" "+i.contextual+" "+i.other)),u=o(i.typeDeclaration+" "+i.contextual+" "+i.other),d=o(i.type+" "+i.typeDeclaration+" "+i.other),c=a(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source,2),g=a(/\((?:[^()]|<>)*\)/.source,2),p=/@?\b[A-Za-z_]\w*\b/.source,m=n(/<<0>>(?:\s*<<1>>)?/.source,[p,c]),E=n(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source,[u,m]),S=/\[\s*(?:,\s*)*\]/.source,y=n(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source,[E,S]),A=n(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source,[c,g,S]),w=n(/\(<<0>>+(?:,<<0>>+)+\)/.source,[A]),R=n(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source,[w,E,S]),v={keyword:l,punctuation:/[<>()?,.:[\]]/},T=/'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source,D=/"(?:\\.|[^\\"\r\n])*"/.source,M=/@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source;t.languages.csharp=t.languages.extend("clike",{string:[{pattern:r(/(^|[^$\\])<<0>>/.source,[M]),lookbehind:!0,greedy:!0},{pattern:r(/(^|[^@$\\])<<0>>/.source,[D]),lookbehind:!0,greedy:!0}],"class-name":[{pattern:r(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source,[E]),lookbehind:!0,inside:v},{pattern:r(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source,[p,R]),lookbehind:!0,inside:v},{pattern:r(/(\busing\s+)<<0>>(?=\s*=)/.source,[p]),lookbehind:!0},{pattern:r(/(\b<<0>>\s+)<<1>>/.source,[s,m]),lookbehind:!0,inside:v},{pattern:r(/(\bcatch\s*\(\s*)<<0>>/.source,[E]),lookbehind:!0,inside:v},{pattern:r(/(\bwhere\s+)<<0>>/.source,[p]),lookbehind:!0},{pattern:r(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source,[y]),lookbehind:!0,inside:v},{pattern:r(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source,[R,d,p]),inside:v}],keyword:l,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),t.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),t.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:r(/([(,]\s*)<<0>>(?=\s*:)/.source,[p]),lookbehind:!0,alias:"punctuation"}}),t.languages.insertBefore("csharp","class-name",{namespace:{pattern:r(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source,[p]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:r(/(\b(?:default|sizeof|typeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source,[g]),lookbehind:!0,alias:"class-name",inside:v},"return-type":{pattern:r(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source,[R,E]),inside:v,alias:"class-name"},"constructor-invocation":{pattern:r(/(\bnew\s+)<<0>>(?=\s*[[({])/.source,[R]),lookbehind:!0,inside:v,alias:"class-name"},"generic-method":{pattern:r(/<<0>>\s*<<1>>(?=\s*\()/.source,[p,c]),inside:{function:r(/^<<0>>/.source,[p]),generic:{pattern:RegExp(c),alias:"class-name",inside:v}}},"type-list":{pattern:r(/\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source,[s,m,p,R,l.source,g,/\bnew\s*\(\s*\)/.source]),lookbehind:!0,inside:{"record-arguments":{pattern:r(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source,[m,g]),lookbehind:!0,greedy:!0,inside:t.languages.csharp},keyword:l,"class-name":{pattern:RegExp(R),greedy:!0,inside:v},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var U=D+"|"+T,k=n(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source,[U]),C=a(n(/[^"'/()]|<<0>>|\(<>*\)/.source,[k]),2),O=/\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source,L=n(/<<0>>(?:\s*\(<<1>>*\))?/.source,[E,C]);t.languages.insertBefore("csharp","class-name",{attribute:{pattern:r(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source,[O,L]),lookbehind:!0,greedy:!0,inside:{target:{pattern:r(/^<<0>>(?=\s*:)/.source,[O]),alias:"keyword"},"attribute-arguments":{pattern:r(/\(<<0>>*\)/.source,[C]),inside:t.languages.csharp},"class-name":{pattern:RegExp(E),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var z=/:[^}\r\n]+/.source,P=a(n(/[^"'/()]|<<0>>|\(<>*\)/.source,[k]),2),Z=n(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[P,z]),q=a(n(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source,[U]),2),ee=n(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[q,z]);function re(b,Q){return{interpolation:{pattern:r(/((?:^|[^{])(?:\{\{)*)<<0>>/.source,[b]),lookbehind:!0,inside:{"format-string":{pattern:r(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source,[Q,z]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:t.languages.csharp}}},string:/[\s\S]+/}}t.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:r(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source,[Z]),lookbehind:!0,greedy:!0,inside:re(Z,P)},{pattern:r(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source,[ee]),lookbehind:!0,greedy:!0,inside:re(ee,q)}],char:{pattern:RegExp(T),greedy:!0}}),t.languages.dotnet=t.languages.cs=t.languages.csharp}(e)}sr.displayName="aspnet";sr.aliases=[];function sr(e){e.register(qe),e.register(ne),e.languages.aspnet=e.languages.extend("markup",{"page-directive":{pattern:/<%\s*@.*%>/,alias:"tag",inside:{"page-directive":{pattern:/<%\s*@\s*(?:Assembly|Control|Implements|Import|Master(?:Type)?|OutputCache|Page|PreviousPageType|Reference|Register)?|%>/i,alias:"tag"},rest:e.languages.markup.tag.inside}},directive:{pattern:/<%.*%>/,alias:"tag",inside:{directive:{pattern:/<%\s*?[$=%#:]{0,2}|%>/,alias:"tag"},rest:e.languages.csharp}}}),e.languages.aspnet.tag.pattern=/<(?!%)\/?[^\s>\/]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/,e.languages.insertBefore("inside","punctuation",{directive:e.languages.aspnet.directive},e.languages.aspnet.tag.inside["attr-value"]),e.languages.insertBefore("aspnet","comment",{"asp-comment":{pattern:/<%--[\s\S]*?--%>/,alias:["asp","comment"]}}),e.languages.insertBefore("aspnet",e.languages.javascript?"script":"tag",{"asp-script":{pattern:/(