From 902a3df8b55726c84e2e6693b6e5a0ae181f9506 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Wed, 24 Jun 2026 09:11:13 +0200 Subject: [PATCH] feat: add Obsidian CLI API service - Add Docker image for the official Obsidian desktop app and CLI. - Start Obsidian headlessly with Xvfb and DBus setup. - Expose a safe structured HTTP command API without shell execution. - Add JWT-based vault, path, and command authorization. - Support single-vault and multi-vault container mounts. - Add TypeScript SDK helpers for Obsidian CLI commands. - Add n8n community node package with Obsidian operations. - Add docs, compose config, tests, and production image workflow. --- .dockerignore | 13 + .env.example | 34 + .gitea/workflows/prod-docker-images.yml | 50 + .gitignore | 10 + Dockerfile | 67 + README.md | 271 ++ app/__init__.py | 0 app/auth.py | 84 + app/config.py | 37 + app/main.py | 122 + app/policy.py | 150 ++ app/runner.py | 58 + docker-compose.yml | 30 + docs/n8n.md | 133 + packages/n8n-nodes-obsidian-api/README.md | 79 + .../credentials/ObsidianApi.credentials.ts | 40 + packages/n8n-nodes-obsidian-api/index.ts | 1 + .../nodes/ObsidianApi/ObsidianApi.node.ts | 324 +++ .../nodes/ObsidianApi/obsidian.svg | 51 + .../n8n-nodes-obsidian-api/package-lock.json | 2304 +++++++++++++++++ packages/n8n-nodes-obsidian-api/package.json | 50 + .../shared/ObsidianApiClient.ts | 77 + packages/n8n-nodes-obsidian-api/tsconfig.json | 17 + pyproject.toml | 21 + scripts/create_jwt.py | 69 + scripts/entrypoint.sh | 59 + scripts/render_config.py | 83 + sdk/typescript/README.md | 59 + sdk/typescript/package-lock.json | 33 + sdk/typescript/package.json | 28 + sdk/typescript/src/index.ts | 258 ++ sdk/typescript/tsconfig.json | 17 + tests/test_api.py | 167 ++ uv.lock | 357 +++ 34 files changed, 5153 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/prod-docker-images.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/auth.py create mode 100644 app/config.py create mode 100644 app/main.py create mode 100644 app/policy.py create mode 100644 app/runner.py create mode 100644 docker-compose.yml create mode 100644 docs/n8n.md create mode 100644 packages/n8n-nodes-obsidian-api/README.md create mode 100644 packages/n8n-nodes-obsidian-api/credentials/ObsidianApi.credentials.ts create mode 100644 packages/n8n-nodes-obsidian-api/index.ts create mode 100644 packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/ObsidianApi.node.ts create mode 100644 packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/obsidian.svg create mode 100644 packages/n8n-nodes-obsidian-api/package-lock.json create mode 100644 packages/n8n-nodes-obsidian-api/package.json create mode 100644 packages/n8n-nodes-obsidian-api/shared/ObsidianApiClient.ts create mode 100644 packages/n8n-nodes-obsidian-api/tsconfig.json create mode 100644 pyproject.toml create mode 100644 scripts/create_jwt.py create mode 100644 scripts/entrypoint.sh create mode 100644 scripts/render_config.py create mode 100644 sdk/typescript/README.md create mode 100644 sdk/typescript/package-lock.json create mode 100644 sdk/typescript/package.json create mode 100644 sdk/typescript/src/index.ts create mode 100644 sdk/typescript/tsconfig.json create mode 100644 tests/test_api.py create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..70afb1a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.venv +__pycache__ +.pytest_cache +.ruff_cache +.env +vault +*.pyc +node_modules +sdk/typescript/node_modules +sdk/typescript/dist +packages/*/node_modules +packages/*/dist diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9b90020 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Copy to .env and adjust. + +# JWT signing secret. Use the same value to create JWTs with scripts/create_jwt.py. +OBSIDIAN_JWT_SECRET=change-this-long-random-secret + +# Host path to your default/single Obsidian vault. +# Examples: +# OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/MainVault +# OBSIDIAN_VAULT_PATH=/mnt/storage/ObsidianVault +OBSIDIAN_VAULT_PATH=./vault + +# Optional: mount a parent folder containing additional vaults at /vaults. +# Then register all vaults by name using container paths. +# Example: +# OBSIDIAN_VAULTS_ROOT=/home/daniel/Documents/Obsidian +# OBSIDIAN_VAULTS=main=/vault,work=/vaults/Work,personal=/vaults/Personal +OBSIDIAN_VAULTS_ROOT=./vaults +OBSIDIAN_VAULTS= + +# Host port for the HTTP API. +OBSIDIAN_API_PORT=8080 + +# Official Obsidian desktop/CLI version installed in the image. +OBSIDIAN_VERSION=1.12.7 + +# Official CLI binary inside the container. +OBSIDIAN_CLI_BIN=obsidian + +# Seconds to wait for the desktop app to expose the official CLI socket. +OBSIDIAN_STARTUP_TIMEOUT=60 + +# Command timeout limits in seconds. +COMMAND_TIMEOUT=60 +MAX_COMMAND_TIMEOUT=300 diff --git a/.gitea/workflows/prod-docker-images.yml b/.gitea/workflows/prod-docker-images.yml new file mode 100644 index 0000000..2a62052 --- /dev/null +++ b/.gitea/workflows/prod-docker-images.yml @@ -0,0 +1,50 @@ +name: Build and Push Docker Container + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + +env: + IMAGE_NAME: obsidian-api + OBSIDIAN_VERSION: "1.12.7" + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Rewrite SSH submodule URLs to HTTPS for CI + run: | + git config --global url."https://x-token:${{ secrets.ACTION_ACCESS_TOKEN }}@git.yiprawr.dev/".insteadOf "git@git.yiprawr.dev:" + + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: "${{ secrets.ACTION_ACCESS_TOKEN }}" + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to Docker registry + uses: docker/login-action@v4 + with: + registry: ${{ vars.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.ACTION_ACCESS_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + build-args: | + OBSIDIAN_VERSION=${{ env.OBSIDIAN_VERSION }} + tags: | + ${{ vars.DOCKER_REGISTRY_URL }}/daniel156161/${{ env.IMAGE_NAME }}:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..110f818 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +obsidian_api.egg-info/ +.venv/ +__pycache__/ +.pytest_cache/ +vault/ +.env +node_modules/ +packages/*/node_modules/ +packages/*/dist/ +sdk/typescript/dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b34876a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim + +ARG OBSIDIAN_VERSION=1.12.7 +ARG TARGETARCH + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + PATH="/app/.venv/bin:/usr/local/bin:${PATH}" \ + OBSIDIAN_CLI_BIN=obsidian \ + OBSIDIAN_VAULT_PATH=/vault \ + OBSIDIAN_API_HOST=0.0.0.0 \ + OBSIDIAN_API_PORT=8080 \ + DISPLAY=:99 \ + XDG_RUNTIME_DIR=/tmp/runtime-obsidian \ + ELECTRON_DISABLE_SECURITY_WARNINGS=true + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + dumb-init \ + xvfb \ + xauth \ + dbus-x11 \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libatspi2.0-0 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libdrm2 \ + libgbm1 \ + libgtk-3-0 \ + libnss3 \ + libpango-1.0-0 \ + libx11-xcb1 \ + libxcb-dri3-0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxkbcommon0 \ + libxrandr2 \ + && case "${TARGETARCH:-amd64}" in \ + amd64) OBSIDIAN_DEB_ARCH=amd64 ;; \ + arm64) OBSIDIAN_DEB_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && curl -fsSL -o /tmp/obsidian.deb "https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/obsidian_${OBSIDIAN_VERSION}_${OBSIDIAN_DEB_ARCH}.deb" \ + && apt-get install -y --no-install-recommends /tmp/obsidian.deb \ + && ln -sf /opt/Obsidian/obsidian-cli /usr/local/bin/obsidian \ + && rm -rf /tmp/obsidian.deb /var/lib/apt/lists/* + +WORKDIR /app +COPY pyproject.toml ./ +RUN uv sync --no-dev + +COPY app ./app +COPY sdk ./sdk +COPY scripts ./scripts +RUN chmod +x /app/scripts/entrypoint.sh /app/scripts/create_jwt.py + +EXPOSE 8080 +ENTRYPOINT ["dumb-init", "--"] +CMD ["/app/scripts/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..81a27bf --- /dev/null +++ b/README.md @@ -0,0 +1,271 @@ +# Obsidian API +Docker service that exposes an HTTP API for the **official Obsidian CLI**. + +It is meant for: +- n8n custom nodes, HTTP Request nodes, and Code nodes +- Prism / TypeScript agent harnesses +- local automation against a mounted Obsidian vault + +## Important: official CLI architecture +This uses the real Obsidian CLI from `obsidian.md/help/cli`. + +That CLI is not an npm package. It ships inside the Obsidian desktop app and requires the app to be running. Therefore this container runs: +- official Obsidian desktop app +- Xvfb virtual display +- official `/opt/Obsidian/obsidian-cli` exposed as `obsidian` +- this Starlette HTTP API + +Because Electron is required, the image is Debian-based and larger than Alpine. + +## Security model +The HTTP API does **not** accept shell commands and does **not** accept plain command strings. + +Allowed: +```json +{ + "args": ["read", "path=Inbox/Note.md"] +} +``` + +Rejected: +```json +{ + "command": "read path=Inbox/Note.md" +} +``` + +```json +{ + "mode": "shell", + "args": ["ls -la"] +} +``` + +The service always runs the configured binary directly with `exec` semantics: + +```bash +obsidian +``` + +No shell is involved. + +### JWT access policies +Auth uses signed HS256 JWT bearer tokens. The token itself contains the policy claims. + +Create a signing secret: +```bash +export OBSIDIAN_JWT_SECRET="$(openssl rand -base64 48)" +``` + +Generate a token with the same secret: +```bash +python scripts/create_jwt.py \ + --sub n8n \ + --vault work \ + --path Inbox/n8n/ \ + --path Automation.md \ + --command create \ + --command read \ + --command append \ + --command search \ + --command search:context \ + --days 365 +``` + +Set the same secret for the API: +```env +OBSIDIAN_JWT_SECRET=change-this-long-random-secret +``` + +JWT claims: +```json +{ + "sub": "n8n", + "vaults": ["work"], + "paths": ["Inbox/n8n/", "Automation.md"], + "commands": ["create", "read", "append", "search", "search:context"], + "iat": 1760000000, + "exp": 1791536000 +} +``` + +Policy claims: +- `vaults`: allowed Obsidian vault names, e.g. `["work"]` +- `paths`: allowed note/path prefixes, e.g. `["Inbox/n8n/", "Automation.md"]` +- `commands`: allowed official CLI commands, e.g. `["read", "create", "append"]` +- `exp`: token expiry timestamp +- `nbf`: optional not-before timestamp +- `iat`: optional issued-at timestamp + +Use `["*"]` for unrestricted access in a dimension. + +Example request: +```bash +JWT="$(python scripts/create_jwt.py --sub n8n --vault work --path Inbox/n8n/ --command create --days 365)" + +curl -X POST http://localhost:8080/commands \ + -H "Authorization: Bearer $JWT" \ + -H "Content-Type: application/json" \ + -d '{"args":["vault=work","create","path=Inbox/n8n/Event.md","content=Hello","overwrite"]}' +``` + +Path restrictions are enforced before the Obsidian CLI process starts. If a token has path restrictions, commands that read/write notes must include an explicit `path=`, `file=`, `folder=`, `name=` or `to=` argument. + +## Start +```bash +cp .env.example .env +``` + +Edit: +```env +OBSIDIAN_JWT_SECRET=change-this-long-random-secret +OBSIDIAN_VAULT_PATH=/absolute/path/to/your/vault +OBSIDIAN_API_PORT=8080 +``` + +### Multiple vaults +Single vault is the default: +```env +OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main +``` + +For more vaults, mount a parent folder and register container paths: +```env +OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main +OBSIDIAN_VAULTS_ROOT=/home/daniel/Documents/Obsidian +OBSIDIAN_VAULTS=main=/vault,work=/vaults/Work,personal=/vaults/Personal +``` + +Then target a vault with the official CLI prefix: +```json +{ + "args": ["vault=work", "read", "path=Inbox/Note.md"] +} +``` + +If no `vault=` prefix is provided, the first vault in `OBSIDIAN_VAULTS` is used as the default. + +Start: +```bash +docker compose up -d --build +``` + +Logs: +```bash +docker compose logs -f +``` + +## API + +### Health + +```bash +curl http://localhost:8080/health +``` + +### Run official Obsidian CLI command + +```bash +curl -X POST http://localhost:8080/commands \ + -H "Authorization: Bearer change-me" \ + -H "Content-Type: application/json" \ + -d '{"args":["version"]}' +``` + +Create a note: +```bash +curl -X POST http://localhost:8080/commands \ + -H "Authorization: Bearer change-me" \ + -H "Content-Type: application/json" \ + -d '{"args":["create","path=Inbox/Hello.md","content=# Hello","overwrite"]}' +``` + +Read a note: + +```bash +curl -X POST http://localhost:8080/commands \ + -H "Authorization: Bearer change-me" \ + -H "Content-Type: application/json" \ + -d '{"args":["read","path=Inbox/Hello.md"]}' +``` + +Search: +```bash +curl -X POST http://localhost:8080/commands \ + -H "Authorization: Bearer change-me" \ + -H "Content-Type: application/json" \ + -d '{"args":["search","query=Hello","format=json"]}' +``` + +## TypeScript SDK + +SDK path: +```text +sdk/typescript +``` + +Install locally: +```bash +npm install ./sdk/typescript +``` + +Use in TypeScript / Node / Prism: +```ts +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: 'http://localhost:8080', + token: 'change-me', +}); + +await obsidian.create({ + path: 'Inbox/Hello.md', + content: '# Hello from TypeScript', + overwrite: true, +}); + +const note = await obsidian.read({ path: 'Inbox/Hello.md' }); +console.log(note.stdout); + +const results = await obsidian.search('Hello', { format: 'json' }); +console.log(JSON.parse(results.stdout)); + +// Multi-vault: scoped client +const work = obsidian.inVault('work'); +await work.create({ path: 'Inbox/Work.md', content: 'Hello work', overwrite: true }); +``` + +n8n options: +- real community-node package: [`packages/n8n-nodes-obsidian-api`](packages/n8n-nodes-obsidian-api) +- Code node / HTTP examples: [`docs/n8n.md`](docs/n8n.md) + +Raw escape hatch, still safe because it sends an args array: +```ts +await obsidian.runRaw('daily:append', 'content=- [ ] Buy groceries'); +``` + +## Official CLI command examples +The SDK covers the official command groups from `obsidian.md/help/cli`, including: +- General: `help`, `version`, `reload`, `restart` +- Files: `create`, `read`, `append`, `prepend`, `move`, `rename`, `delete`, `files`, `folders` +- Daily notes: `daily`, `daily:path`, `daily:read`, `daily:append`, `daily:prepend` +- Search: `search`, `search:context`, `search:open` +- Links: `backlinks`, `links`, `unresolved`, `orphans`, `deadends` +- Tags/tasks/properties/templates +- Plugins/themes/snippets +- Sync/Publish commands +- Workspace/tabs/recents +- Developer commands: `dev:*`, `eval` + +## Development +Python API tests: +```bash +uv run pytest -q +``` + +TypeScript SDK typecheck: +```bash +cd sdk/typescript +npm install +npm run typecheck +``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..01ae2d4 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass +import hashlib, base64, hmac +from typing import Any +import json, time + +from starlette.requests import Request +from starlette.responses import JSONResponse + +from app.config import JWT_SECRET +from app.policy import TokenPolicy, tuple_claim + +@dataclass(frozen=True) +class AuthContext: + policy:TokenPolicy|None + +def error(status_code:int, detail:str) -> JSONResponse: + return JSONResponse({"detail": detail}, status_code=status_code) + +def b64url_decode(value:str) -> bytes: + padding = "=" * (-len(value) % 4) + return base64.urlsafe_b64decode((value + padding).encode()) + +def decode_hs256_jwt(token:str, secret:bytes) -> dict[str, Any]: + parts = token.split(".") + if len(parts) != 3: + raise ValueError("JWT must have three parts") + + header_b64, payload_b64, signature_b64 = parts + try: + header = json.loads(b64url_decode(header_b64)) + payload = json.loads(b64url_decode(payload_b64)) + except (json.JSONDecodeError, ValueError) as exc: + raise ValueError("JWT header or payload is invalid") from exc + + if not isinstance(header, dict) or not isinstance(payload, dict): + raise ValueError("JWT header and payload must be JSON objects") + if header.get("alg") != "HS256": + raise ValueError("only HS256 JWTs are supported") + if header.get("typ") not in (None, "JWT"): + raise ValueError("JWT typ must be JWT") + + signing_input = f"{header_b64}.{payload_b64}".encode() + expected = hmac.new(secret, signing_input, hashlib.sha256).digest() + supplied = b64url_decode(signature_b64) + if not hmac.compare_digest(supplied, expected): + raise ValueError("JWT signature is invalid") + + now = int(time.time()) + exp = payload.get("exp") + if exp is not None and (not isinstance(exp, int | float) or now >= int(exp)): + raise ValueError("JWT has expired") + nbf = payload.get("nbf") + if nbf is not None and (not isinstance(nbf, int | float) or now < int(nbf)): + raise ValueError("JWT is not active yet") + iat = payload.get("iat") + if iat is not None and (not isinstance(iat, int | float) or int(iat) > now + 300): + raise ValueError("JWT iat is in the future") + + return payload + +def policy_from_jwt_claims(token:str, claims:dict[str, Any]) -> TokenPolicy: + name = str(claims.get("name") or claims.get("sub") or "jwt") + return TokenPolicy( + token=token, + name=name, + vaults=tuple_claim(claims.get("vaults"), ("*",)), + paths=tuple_claim(claims.get("paths"), ("*",)), + commands=tuple_claim(claims.get("commands"), ("*",)), + ) + +def require_auth(request:Request) -> AuthContext|JSONResponse: + if not JWT_SECRET: + return error(500, "OBSIDIAN_JWT_SECRET is required") + + header = request.headers.get("authorization", "") + if not header.startswith("Bearer "): + return error(401, "Unauthorized") + + supplied = header.removeprefix("Bearer ") + try: + claims = decode_hs256_jwt(supplied, JWT_SECRET.encode()) + return AuthContext(policy=policy_from_jwt_claims(supplied, claims)) + except ValueError: + return error(401, "Unauthorized") diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..8318df4 --- /dev/null +++ b/app/config.py @@ -0,0 +1,37 @@ +from pathlib import Path +import json, os + +JWT_SECRET = os.getenv("OBSIDIAN_JWT_SECRET", "") +DEFAULT_TIMEOUT = float(os.getenv("COMMAND_TIMEOUT", "60")) +MAX_TIMEOUT = float(os.getenv("MAX_COMMAND_TIMEOUT", "300")) +CLI_BIN = os.getenv("OBSIDIAN_CLI_BIN", "obsidian") +VAULT_PATH = Path(os.getenv("OBSIDIAN_VAULT_PATH", "/vault")).resolve() +HOST = os.getenv("OBSIDIAN_API_HOST", "0.0.0.0") +PORT = int(os.getenv("OBSIDIAN_API_PORT", "8080")) + +def parse_registered_vaults() -> dict[str, str]: + raw = os.getenv("OBSIDIAN_VAULTS", "").strip() + vaults:dict[str, str] = {} + + if raw: + try: + parsed = json.loads(raw) + if isinstance(parsed, dict): + return {str(name): str(path) for name, path in parsed.items()} + except json.JSONDecodeError: + pass + + for chunk in raw.replace("\n", ",").split(","): + chunk = chunk.strip() + if not chunk or "=" not in chunk: + continue + name, path = chunk.split("=", 1) + vaults[name.strip()] = path.strip() + + if not vaults: + vaults[VAULT_PATH.name or "vault"] = str(VAULT_PATH) + + return vaults + +REGISTERED_VAULTS = parse_registered_vaults() +DEFAULT_VAULT_NAME = next(iter(REGISTERED_VAULTS), VAULT_PATH.name or "vault") diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0c6ba3c --- /dev/null +++ b/app/main.py @@ -0,0 +1,122 @@ +from pathlib import Path +from typing import Any +import json + +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Route + +from app import auth +from app.auth import require_auth +from app.config import CLI_BIN, DEFAULT_TIMEOUT, HOST, MAX_TIMEOUT, PORT, VAULT_PATH +from app.policy import authorize_command, vault_for_path +from app.runner import execute_command + +def error(status_code:int, detail:str) -> JSONResponse: + return JSONResponse({"detail": detail}, status_code=status_code) + +def parse_args(value:Any) -> list[str]: + if value is None: + return [] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError("args must be a list of strings") + return value + +def parse_timeout(value:Any) -> float: + if value is None: + return DEFAULT_TIMEOUT + if not isinstance(value, int | float) or value <= 0: + raise ValueError("timeout must be a positive number") + return min(float(value), MAX_TIMEOUT) + +def safe_cwd(cwd:str|None) -> Path: + path = Path(cwd).resolve() if cwd else VAULT_PATH + if not path.exists() or not path.is_dir(): + raise ValueError(f"cwd does not exist or is not a directory: {path}") + if vault_for_path(path) is None: + raise ValueError(f"cwd must be inside a registered vault: {path}") + return path + +async def health(request:Request) -> JSONResponse: + return JSONResponse( + { + "ok": True, + "vault": str(VAULT_PATH), + "cli": CLI_BIN, + "auth": bool(auth.JWT_SECRET), + "jwt": bool(auth.JWT_SECRET), + } + ) + +async def run_command(request:Request) -> JSONResponse: + auth_context = require_auth(request) + if isinstance(auth_context, JSONResponse): + return auth_context + + try: + payload = await request.json() + except json.JSONDecodeError: + return error(400, "invalid JSON body") + + try: + if "command" in payload: + raise ValueError("command strings are not allowed; use args as a list of strings") + if "mode" in payload: + raise ValueError("mode is not allowed; only the configured Obsidian CLI can be executed") + + args = parse_args(payload.get("args")) + stdin = payload.get("stdin") + if stdin is not None and not isinstance(stdin, str): + raise ValueError("stdin must be a string") + + cwd_value = payload.get("cwd") + if cwd_value is not None and not isinstance(cwd_value, str): + raise ValueError("cwd must be a string") + + cwd = safe_cwd(cwd_value) + timeout = parse_timeout(payload.get("timeout")) + authorize_command(auth_context.policy, args, cwd) + except ValueError as exc: + return error(400, str(exc)) + except PermissionError as exc: + return error(403, str(exc)) + + return await execute_command(args=args, cwd=cwd, stdin=stdin, timeout=timeout) + +async def typescript_sdk_example(request:Request) -> JSONResponse: + return JSONResponse( + { + "example": """ +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: 'http://localhost:8080', + token: '', +}); + +await obsidian.create({ + path: 'Inbox/Hello.md', + content: '# Hello from n8n/Prism', + overwrite: true, +}); + +const note = await obsidian.read({ path: 'Inbox/Hello.md' }); +console.log(note.stdout); +""".strip() + } + ) + +app = Starlette( + debug=False, + routes=[ + Route("/health", health, methods=["GET"]), + Route("/commands", run_command, methods=["POST"]), + Route("/sdk/typescript", typescript_sdk_example, methods=["GET"]), + ], +) + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("app.main:app", host=HOST, port=PORT) diff --git a/app/policy.py b/app/policy.py new file mode 100644 index 0000000..0ae1615 --- /dev/null +++ b/app/policy.py @@ -0,0 +1,150 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from app.config import DEFAULT_VAULT_NAME, REGISTERED_VAULTS + +@dataclass(frozen=True) +class TokenPolicy: + token:str = "" + name:str = "jwt" + vaults:tuple[str, ...] = ("*",) + paths:tuple[str, ...] = ("*",) + commands:tuple[str, ...] = ("*",) + + def allows_all_vaults(self) -> bool: + return "*" in self.vaults + + def allows_all_paths(self) -> bool: + return "*" in self.paths + + def allows_all_commands(self) -> bool: + return "*" in self.commands + +@dataclass(frozen=True) +class CommandTarget: + command:str|None + vault:str|None + paths:tuple[str, ...] + +PATH_KEYS = {"path", "file", "folder", "name", "to"} +PATH_REQUIRED_WHEN_RESTRICTED = { + "append", "prepend", "read", "create", "delete", "rename", "move", "file", "folder", "open", "diff", + "search", "search:context", "files", "folders", + "property:set", "property:remove", "property:read", "properties", + "aliases", "backlinks", "links", "outlinks", "tags", "tasks", "task", +} +PATHLESS_ALLOWED_WHEN_RESTRICTED = {"help", "version"} + +def tuple_claim(value:Any, default:tuple[str, ...]) -> tuple[str, ...]: + if value is None: + return default + if isinstance(value, str): + return (value,) + if isinstance(value, list) and all(isinstance(item, str) for item in value): + return tuple(value) + raise ValueError("JWT claims vaults, paths and commands must be strings or string arrays") + +def vault_for_path(path:Path) -> str|None: + resolved = path.resolve() + best_name:str|None = None + best_len = -1 + + for name, vault_path in REGISTERED_VAULTS.items(): + root = Path(vault_path).resolve() + try: + resolved.relative_to(root) + except ValueError: + continue + + root_len = len(str(root)) + if root_len > best_len: + best_name = name + best_len = root_len + + return best_name + +def normalize_policy_path(value:str) -> str: + normalized = value.strip().replace("\\", "/").lstrip("/") + while "//" in normalized: + normalized = normalized.replace("//", "/") + return normalized + +def is_path_allowed(path:str, allowed_patterns:tuple[str, ...]) -> bool: + normalized = normalize_policy_path(path) + + for pattern in allowed_patterns: + pattern_normalized = normalize_policy_path(pattern) + if pattern_normalized == "*": + return True + if pattern_normalized.endswith("/**"): + prefix = pattern_normalized[:-3].rstrip("/") + if normalized == prefix or normalized.startswith(prefix + "/"): + return True + elif pattern_normalized.endswith("/"): + if normalized.startswith(pattern_normalized): + return True + elif normalized == pattern_normalized or normalized.startswith(pattern_normalized.rstrip("/") + "/"): + return True + + return False + +def parse_kv_arg(arg:str) -> tuple[str, str]|None: + if "=" not in arg: + return None + key, value = arg.split("=", 1) + if not key: + return None + return key, value + +def extract_command_target(args:list[str]) -> CommandTarget: + vault:str|None = None + command:str|None = None + paths:list[str] = [] + + for arg in args: + parsed = parse_kv_arg(arg) + if parsed and parsed[0] == "vault" and command is None: + vault = parsed[1] + continue + command = arg + break + + for arg in args: + parsed = parse_kv_arg(arg) + if not parsed: + continue + key, value = parsed + if key in PATH_KEYS and value: + paths.append(value) + + return CommandTarget(command=command, vault=vault, paths=tuple(paths)) + +def authorize_command(policy:TokenPolicy|None, args:list[str], cwd:Path) -> None: + if policy is None: + return + + target = extract_command_target(args) + command = target.command or "" + + if not policy.allows_all_commands() and command not in policy.commands: + raise PermissionError(f"token is not allowed to run command: {command}") + + if not policy.allows_all_vaults(): + vault = target.vault or vault_for_path(cwd) or DEFAULT_VAULT_NAME + if vault not in policy.vaults: + raise PermissionError(f"token is not allowed to access vault: {vault}") + + if policy.allows_all_paths(): + return + + if not target.paths: + if command in PATHLESS_ALLOWED_WHEN_RESTRICTED: + return + if command in PATH_REQUIRED_WHEN_RESTRICTED: + raise PermissionError("token has path restrictions; command must include an allowed path/file/folder/name argument") + raise PermissionError(f"token has path restrictions; command is not allowed without an explicit path: {command}") + + denied = [path for path in target.paths if not is_path_allowed(path, policy.paths)] + if denied: + raise PermissionError(f"token is not allowed to access path: {denied[0]}") diff --git a/app/runner.py b/app/runner.py new file mode 100644 index 0000000..55650c8 --- /dev/null +++ b/app/runner.py @@ -0,0 +1,58 @@ +import asyncio, uuid, time +from pathlib import Path + +from starlette.responses import JSONResponse + +from app.config import CLI_BIN + +def error(status_code:int, detail:str) -> JSONResponse: + return JSONResponse({"detail": detail}, status_code=status_code) + +async def execute_command(args:list[str], cwd:Path, stdin:str|None, timeout:float) -> JSONResponse: + command_id = str(uuid.uuid4()) + command = [CLI_BIN, *args] + started = time.perf_counter() + + try: + process = await asyncio.create_subprocess_exec( + *command, + cwd=str(cwd), + stdin=asyncio.subprocess.PIPE if stdin is not None else None, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + except FileNotFoundError: + return error(500, f"configured CLI binary was not found: {CLI_BIN}") + + try: + stdout_b, stderr_b = await asyncio.wait_for( + process.communicate(None if stdin is None else stdin.encode()), + timeout=timeout, + ) + timed_out = False + except asyncio.TimeoutError: + process.kill() + stdout_b, stderr_b = await process.communicate() + timed_out = True + + duration_ms = int((time.perf_counter() - started) * 1000) + stdout = stdout_b.decode(errors="replace") + stderr = stderr_b.decode(errors="replace") + exit_code = process.returncode if process.returncode is not None else -1 + + if timed_out: + exit_code = 124 + stderr = (stderr + f"\nCommand timed out after {timeout}s").strip() + + return JSONResponse( + { + "id": command_id, + "command": command, + "cwd": str(cwd), + "exit_code": exit_code, + "stdout": stdout, + "stderr": stderr, + "duration_ms": duration_ms, + "timed_out": timed_out, + } + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d8acdb7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + obsidian-api: + build: + context: . + args: + OBSIDIAN_VERSION: ${OBSIDIAN_VERSION:-1.12.7} + image: obsidian-api:local + container_name: obsidian-api + restart: unless-stopped + environment: + # JWT signing secret. Bearer tokens are HS256 JWTs with vault/path/command claims. + OBSIDIAN_JWT_SECRET: ${OBSIDIAN_JWT_SECRET:-} + OBSIDIAN_CLI_BIN: ${OBSIDIAN_CLI_BIN:-obsidian} + # Single-vault fallback. For multiple vaults, set OBSIDIAN_VAULTS below. + OBSIDIAN_VAULT_PATH: /vault + # Multi-vault syntax: name=/container/path,name2=/container/path2 + # Example with the optional /vaults mount below: main=/vault,work=/vaults/work,personal=/vaults/personal + OBSIDIAN_VAULTS: ${OBSIDIAN_VAULTS:-} + OBSIDIAN_STARTUP_TIMEOUT: ${OBSIDIAN_STARTUP_TIMEOUT:-60} + COMMAND_TIMEOUT: ${COMMAND_TIMEOUT:-60} + MAX_COMMAND_TIMEOUT: ${MAX_COMMAND_TIMEOUT:-300} + volumes: + # Default/single vault mount. + - ${OBSIDIAN_VAULT_PATH:-./vault}:/vault + # Optional parent-folder mount for multiple vaults. Example: + # OBSIDIAN_VAULTS_ROOT=/home/daniel/Documents/Obsidian + # OBSIDIAN_VAULTS=main=/vault,work=/vaults/Work,personal=/vaults/Personal + - ${OBSIDIAN_VAULTS_ROOT:-./vaults}:/vaults + ports: + - "${OBSIDIAN_API_PORT:-8080}:8080" diff --git a/docs/n8n.md b/docs/n8n.md new file mode 100644 index 0000000..6978601 --- /dev/null +++ b/docs/n8n.md @@ -0,0 +1,133 @@ +# n8n usage notes +Use the TypeScript SDK from an n8n **Code** node when n8n can import local or bundled npm packages. In self-hosted n8n this usually means installing the SDK where n8n can resolve it and allowing external modules, for example with `NODE_FUNCTION_ALLOW_EXTERNAL=@obsidian-api/sdk` or `NODE_FUNCTION_ALLOW_EXTERNAL=*`. + +The SDK only sends structured `args` arrays to the HTTP API. It never sends shell command strings. + +## Environment variables +Recommended n8n env vars: + +```env +OBSIDIAN_API_URL=http://obsidian-api:8080 +OBSIDIAN_API_JWT= +OBSIDIAN_VAULT=work +``` + +Recommended: use a restricted JWT for n8n. Create it with: +```bash +OBSIDIAN_JWT_SECRET=change-this-long-random-secret \ +python scripts/create_jwt.py \ + --sub n8n \ + --vault work \ + --path Inbox/n8n/ \ + --path Automation.md \ + --command create \ + --command read \ + --command append \ + --command search \ + --command search:context \ + --days 365 +``` + +The JWT contains claims like: +```json +{ + "sub": "n8n", + "vaults": ["work"], + "paths": ["Inbox/n8n/", "Automation.md"], + "commands": ["create", "read", "append", "search", "search:context"], + "exp": 1791536000 +} +``` + +If n8n runs outside the Docker network, use the exposed host URL instead: + +```env +OBSIDIAN_API_URL=http://localhost:8080 +``` + +## Basic Code node +```ts +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: process.env.OBSIDIAN_API_URL!, + token: process.env.OBSIDIAN_API_JWT!, + vault: process.env.OBSIDIAN_VAULT, +}); + +const title = $json.title ?? 'n8n note'; +const content = `# ${title}\n\nCreated from n8n.\n`; + +const result = await obsidian.create({ + path: `Inbox/${title}.md`, + content, + overwrite: true, +}); + +return [{ json: result }]; +``` + +## Append incoming webhook payload to a daily note +```ts +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: process.env.OBSIDIAN_API_URL!, + token: process.env.OBSIDIAN_API_JWT!, + vault: process.env.OBSIDIAN_VAULT, +}); + +const payload = JSON.stringify($json, null, 2).replace(/```/g, '``\\`'); + +const result = await obsidian.dailyAppend( + `\n## n8n event\n\n\`\`\`json\n${payload}\n\`\`\`\n`, +); + +return [{ json: result }]; +``` + +## Search notes +```ts +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: process.env.OBSIDIAN_API_URL!, + token: process.env.OBSIDIAN_API_JWT!, + vault: process.env.OBSIDIAN_VAULT, +}); + +const result = await obsidian.search($json.query, { format: 'json' }); +return [{ json: { matches: JSON.parse(result.stdout) } }]; +``` + +## Multi-vault usage +If the API container registers multiple vaults, use a scoped client per vault: +```ts +const base = new ObsidianCliClient({ + baseUrl: process.env.OBSIDIAN_API_URL!, + token: process.env.OBSIDIAN_API_JWT!, +}); + +const work = base.inVault('work'); +const personal = base.inVault('personal'); + +await work.create({ path: 'Inbox/Work.md', content: 'Work note', overwrite: true }); +await personal.create({ path: 'Inbox/Personal.md', content: 'Personal note', overwrite: true }); +``` + +## Without SDK +If package imports are not available in your n8n environment, call the HTTP API directly: +```ts +const response = await fetch(`${process.env.OBSIDIAN_API_URL}/commands`, { + method: 'POST', + headers: { + authorization: `Bearer ${process.env.OBSIDIAN_API_JWT}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + args: ['vault=work', 'create', 'path=Inbox/n8n.md', 'content=Hello from n8n', 'overwrite'], + }), +}); + +return [{ json: await response.json() }]; +``` diff --git a/packages/n8n-nodes-obsidian-api/README.md b/packages/n8n-nodes-obsidian-api/README.md new file mode 100644 index 0000000..7061856 --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/README.md @@ -0,0 +1,79 @@ +# n8n-nodes-obsidian-api +n8n community-node package for the Obsidian API Docker service. + +It talks to the HTTP API, which then runs the **official Obsidian CLI** against mounted vaults. The node sends structured `args` arrays only. It does not send shell commands. + +## Nodes + +### Obsidian API + +Resources: +- **Note** + - Create + - Read + - Append + - Prepend + - Delete + - Rename + - List Files +- **Daily Note** + - Append + - Prepend + - Read + - Path +- **Search** + - Search + - Search With Context +- **Vault** + - Info + - List Vaults +- **Raw Command** + - Safe args-array escape hatch for any official CLI command + +## Credentials +Credential type: **Obsidian API** + +Fields: +- **Base URL**: e.g. `http://obsidian-api:8080` +- **API Token**: JWT created with `scripts/create_jwt.py` +- **Default Vault**: optional, e.g. `work` + +## Multi-vault +Set the `Vault` field on a node to override the credential default vault. + +The node sends this as the official CLI prefix: +```text +vault=work +``` + +Raw command example: +```json +["vault=work", "read", "path=Inbox/Note.md"] +``` + +## Local install in self-hosted n8n +From this repo: +```bash +cd packages/n8n-nodes-obsidian-api +npm install --ignore-scripts +npm run build +npm pack +``` + +Then install the generated `.tgz` in your n8n custom extensions setup. + +For Docker-based n8n, one common setup is mounting this package into n8n's custom extensions directory and setting: +```env +N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom +``` + +Exact installation depends on your n8n deployment. + +## Development +```bash +npm install --ignore-scripts +npm run typecheck +npm run build +``` + +`--ignore-scripts` avoids local native builds from n8n transitive dependencies on unsupported bleeding-edge Node versions. Use the Node version recommended by your n8n release for production builds. diff --git a/packages/n8n-nodes-obsidian-api/credentials/ObsidianApi.credentials.ts b/packages/n8n-nodes-obsidian-api/credentials/ObsidianApi.credentials.ts new file mode 100644 index 0000000..5a0408e --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/credentials/ObsidianApi.credentials.ts @@ -0,0 +1,40 @@ +import type { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class ObsidianApi implements ICredentialType { + name = 'obsidianApi'; + displayName = 'Obsidian API'; + documentationUrl = 'https://git.yiprawr.dev/Docker/obsidian-api'; + properties: INodeProperties[] = [ + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + default: 'http://obsidian-api:8080', + placeholder: 'http://obsidian-api:8080', + description: 'Base URL of the Obsidian API Docker service', + required: true, + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + description: 'Bearer JWT created with scripts/create_jwt.py', + required: true, + }, + { + displayName: 'Default Vault', + name: 'defaultVault', + type: 'string', + default: '', + placeholder: 'work', + description: 'Optional default vault name/path. Sent as vault= before every command unless the node overrides it.', + }, + ]; +} diff --git a/packages/n8n-nodes-obsidian-api/index.ts b/packages/n8n-nodes-obsidian-api/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/ObsidianApi.node.ts b/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/ObsidianApi.node.ts new file mode 100644 index 0000000..9e40417 --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/ObsidianApi.node.ts @@ -0,0 +1,324 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { compactArgs, executeObsidianCommand, kv } from '../../shared/ObsidianApiClient'; + +export class ObsidianApi implements INodeType { + description: INodeTypeDescription = { + displayName: 'Obsidian API', + name: 'obsidianApi', + icon: 'file:obsidian.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Run official Obsidian CLI commands through the Obsidian API Docker service', + defaults: { + name: 'Obsidian API', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'obsidianApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Vault', + name: 'vault', + type: 'string', + default: '', + placeholder: 'work', + description: 'Optional vault name/path. Overrides the credential default vault. Sent as vault=.', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { name: 'Daily Note', value: 'daily' }, + { name: 'Note', value: 'note' }, + { name: 'Raw Command', value: 'raw' }, + { name: 'Search', value: 'search' }, + { name: 'Vault', value: 'vault' }, + ], + default: 'note', + }, + + // Note operations + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['note'] } }, + options: [ + { name: 'Append', value: 'append' }, + { name: 'Create', value: 'create' }, + { name: 'Delete', value: 'delete' }, + { name: 'List Files', value: 'files' }, + { name: 'Prepend', value: 'prepend' }, + { name: 'Read', value: 'read' }, + { name: 'Rename', value: 'rename' }, + ], + default: 'create', + }, + { + displayName: 'Path', + name: 'path', + type: 'string', + displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'delete', 'prepend', 'read', 'rename'] } }, + default: '', + placeholder: 'Inbox/Note.md', + required: true, + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { rows: 8 }, + displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'prepend'] } }, + default: '', + required: true, + }, + { + displayName: 'Overwrite', + name: 'overwrite', + type: 'boolean', + displayOptions: { show: { resource: ['note'], operation: ['create'] } }, + default: false, + }, + { + displayName: 'New Name', + name: 'newName', + type: 'string', + displayOptions: { show: { resource: ['note'], operation: ['rename'] } }, + default: '', + required: true, + }, + { + displayName: 'Folder', + name: 'folder', + type: 'string', + displayOptions: { show: { resource: ['note'], operation: ['files'] } }, + default: '', + placeholder: 'Inbox', + }, + + // Daily note operations + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['daily'] } }, + options: [ + { name: 'Append', value: 'dailyAppend' }, + { name: 'Path', value: 'dailyPath' }, + { name: 'Prepend', value: 'dailyPrepend' }, + { name: 'Read', value: 'dailyRead' }, + ], + default: 'dailyAppend', + }, + { + displayName: 'Content', + name: 'dailyContent', + type: 'string', + typeOptions: { rows: 8 }, + displayOptions: { show: { resource: ['daily'], operation: ['dailyAppend', 'dailyPrepend'] } }, + default: '', + required: true, + }, + + // Search operations + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['search'] } }, + options: [ + { name: 'Search', value: 'search' }, + { name: 'Search With Context', value: 'searchContext' }, + ], + default: 'search', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { show: { resource: ['search'] } }, + default: '', + required: true, + }, + { + displayName: 'Format', + name: 'format', + type: 'options', + displayOptions: { show: { resource: ['search'] } }, + options: [ + { name: 'Text', value: 'text' }, + { name: 'JSON', value: 'json' }, + ], + default: 'text', + }, + + // Vault operations + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['vault'] } }, + options: [ + { name: 'Info', value: 'vaultInfo' }, + { name: 'List Vaults', value: 'vaults' }, + ], + default: 'vaultInfo', + }, + { + displayName: 'Verbose', + name: 'verbose', + type: 'boolean', + displayOptions: { show: { resource: ['vault'], operation: ['vaults'] } }, + default: true, + }, + + // Raw command + { + displayName: 'Args JSON', + name: 'rawArgs', + type: 'json', + displayOptions: { show: { resource: ['raw'] } }, + default: '["version"]', + description: 'Structured CLI args array. Example: ["create", "path=Inbox/Test.md", "content=Hello", "overwrite"]. Do not include shell strings.', + required: true, + }, + { + displayName: 'Timeout Seconds', + name: 'timeout', + type: 'number', + default: 60, + description: 'Command timeout in seconds', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const resource = this.getNodeParameter('resource', itemIndex) as string; + const operation = this.getNodeParameter('operation', itemIndex) as string; + const vault = this.getNodeParameter('vault', itemIndex, '') as string; + const timeout = this.getNodeParameter('timeout', itemIndex, 60) as number; + + let args: string[]; + + switch (resource) { + case 'note': { + args = buildNoteArgs(this, operation, itemIndex); + break; + } + case 'daily': { + args = buildDailyArgs(this, operation, itemIndex); + break; + } + case 'search': { + args = buildSearchArgs(this, operation, itemIndex); + break; + } + case 'vault': { + args = buildVaultArgs(this, operation, itemIndex); + break; + } + case 'raw': { + args = parseRawArgs(this, itemIndex); + break; + } + default: + throw new NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, { itemIndex }); + } + + const result = await executeObsidianCommand(this, itemIndex, args, { vault, timeout }); + returnData.push({ json: result, pairedItem: { item: itemIndex } }); + } + + return [returnData]; + } +} + +function buildNoteArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { + const path = context.getNodeParameter('path', itemIndex, '') as string; + switch (operation) { + case 'create': + return compactArgs(['create', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string), kv('overwrite', context.getNodeParameter('overwrite', itemIndex) as boolean)]); + case 'read': + return compactArgs(['read', kv('path', path)]); + case 'append': + return compactArgs(['append', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]); + case 'prepend': + return compactArgs(['prepend', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]); + case 'delete': + return compactArgs(['delete', kv('path', path)]); + case 'rename': + return compactArgs(['rename', kv('path', path), kv('name', context.getNodeParameter('newName', itemIndex) as string)]); + case 'files': + return compactArgs(['files', kv('folder', context.getNodeParameter('folder', itemIndex, '') as string)]); + default: + throw new NodeOperationError(context.getNode(), `Unsupported note operation: ${operation}`, { itemIndex }); +} +} + +function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { + switch (operation) { + case 'dailyAppend': + return compactArgs(['daily:append', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]); + case 'dailyPrepend': + return compactArgs(['daily:prepend', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]); + case 'dailyRead': + return ['daily:read']; + case 'dailyPath': + return ['daily:path']; + default: + throw new NodeOperationError(context.getNode(), `Unsupported daily operation: ${operation}`, { itemIndex }); +} +} + +function buildSearchArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { + const command = operation === 'searchContext' ? 'search:context' : 'search'; + return compactArgs([ + command, + kv('query', context.getNodeParameter('query', itemIndex) as string), + kv('format', context.getNodeParameter('format', itemIndex) as string), + ]); +} + +function buildVaultArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] { + switch (operation) { + case 'vaultInfo': + return ['vault']; + case 'vaults': + return compactArgs(['vaults', kv('verbose', context.getNodeParameter('verbose', itemIndex) as boolean)]); + default: + throw new NodeOperationError(context.getNode(), `Unsupported vault operation: ${operation}`, { itemIndex }); +} +} + +function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] { + const raw = context.getNodeParameter('rawArgs', itemIndex) as string | string[]; + const parsed = Array.isArray(raw) ? raw : JSON.parse(raw); + if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) { + throw new NodeOperationError(context.getNode(), 'Args JSON must be an array of strings', { itemIndex }); +} + return parsed; +} + diff --git a/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/obsidian.svg b/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/obsidian.svg new file mode 100644 index 0000000..679a19b --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/nodes/ObsidianApi/obsidian.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/n8n-nodes-obsidian-api/package-lock.json b/packages/n8n-nodes-obsidian-api/package-lock.json new file mode 100644 index 0000000..e221888 --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/package-lock.json @@ -0,0 +1,2304 @@ +{ + "name": "n8n-nodes-obsidian-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "n8n-nodes-obsidian-api", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^24.0.0", + "eslint": "^9.29.0", + "n8n-workflow": "^1.82.0", + "typescript": "^5.9.0" + }, + "peerDependencies": { + "n8n-workflow": "*" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@n8n_io/riot-tmpl": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@n8n_io/riot-tmpl/-/riot-tmpl-4.0.1.tgz", + "integrity": "sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-riot": "^1.0.0" + } + }, + "node_modules/@n8n/errors": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@n8n/errors/-/errors-0.5.1.tgz", + "integrity": "sha512-1FkdDcuOHKNE2U6irsljl3+RYXif9wZxqOaDjwZpTvTMxEKLMHt8oneN5/mIaLpAhPgk2eSyUcCkHEW+UjILCQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "callsites": "3.1.0" + } + }, + "node_modules/@n8n/expression-runtime": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@n8n/expression-runtime/-/expression-runtime-0.14.3.tgz", + "integrity": "sha512-l4zPD6yTQFwcpxjMGKfcW75Mg/L9HlLN58UJOPlFe2+H7mUiuZDk9FwTTFgxSuKmKDJr3q2yDptgceJGCRrqrg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@n8n/errors": "0.5.1", + "@n8n/tournament": "1.2.0", + "isolated-vm": "^6.1.2", + "jmespath": "0.16.0", + "js-base64": "3.7.2", + "jssha": "3.3.1", + "lodash": "4.18.1", + "luxon": "3.4.4", + "md5": "2.3.0", + "title-case": "3.0.3", + "transliteration": "2.3.5", + "zod": "3.25.67" + } + }, + "node_modules/@n8n/expression-runtime/node_modules/@n8n/tournament": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@n8n/tournament/-/tournament-1.2.0.tgz", + "integrity": "sha512-EY/vqijjYi2LhnKeShNXNtVBJW7lSRGFe5D2ikv5AfDZiLOqneRnNdfwZbv1bTLByRYyzY708KiXF8P66Gr1dg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "ast-types": "^0.16.1", + "esprima-next": "^5.8.4", + "recast": "^0.22.0" + }, + "engines": { + "node": ">=20.15", + "pnpm": ">=9.5" + } + }, + "node_modules/@n8n/tournament": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@n8n/tournament/-/tournament-1.0.6.tgz", + "integrity": "sha512-UGSxYXXVuOX0yL6HTLBStKYwLIa0+JmRKiSZSCMcM2s2Wax984KWT6XIA1TR/27i7yYpDk1MY14KsTPnuEp27A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@n8n_io/riot-tmpl": "^4.0.1", + "ast-types": "^0.16.1", + "esprima-next": "^5.8.4", + "recast": "^0.22.0" + }, + "engines": { + "node": ">=20.15", + "pnpm": ">=9.5" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-riot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-riot/-/eslint-config-riot-1.0.0.tgz", + "integrity": "sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima-next": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/esprima-next/-/esprima-next-5.8.4.tgz", + "integrity": "sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isolated-vm": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-6.1.2.tgz", + "integrity": "sha512-GGfsHqtlZiiurZaxB/3kY7LLAXR3sgzDul0fom4cSyBjx6ZbjpTrFWiH3z/nUfLJGJ8PIq9LQmQFiAxu24+I7A==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonrepair": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.2.tgz", + "integrity": "sha512-Leuly0nbM4R+S5SVJk3VHfw1oxnlEK9KygdZvfUtEtTawNDyzB4qa1xWTmFt1aeoA7sXZkVTRuIixJ8bAvqVUg==", + "dev": true, + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/n8n-workflow": { + "version": "1.120.18", + "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.120.18.tgz", + "integrity": "sha512-7SHVX+MTyrTvELEiCxdTPp3hA/du4knn3iQxbvVwg6td+XuApXNwuHHv8jBSN2XQlT+vigBPiYVHfcGFH2J0KQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@n8n/errors": "0.5.1", + "@n8n/expression-runtime": "0.14.3", + "@n8n/tournament": "1.0.6", + "ast-types": "0.16.1", + "callsites": "3.1.0", + "esprima-next": "5.8.4", + "form-data": "4.0.0", + "jmespath": "0.16.0", + "js-base64": "3.7.2", + "jsonrepair": "3.13.2", + "jssha": "3.3.1", + "lodash": "4.18.1", + "luxon": "3.4.4", + "md5": "2.3.0", + "recast": "0.22.0", + "title-case": "3.0.3", + "transliteration": "2.3.5", + "xml2js": "0.6.2", + "zod": "3.25.67" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/recast": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", + "integrity": "sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/transliteration": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", + "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yargs": "^17.5.1" + }, + "bin": { + "slugify": "dist/bin/slugify", + "transliterate": "dist/bin/transliterate" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/n8n-nodes-obsidian-api/package.json b/packages/n8n-nodes-obsidian-api/package.json new file mode 100644 index 0000000..1e365fb --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/package.json @@ -0,0 +1,50 @@ +{ + "name": "n8n-nodes-obsidian-api", + "version": "0.1.0", + "description": "n8n community nodes for the Obsidian API Docker service backed by the official Obsidian CLI.", + "license": "MIT", + "homepage": "https://git.yiprawr.dev/Docker/obsidian-api", + "repository": { + "type": "git", + "url": "git+ssh://git@git.yiprawr.dev/Docker/obsidian-api.git", + "directory": "packages/n8n-nodes-obsidian-api" + }, + "keywords": [ + "n8n-community-node-package", + "n8n", + "obsidian", + "obsidian-cli", + "notes" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json && mkdir -p dist/nodes/ObsidianApi && cp nodes/ObsidianApi/obsidian.svg dist/nodes/ObsidianApi/obsidian.svg", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint nodes credentials shared --ext .ts" + }, + "files": [ + "dist", + "nodes", + "credentials", + "shared" + ], + "n8n": { + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/ObsidianApi.credentials.js" + ], + "nodes": [ + "dist/nodes/ObsidianApi/ObsidianApi.node.js" + ] + }, + "devDependencies": { + "@types/node": "^24.0.0", + "eslint": "^9.29.0", + "n8n-workflow": "^1.82.0", + "typescript": "^5.9.0" + }, + "peerDependencies": { + "n8n-workflow": "*" + } +} diff --git a/packages/n8n-nodes-obsidian-api/shared/ObsidianApiClient.ts b/packages/n8n-nodes-obsidian-api/shared/ObsidianApiClient.ts new file mode 100644 index 0000000..0de92d3 --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/shared/ObsidianApiClient.ts @@ -0,0 +1,77 @@ +import type { + IExecuteFunctions, + IDataObject, + IHttpRequestOptions, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +export interface ObsidianCommandResult extends IDataObject { + id: string; + command: string[]; + cwd: string; + exit_code: number; + stdout: string; + stderr: string; + duration_ms: number; + timed_out: boolean; +} + +export interface ObsidianCredentials extends IDataObject { + baseUrl: string; + apiToken: string; + defaultVault?: string; +} + +export function compactArgs(args: Array): string[] { + return args.filter((arg): arg is string => typeof arg === 'string' && arg.length > 0); +} + +export function kv(name: string, value: string | number | boolean | undefined | null): string | undefined { + if (value === undefined || value === null || value === '') return undefined; + if (typeof value === 'boolean') return value ? name : undefined; + return `${name}=${value}`; +} + +export function withVault(args: string[], vault?: string): string[] { + if (!vault || args[0]?.startsWith('vault=')) return args; + return [`vault=${vault}`, ...args]; +} + +export async function executeObsidianCommand( + context: IExecuteFunctions, + itemIndex: number, + args: string[], + options: { vault?: string; timeout?: number; stdin?: string } = {}, +): Promise { + const credentials = await context.getCredentials('obsidianApi') as ObsidianCredentials; + const baseUrl = String(credentials.baseUrl).replace(/\/$/, ''); + const vault = options.vault || String(credentials.defaultVault || ''); + const finalArgs = withVault(args, vault); + + const request: IHttpRequestOptions = { + method: 'POST', + url: `${baseUrl}/commands`, + headers: { + Authorization: `Bearer ${credentials.apiToken}`, + 'Content-Type': 'application/json', + }, + body: { + args: finalArgs, + stdin: options.stdin || undefined, + timeout: options.timeout, + }, + json: true, + }; + + const result = await context.helpers.httpRequest(request) as ObsidianCommandResult; + + if (result.exit_code !== 0) { + throw new NodeOperationError( + context.getNode(), + `Obsidian CLI command failed with exit code ${result.exit_code}: ${result.stderr || result.stdout}`, + { itemIndex }, + ); + } + + return result; +} diff --git a/packages/n8n-nodes-obsidian-api/tsconfig.json b/packages/n8n-nodes-obsidian-api/tsconfig.json new file mode 100644 index 0000000..ffb6868 --- /dev/null +++ b/packages/n8n-nodes-obsidian-api/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022", "DOM"], + "declaration": true, + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["credentials/**/*.ts", "nodes/**/*.ts", "shared/**/*.ts", "index.ts"] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6743ea6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "obsidian-api" +version = "1.0.0" +description = "HTTP API wrapper for running Obsidian CLI commands from n8n, SDKs, or agent harnesses." +requires-python = ">=3.14" +dependencies = [ + "starlette==0.50.0", + "uvicorn[standard]==0.38.0", +] + +[dependency-groups] +dev = [ + "pytest==9.0.2", + "httpx==0.28.1", +] + +[tool.pytest.ini_options] +pythonpath = ["."] + +[tool.uv] +package = false diff --git a/scripts/create_jwt.py b/scripts/create_jwt.py new file mode 100644 index 0000000..fca491e --- /dev/null +++ b/scripts/create_jwt.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Create an HS256 JWT for the Obsidian API. + +Example: + OBSIDIAN_JWT_SECRET=change-this-long-random-secret \ + python scripts/create_jwt.py \ + --sub n8n \ + --vault work \ + --path Inbox/n8n/ \ + --command create --command read --command append \ + --days 365 +""" + +from __future__ import annotations + +import argparse, hashlib, base64 +import json, hmac, time, os +from typing import Any + +def b64url(value: bytes) -> str: + return base64.urlsafe_b64encode(value).decode().rstrip("=") + +def sign_jwt(claims: dict[str, Any], secret: bytes) -> str: + header = {"alg": "HS256", "typ": "JWT"} + header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode()) + payload_b64 = b64url(json.dumps(claims, separators=(",", ":")).encode()) + signing_input = f"{header_b64}.{payload_b64}".encode() + signature = hmac.new(secret, signing_input, hashlib.sha256).digest() + return f"{header_b64}.{payload_b64}.{b64url(signature)}" + +def main() -> int: + parser = argparse.ArgumentParser(description="Create an HS256 JWT for obsidian-api") + parser.add_argument("--secret", help="JWT signing secret. Defaults to OBSIDIAN_JWT_SECRET env.") + parser.add_argument("--sub", required=True, help="Subject/name for the token, e.g. n8n") + parser.add_argument("--name", help="Display name claim") + parser.add_argument("--vault", action="append", dest="vaults", default=[], help="Allowed vault. Repeatable. Default: *") + parser.add_argument("--path", action="append", dest="paths", default=[], help="Allowed path/prefix. Repeatable. Default: *") + parser.add_argument("--command", action="append", dest="commands", default=[], help="Allowed CLI command. Repeatable. Default: *") + parser.add_argument("--days", type=int, default=365, help="Days until expiry") + parser.add_argument("--claim", action="append", default=[], help="Extra claim as key=value. Repeatable.") + args = parser.parse_args() + + secret = args.secret or os.getenv("OBSIDIAN_JWT_SECRET", "") + if not secret: + raise SystemExit("Missing JWT secret. Set OBSIDIAN_JWT_SECRET or pass --secret.") + + now = int(time.time()) + claims: dict[str, Any] = { + "sub": args.sub, + "iat": now, + "exp": now + args.days * 24 * 60 * 60, + "vaults": args.vaults or ["*"], + "paths": args.paths or ["*"], + "commands": args.commands or ["*"], + } + if args.name: + claims["name"] = args.name + + for item in args.claim: + if "=" not in item: + raise SystemExit(f"Invalid --claim {item!r}; expected key=value") + key, value = item.split("=", 1) + claims[key] = value + + print(sign_jwt(claims, secret.encode())) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..51d9638 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh +set -eu + +mkdir -p "$XDG_RUNTIME_DIR" /root/.config /root/.config/obsidian /root/.cache /root/.local/share /run/dbus +chmod 700 "$XDG_RUNTIME_DIR" + +# Electron apps are happier with a DBus session, even under Xvfb. +if command -v dbus-launch >/dev/null 2>&1; then + eval "$(dbus-launch --sh-syntax)" + export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID +fi + +# Enable the official Obsidian CLI and register mounted vaults in global desktop-app settings. +# This is the same CLI setting toggled by Settings > General > Advanced > Command line interface. +DEFAULT_VAULT_PATH="$(python /app/scripts/render_config.py | tail -n 1)" + +# Start a virtual X server for the official Obsidian desktop app. +Xvfb "$DISPLAY" -screen 0 "${XVFB_SCREEN:-1280x1024x24}" -nolisten tcp >/tmp/xvfb.log 2>&1 & +XVFB_PID=$! + +cleanup() { + if [ -n "${OBSIDIAN_PID:-}" ] && kill -0 "$OBSIDIAN_PID" 2>/dev/null; then + kill "$OBSIDIAN_PID" 2>/dev/null || true + fi + kill "$XVFB_PID" 2>/dev/null || true +} +trap cleanup INT TERM EXIT + +# Open the mounted vault using the official desktop app. The official CLI talks +# to this running app through $XDG_RUNTIME_DIR/.obsidian-cli.sock. +/opt/Obsidian/obsidian \ + --no-sandbox \ + --disable-gpu \ + --disable-software-rasterizer \ + --disable-dev-shm-usage \ + --disable-features=UseOzonePlatform \ + "${DEFAULT_VAULT_PATH}" \ + >/tmp/obsidian.log 2>&1 & +OBSIDIAN_PID=$! + +SOCKET="$XDG_RUNTIME_DIR/.obsidian-cli.sock" +WAIT_SECONDS="${OBSIDIAN_STARTUP_TIMEOUT:-60}" +STARTED=0 +while [ "$STARTED" -lt "$WAIT_SECONDS" ]; do + if [ -S "$SOCKET" ]; then + break + fi + STARTED=$((STARTED + 1)) + sleep 1 +done + +if [ ! -S "$SOCKET" ]; then + echo "WARNING: Official Obsidian CLI socket was not created at $SOCKET after ${WAIT_SECONDS}s." >&2 + echo "The HTTP API will still start, but /commands may fail until Obsidian exposes the CLI socket." >&2 + echo "Obsidian log tail:" >&2 + tail -80 /tmp/obsidian.log >&2 || true +fi + +exec uv run python -m app.main diff --git a/scripts/render_config.py b/scripts/render_config.py new file mode 100644 index 0000000..8d2af8b --- /dev/null +++ b/scripts/render_config.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Render the global Obsidian desktop config (~/.config/obsidian/obsidian.json). + +Enables the official CLI and registers every mounted vault so the official +`obsidian` CLI can target them with `vault=`. + +Vaults are read from the environment: + +* ``OBSIDIAN_VAULTS`` (preferred, multi-vault): + - JSON object: {"work": "/vaults/work", "personal": "/vaults/personal"} + - or pairs: work=/vaults/work,personal=/vaults/personal +* ``OBSIDIAN_VAULT_PATH`` (single-vault fallback): /vault + +The first registered vault becomes the default (highest ``ts``), which is the +vault used when a CLI command omits ``vault=``. +""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +def _parse_vaults() -> "list[tuple[str, str]]": + raw = os.environ.get("OBSIDIAN_VAULTS", "").strip() + pairs: list[tuple[str, str]] = [] + + if raw: + # Try JSON object first. + try: + data = json.loads(raw) + if isinstance(data, dict): + return [(str(name), str(path)) for name, path in data.items()] + except json.JSONDecodeError: + pass + + # Fall back to comma/newline separated name=path pairs. + for chunk in raw.replace("\n", ",").split(","): + chunk = chunk.strip() + if not chunk: + continue + if "=" not in chunk: + print(f"WARNING: ignoring malformed OBSIDIAN_VAULTS entry: {chunk!r}", file=sys.stderr) + continue + name, path = chunk.split("=", 1) + pairs.append((name.strip(), path.strip())) + + if not pairs: + single = os.environ.get("OBSIDIAN_VAULT_PATH", "/vault").strip() or "/vault" + name = Path(single).name or "vault" + pairs.append((name, single)) + + return pairs + +def main() -> int: + pairs = _parse_vaults() + + vaults: dict[str, dict] = {} + # Descending timestamps so the first declared vault sorts as most-recent + # (the desktop app treats the highest ts as the active/default vault). + base_ts = 1893456000000 + for index, (name, path) in enumerate(pairs): + vaults[name] = { + "path": path, + "ts": base_ts - index, + "open": True, + } + + config = {"cli": True, "vaults": vaults} + + config_path = Path(os.environ.get("OBSIDIAN_CONFIG_PATH", "/root/.config/obsidian/obsidian.json")) + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") + + names = ", ".join(f"{name} -> {data['path']}" for name, data in vaults.items()) + print(f"Registered {len(vaults)} vault(s): {names}") + # First declared vault is the default for commands without vault=. + print(pairs[0][1]) # default vault path, consumed by the entrypoint + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md new file mode 100644 index 0000000..054c199 --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,59 @@ +# @obsidian-api/sdk +TypeScript SDK for this HTTP wrapper around the **official Obsidian CLI**. + +The SDK is built for Node 18+, n8n Code nodes, Prism, and other TypeScript agent harnesses. + +## Install locally +```bash +npm install /path/to/obsidian-api/sdk/typescript +``` + +## Usage +```ts +import { ObsidianCliClient } from '@obsidian-api/sdk'; + +const obsidian = new ObsidianCliClient({ + baseUrl: 'http://localhost:8080', + token: 'change-me', +}); + +await obsidian.create({ + path: 'Inbox/Hello.md', + content: '# Hello from n8n/Prism', + overwrite: true, +}); + +const note = await obsidian.read({ path: 'Inbox/Hello.md' }); +console.log(note.stdout); + +const results = await obsidian.search('Hello', { format: 'json' }); +console.log(JSON.parse(results.stdout)); +``` + +## Multiple vaults +Use the official CLI `vault=` prefix directly: +```ts +await obsidian.runInVault('work', 'create', 'path=Inbox/Task.md', 'content=Hello'); +``` + +Or create a scoped client: +```ts +const workVault = obsidian.inVault('work'); +await workVault.create({ path: 'Inbox/Task.md', content: 'Hello', overwrite: true }); +await workVault.read({ path: 'Inbox/Task.md' }); +``` + +You can also set a default vault in the constructor: +```ts +const obsidian = new ObsidianCliClient({ + baseUrl: 'http://localhost:8080', + token: 'change-me', + vault: 'work', +}); +``` + +## Raw command escape hatch +Still safe: sends an args array, not a shell command string. +```ts +await obsidian.runRaw('create', 'name=Scratch', 'content=Test', 'overwrite'); +``` diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000..3a2961b --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "@obsidian-api/sdk", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@obsidian-api/sdk", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "^5.9.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 0000000..b62e325 --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,28 @@ +{ + "name": "@obsidian-api/sdk", + "version": "1.0.0", + "description": "TypeScript SDK for the official Obsidian CLI HTTP wrapper. Built for n8n, Prism, and other TS/Node agent harnesses.", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist", "src"], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "node --test" + }, + "engines": { + "node": ">=18" + }, + "license": "MIT", + "devDependencies": { + "typescript": "^5.9.0" + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000..5985719 --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,258 @@ +export type OutputFormat = "json" | "tsv" | "csv" | "text" | "yaml" | "md" | "paths" | "tree"; +export type PaneType = "tab" | "split" | "window"; +export type PluginFilter = "core" | "community"; +export type PropertyType = "text" | "list" | "number" | "checkbox" | "date" | "datetime"; +export type HistoryFilter = "local" | "sync"; +export type LogLevel = "log" | "warn" | "error" | "info" | "debug"; + +export interface CommandResult { + id: string; + command: string[]; + cwd: string; + exit_code: number; + stdout: string; + stderr: string; + duration_ms: number; + timed_out: boolean; +} + +export class ObsidianApiError extends Error { + constructor(public statusCode: number, public detail: string) { + super(`HTTP ${statusCode}: ${detail}`); + this.name = "ObsidianApiError"; + } +} + +export interface ObsidianCliClientOptions { + baseUrl: string; + token?: string; + timeoutMs?: number; + /** Optional default vault name/path. Sent as the official CLI `vault=` prefix. */ + vault?: string; +} + +type ParamValue = string | number | boolean | undefined | null; + +function kv(name: string, value: ParamValue): string | undefined { + if (value === undefined || value === null) return undefined; + if (typeof value === "boolean") return value ? name : undefined; + return `${name}=${value}`; +} + +function args(...items: Array): string[] { + return items.filter((item): item is string => item !== undefined); +} + +export class ObsidianCliClient { + readonly baseUrl: string; + readonly token?: string; + readonly timeoutMs: number; + readonly defaultVault?: string; + + constructor(options: ObsidianCliClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/$/, ""); + this.token = options.token; + this.timeoutMs = options.timeoutMs ?? 60_000; + this.defaultVault = options.vault; + } + + /** + * Return a scoped client that automatically prefixes every command with + * `vault=`, matching the official Obsidian CLI multi-vault syntax. + */ + inVault(vault: string): ObsidianCliClient { + return new ObsidianCliClient({ + baseUrl: this.baseUrl, + token: this.token, + timeoutMs: this.timeoutMs, + vault, + }); + } + + async run(commandArgs: string[], options: { stdin?: string; timeoutMs?: number } = {}): Promise { + const timeoutMs = options.timeoutMs ?? this.timeoutMs; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs + 5_000); + + try { + const response = await fetch(`${this.baseUrl}/commands`, { + method: "POST", + headers: { + "content-type": "application/json", + ...(this.token ? { authorization: `Bearer ${this.token}` } : {}), + }, + body: JSON.stringify({ + args: this.defaultVault && !commandArgs[0]?.startsWith("vault=") ? [`vault=${this.defaultVault}`, ...commandArgs] : commandArgs, + stdin: options.stdin, + timeout: Math.ceil(timeoutMs / 1000), + }), + signal: controller.signal, + }); + + const data = await response.json() as CommandResult | { detail?: string }; + if (!response.ok) { + throw new ObsidianApiError(response.status, "detail" in data && data.detail ? data.detail : response.statusText); + } + return data as CommandResult; + } finally { + clearTimeout(timer); + } + } + + async runRaw(...commandArgs: string[]): Promise { + return this.run(commandArgs); + } + + async runInVault(vault: string, command: string, ...commandArgs: string[]): Promise { + return this.run([`vault=${vault}`, command, ...commandArgs]); + } + + // General + help = (command?: string) => this.run(args("help", command)); + version = () => this.run(["version"]); + reload = () => this.run(["reload"]); + restart = () => this.run(["restart"]); + + // Bases + bases = () => this.run(["bases"]); + baseViews = () => this.run(["base:views"]); + baseCreate = (o: { file?: string; path?: string; view?: string; name?: string; content?: string; open?: boolean; newtab?: boolean } = {}) => + this.run(args("base:create", kv("file", o.file), kv("path", o.path), kv("view", o.view), kv("name", o.name), kv("content", o.content), kv("open", o.open), kv("newtab", o.newtab))); + baseQuery = (o: { file?: string; path?: string; view?: string; format?: "json" | "csv" | "tsv" | "md" | "paths" } = {}) => + this.run(args("base:query", kv("file", o.file), kv("path", o.path), kv("view", o.view), kv("format", o.format))); + + // Bookmarks / command palette / hotkeys + bookmarks = (o: { total?: boolean; verbose?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("bookmarks", kv("total", o.total), kv("verbose", o.verbose), kv("format", o.format))); + bookmark = (o: { file?: string; subpath?: string; folder?: string; search?: string; url?: string; title?: string }) => this.run(args("bookmark", kv("file", o.file), kv("subpath", o.subpath), kv("folder", o.folder), kv("search", o.search), kv("url", o.url), kv("title", o.title))); + commands = (o: { filter?: string } = {}) => this.run(args("commands", kv("filter", o.filter))); + command = (id: string) => this.run(["command", kv("id", id)!]); + hotkeys = (o: { total?: boolean; verbose?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("hotkeys", kv("total", o.total), kv("verbose", o.verbose), kv("format", o.format))); + hotkey = (id: string, o: { verbose?: boolean } = {}) => this.run(args("hotkey", kv("id", id), kv("verbose", o.verbose))); + + // Daily notes + daily = (o: { paneType?: PaneType } = {}) => this.run(args("daily", kv("paneType", o.paneType))); + dailyPath = () => this.run(["daily:path"]); + dailyRead = () => this.run(["daily:read"]); + dailyAppend = (content: string, o: { paneType?: PaneType; inline?: boolean; open?: boolean } = {}) => this.run(args("daily:append", kv("content", content), kv("paneType", o.paneType), kv("inline", o.inline), kv("open", o.open))); + dailyPrepend = (content: string, o: { paneType?: PaneType; inline?: boolean; open?: boolean } = {}) => this.run(args("daily:prepend", kv("content", content), kv("paneType", o.paneType), kv("inline", o.inline), kv("open", o.open))); + + // File history + diff = (o: { file?: string; path?: string; from?: number; to?: number; filter?: HistoryFilter } = {}) => this.run(args("diff", kv("file", o.file), kv("path", o.path), kv("from", o.from), kv("to", o.to), kv("filter", o.filter))); + history = (o: { file?: string; path?: string } = {}) => this.run(args("history", kv("file", o.file), kv("path", o.path))); + historyList = () => this.run(["history:list"]); + historyRead = (o: { file?: string; path?: string; version?: number } = {}) => this.run(args("history:read", kv("file", o.file), kv("path", o.path), kv("version", o.version))); + historyRestore = (version: number, o: { file?: string; path?: string } = {}) => this.run(args("history:restore", kv("file", o.file), kv("path", o.path), kv("version", version))); + historyOpen = (o: { file?: string; path?: string } = {}) => this.run(args("history:open", kv("file", o.file), kv("path", o.path))); + + // Files and folders + file = (o: { file?: string; path?: string } = {}) => this.run(args("file", kv("file", o.file), kv("path", o.path))); + files = (o: { folder?: string; ext?: string; total?: boolean } = {}) => this.run(args("files", kv("folder", o.folder), kv("ext", o.ext), kv("total", o.total))); + folder = (path: string, o: { info?: "files" | "folders" | "size" } = {}) => this.run(args("folder", kv("path", path), kv("info", o.info))); + folders = (o: { folder?: string; total?: boolean } = {}) => this.run(args("folders", kv("folder", o.folder), kv("total", o.total))); + open = (o: { file?: string; path?: string; newtab?: boolean } = {}) => this.run(args("open", kv("file", o.file), kv("path", o.path), kv("newtab", o.newtab))); + create = (o: { name?: string; path?: string; content?: string; template?: string; overwrite?: boolean; open?: boolean; newtab?: boolean } = {}) => this.run(args("create", kv("name", o.name), kv("path", o.path), kv("content", o.content), kv("template", o.template), kv("overwrite", o.overwrite), kv("open", o.open), kv("newtab", o.newtab))); + read = (o: { file?: string; path?: string } = {}) => this.run(args("read", kv("file", o.file), kv("path", o.path))); + append = (content: string, o: { file?: string; path?: string; inline?: boolean } = {}) => this.run(args("append", kv("file", o.file), kv("path", o.path), kv("content", content), kv("inline", o.inline))); + prepend = (content: string, o: { file?: string; path?: string; inline?: boolean } = {}) => this.run(args("prepend", kv("file", o.file), kv("path", o.path), kv("content", content), kv("inline", o.inline))); + move = (to: string, o: { file?: string; path?: string } = {}) => this.run(args("move", kv("file", o.file), kv("path", o.path), kv("to", to))); + rename = (name: string, o: { file?: string; path?: string } = {}) => this.run(args("rename", kv("file", o.file), kv("path", o.path), kv("name", name))); + delete = (o: { file?: string; path?: string; permanent?: boolean } = {}) => this.run(args("delete", kv("file", o.file), kv("path", o.path), kv("permanent", o.permanent))); + + // Links / outline + backlinks = (o: { file?: string; path?: string; counts?: boolean; total?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("backlinks", kv("file", o.file), kv("path", o.path), kv("counts", o.counts), kv("total", o.total), kv("format", o.format))); + links = (o: { file?: string; path?: string; total?: boolean } = {}) => this.run(args("links", kv("file", o.file), kv("path", o.path), kv("total", o.total))); + unresolved = (o: { total?: boolean; counts?: boolean; verbose?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("unresolved", kv("total", o.total), kv("counts", o.counts), kv("verbose", o.verbose), kv("format", o.format))); + orphans = (o: { total?: boolean } = {}) => this.run(args("orphans", kv("total", o.total))); + deadends = (o: { total?: boolean } = {}) => this.run(args("deadends", kv("total", o.total))); + outline = (o: { file?: string; path?: string; format?: "tree" | "md" | "json"; total?: boolean } = {}) => this.run(args("outline", kv("file", o.file), kv("path", o.path), kv("format", o.format), kv("total", o.total))); + + // Plugins + plugins = (o: { filter?: PluginFilter; versions?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("plugins", kv("filter", o.filter), kv("versions", o.versions), kv("format", o.format))); + pluginsEnabled = (o: { filter?: PluginFilter; versions?: boolean; format?: "json" | "tsv" | "csv" } = {}) => this.run(args("plugins:enabled", kv("filter", o.filter), kv("versions", o.versions), kv("format", o.format))); + pluginsRestrict = (o: { on?: boolean; off?: boolean } = {}) => this.run(args("plugins:restrict", kv("on", o.on), kv("off", o.off))); + plugin = (id: string) => this.run(["plugin", kv("id", id)!]); + pluginEnable = (id: string, o: { filter?: PluginFilter } = {}) => this.run(args("plugin:enable", kv("id", id), kv("filter", o.filter))); + pluginDisable = (id: string, o: { filter?: PluginFilter } = {}) => this.run(args("plugin:disable", kv("id", id), kv("filter", o.filter))); + pluginInstall = (id: string, o: { enable?: boolean } = {}) => this.run(args("plugin:install", kv("id", id), kv("enable", o.enable))); + pluginUninstall = (id: string) => this.run(["plugin:uninstall", kv("id", id)!]); + pluginReload = (id: string) => this.run(["plugin:reload", kv("id", id)!]); + + // Properties + aliases = (o: { file?: string; path?: string; total?: boolean; verbose?: boolean; active?: boolean } = {}) => this.run(args("aliases", kv("file", o.file), kv("path", o.path), kv("total", o.total), kv("verbose", o.verbose), kv("active", o.active))); + properties = (o: { file?: string; path?: string; name?: string; sort?: "count"; format?: "yaml" | "json" | "tsv"; total?: boolean; counts?: boolean; active?: boolean } = {}) => this.run(args("properties", kv("file", o.file), kv("path", o.path), kv("name", o.name), kv("sort", o.sort), kv("format", o.format), kv("total", o.total), kv("counts", o.counts), kv("active", o.active))); + propertySet = (name: string, value: string, o: { type?: PropertyType; file?: string; path?: string } = {}) => this.run(args("property:set", kv("name", name), kv("value", value), kv("type", o.type), kv("file", o.file), kv("path", o.path))); + propertyRemove = (name: string, o: { file?: string; path?: string } = {}) => this.run(args("property:remove", kv("name", name), kv("file", o.file), kv("path", o.path))); + propertyRead = (name: string, o: { file?: string; path?: string } = {}) => this.run(args("property:read", kv("name", name), kv("file", o.file), kv("path", o.path))); + + // Publish + publishSite = () => this.run(["publish:site"]); + publishList = (o: { total?: boolean } = {}) => this.run(args("publish:list", kv("total", o.total))); + publishStatus = (o: { total?: boolean; new?: boolean; changed?: boolean; deleted?: boolean } = {}) => this.run(args("publish:status", kv("total", o.total), kv("new", o.new), kv("changed", o.changed), kv("deleted", o.deleted))); + publishAdd = (o: { file?: string; path?: string; changed?: boolean } = {}) => this.run(args("publish:add", kv("file", o.file), kv("path", o.path), kv("changed", o.changed))); + publishRemove = (o: { file?: string; path?: string } = {}) => this.run(args("publish:remove", kv("file", o.file), kv("path", o.path))); + publishOpen = (o: { file?: string; path?: string } = {}) => this.run(args("publish:open", kv("file", o.file), kv("path", o.path))); + + // Random / search + random = (o: { folder?: string; newtab?: boolean } = {}) => this.run(args("random", kv("folder", o.folder), kv("newtab", o.newtab))); + randomRead = (o: { folder?: string } = {}) => this.run(args("random:read", kv("folder", o.folder))); + search = (query: string, o: { path?: string; limit?: number; format?: "text" | "json"; total?: boolean; case?: boolean } = {}) => this.run(args("search", kv("query", query), kv("path", o.path), kv("limit", o.limit), kv("format", o.format), kv("total", o.total), kv("case", o.case))); + searchContext = (query: string, o: { path?: string; limit?: number; format?: "text" | "json"; case?: boolean } = {}) => this.run(args("search:context", kv("query", query), kv("path", o.path), kv("limit", o.limit), kv("format", o.format), kv("case", o.case))); + searchOpen = (o: { query?: string } = {}) => this.run(args("search:open", kv("query", o.query))); + + // Sync + sync = (o: { on?: boolean; off?: boolean } = {}) => this.run(args("sync", kv("on", o.on), kv("off", o.off))); + syncStatus = () => this.run(["sync:status"]); + syncHistory = (o: { file?: string; path?: string; total?: boolean } = {}) => this.run(args("sync:history", kv("file", o.file), kv("path", o.path), kv("total", o.total))); + syncRead = (version: number, o: { file?: string; path?: string } = {}) => this.run(args("sync:read", kv("file", o.file), kv("path", o.path), kv("version", version))); + syncRestore = (version: number, o: { file?: string; path?: string } = {}) => this.run(args("sync:restore", kv("file", o.file), kv("path", o.path), kv("version", version))); + syncOpen = (o: { file?: string; path?: string } = {}) => this.run(args("sync:open", kv("file", o.file), kv("path", o.path))); + syncDeleted = (o: { total?: boolean } = {}) => this.run(args("sync:deleted", kv("total", o.total))); + + // Tags / tasks + tags = (o: { file?: string; path?: string; sort?: "count"; total?: boolean; counts?: boolean; format?: "json" | "tsv" | "csv"; active?: boolean } = {}) => this.run(args("tags", kv("file", o.file), kv("path", o.path), kv("sort", o.sort), kv("total", o.total), kv("counts", o.counts), kv("format", o.format), kv("active", o.active))); + tag = (name: string, o: { total?: boolean; verbose?: boolean } = {}) => this.run(args("tag", kv("name", name), kv("total", o.total), kv("verbose", o.verbose))); + tasks = (o: { file?: string; path?: string; status?: string; total?: boolean; done?: boolean; todo?: boolean; verbose?: boolean; format?: "json" | "tsv" | "csv"; active?: boolean; daily?: boolean } = {}) => this.run(args("tasks", kv("file", o.file), kv("path", o.path), kv("status", o.status), kv("total", o.total), kv("done", o.done), kv("todo", o.todo), kv("verbose", o.verbose), kv("format", o.format), kv("active", o.active), kv("daily", o.daily))); + task = (o: { ref?: string; file?: string; path?: string; line?: number; status?: string; toggle?: boolean; daily?: boolean; done?: boolean; todo?: boolean } = {}) => this.run(args("task", kv("ref", o.ref), kv("file", o.file), kv("path", o.path), kv("line", o.line), kv("status", o.status), kv("toggle", o.toggle), kv("daily", o.daily), kv("done", o.done), kv("todo", o.todo))); + + // Templates / themes / snippets + templates = (o: { total?: boolean } = {}) => this.run(args("templates", kv("total", o.total))); + templateRead = (name: string, o: { title?: string; resolve?: boolean } = {}) => this.run(args("template:read", kv("name", name), kv("title", o.title), kv("resolve", o.resolve))); + templateInsert = (name: string) => this.run(["template:insert", kv("name", name)!]); + themes = (o: { versions?: boolean } = {}) => this.run(args("themes", kv("versions", o.versions))); + theme = (o: { name?: string } = {}) => this.run(args("theme", kv("name", o.name))); + themeSet = (name: string) => this.run(["theme:set", kv("name", name)!]); + themeInstall = (name: string, o: { enable?: boolean } = {}) => this.run(args("theme:install", kv("name", name), kv("enable", o.enable))); + themeUninstall = (name: string) => this.run(["theme:uninstall", kv("name", name)!]); + snippets = () => this.run(["snippets"]); + snippetsEnabled = () => this.run(["snippets:enabled"]); + snippetEnable = (name: string) => this.run(["snippet:enable", kv("name", name)!]); + snippetDisable = (name: string) => this.run(["snippet:disable", kv("name", name)!]); + + // Unique / vault / web / wordcount / workspace + unique = (o: { name?: string; content?: string; paneType?: PaneType; open?: boolean } = {}) => this.run(args("unique", kv("name", o.name), kv("content", o.content), kv("paneType", o.paneType), kv("open", o.open))); + vault = (o: { info?: "name" | "path" | "files" | "folders" | "size" } = {}) => this.run(args("vault", kv("info", o.info))); + vaults = (o: { total?: boolean; verbose?: boolean } = {}) => this.run(args("vaults", kv("total", o.total), kv("verbose", o.verbose))); + vaultOpen = (name: string) => this.run(["vault:open", kv("name", name)!]); + web = (url: string, o: { newtab?: boolean } = {}) => this.run(args("web", kv("url", url), kv("newtab", o.newtab))); + wordcount = (o: { file?: string; path?: string; words?: boolean; characters?: boolean } = {}) => this.run(args("wordcount", kv("file", o.file), kv("path", o.path), kv("words", o.words), kv("characters", o.characters))); + workspace = (o: { ids?: boolean } = {}) => this.run(args("workspace", kv("ids", o.ids))); + workspaces = (o: { total?: boolean } = {}) => this.run(args("workspaces", kv("total", o.total))); + workspaceSave = (name: string) => this.run(["workspace:save", kv("name", name)!]); + workspaceLoad = (name: string) => this.run(["workspace:load", kv("name", name)!]); + workspaceDelete = (name: string) => this.run(["workspace:delete", kv("name", name)!]); + tabs = (o: { ids?: boolean } = {}) => this.run(args("tabs", kv("ids", o.ids))); + tabOpen = (o: { group?: string; file?: string; view?: string } = {}) => this.run(args("tab:open", kv("group", o.group), kv("file", o.file), kv("view", o.view))); + recents = (o: { total?: boolean } = {}) => this.run(args("recents", kv("total", o.total))); + + // Developer commands + devtools = () => this.run(["devtools"]); + devDebug = (o: { on?: boolean; off?: boolean } = {}) => this.run(args("dev:debug", kv("on", o.on), kv("off", o.off))); + devCdp = (method: string, o: { params?: string } = {}) => this.run(args("dev:cdp", kv("method", method), kv("params", o.params))); + devErrors = (o: { clear?: boolean } = {}) => this.run(args("dev:errors", kv("clear", o.clear))); + devScreenshot = (o: { path?: string } = {}) => this.run(args("dev:screenshot", kv("path", o.path))); + devConsole = (o: { limit?: number; level?: LogLevel; clear?: boolean } = {}) => this.run(args("dev:console", kv("limit", o.limit), kv("level", o.level), kv("clear", o.clear))); + devCss = (selector: string, o: { prop?: string } = {}) => this.run(args("dev:css", kv("selector", selector), kv("prop", o.prop))); + devDom = (selector: string, o: { attr?: string; css?: string; total?: boolean; text?: boolean; inner?: boolean; all?: boolean } = {}) => this.run(args("dev:dom", kv("selector", selector), kv("attr", o.attr), kv("css", o.css), kv("total", o.total), kv("text", o.text), kv("inner", o.inner), kv("all", o.all))); + devMobile = (o: { on?: boolean; off?: boolean } = {}) => this.run(args("dev:mobile", kv("on", o.on), kv("off", o.off))); + eval = (code: string) => this.run(["eval", kv("code", code)!]); +} diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000..5499e8d --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..23a446e --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,167 @@ +import hashlib, base64, json, hmac +import time, os + +from starlette.testclient import TestClient + +os.environ.setdefault("OBSIDIAN_JWT_SECRET", "test-jwt-secret") +os.environ.setdefault("OBSIDIAN_CLI_BIN", "python") +os.environ.setdefault("OBSIDIAN_VAULT_PATH", ".") + +import app.auth as authmod # noqa: E402 +from app.main import app # noqa: E402 + +client = TestClient(app) + +def make_jwt(claims:dict, secret:bytes = b"test-jwt-secret") -> str: + header = {"alg": "HS256", "typ": "JWT"} + + def enc(value:dict) -> str: + raw = json.dumps(value, separators=(",", ":")).encode() + return base64.urlsafe_b64encode(raw).decode().rstrip("=") + + signing_input = f"{enc(header)}.{enc(claims)}" + signature = hmac.new(secret, signing_input.encode(), hashlib.sha256).digest() + return f"{signing_input}.{base64.urlsafe_b64encode(signature).decode().rstrip('=')}" + +def jwt(claims:dict|None = None) -> str: + claims = claims or {} + claims.setdefault("sub", "tests") + claims.setdefault("vaults", ["*"]) + claims.setdefault("paths", ["*"]) + claims.setdefault("commands", ["*"]) + claims.setdefault("exp", int(time.time()) + 3600) + return make_jwt(claims) + +def use_jwt_auth(monkeypatch, claims:dict) -> str: + monkeypatch.setattr(authmod, "JWT_SECRET", "test-jwt-secret") + return jwt(claims) + +def test_health(): + response = client.get("/health") + + assert response.status_code == 200 + body = response.json() + assert body["ok"] is True + assert "vault" in body + assert body["cli"] == "python" + assert body["jwt"] is True + +def test_command_requires_auth(): + response = client.post("/commands", json={"args": ["--version"]}) + + assert response.status_code == 401 + +def test_command_runs_configured_cli_only(): + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {jwt()}"}, + json={"args": ["--version"], "timeout": 5}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["exit_code"] == 0 + assert body["command"] == ["python", "--version"] + assert "Python" in body["stdout"] + assert body["timed_out"] is False + +def test_plain_command_string_is_rejected(): + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {jwt()}"}, + json={"command": "--version"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "command strings are not allowed; use args as a list of strings" + +def test_shell_mode_is_rejected(): + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {jwt()}"}, + json={"mode": "shell", "args": ["echo nope"]}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "mode is not allowed; only the configured Obsidian CLI can be executed" + +def test_command_timeout(): + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {jwt()}"}, + json={"args": ["-c", "import time; time.sleep(2)"], "timeout": 0.1}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["exit_code"] == 124 + assert body["timed_out"] is True + +def test_jwt_claims_become_policy(): + token = jwt({"sub": "file", "vaults": ["work"], "paths": ["Inbox/"], "commands": ["read"]}) + claims = authmod.decode_hs256_jwt(token, b"test-jwt-secret") + policy = authmod.policy_from_jwt_claims(token, claims) + + assert policy.token == token + assert policy.name == "file" + assert policy.vaults == ("work",) + assert policy.paths == ("Inbox/",) + assert policy.commands == ("read",) + +def test_token_policy_restricts_command(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "limited", "commands": ["read"], "vaults": ["*"], "paths": ["*"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["create", "path=Inbox/Nope.md", "content=Nope"]}, + ) + + assert response.status_code == 403 + assert response.json()["detail"] == "token is not allowed to run command: create" + +def test_token_policy_restricts_vault(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "work", "commands": ["*"], "vaults": ["work"], "paths": ["*"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["vault=personal", "read", "path=Inbox/Test.md"]}, + ) + + assert response.status_code == 403 + assert response.json()["detail"] == "token is not allowed to access vault: personal" + +def test_token_policy_restricts_path(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["read", "path=Private/Secret.md"]}, + ) + + assert response.status_code == 403 + assert response.json()["detail"] == "token is not allowed to access path: Private/Secret.md" + +def test_token_policy_allows_allowed_path(monkeypatch): + token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]}) + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["-c", "print('ok')"]}, + ) + + assert response.status_code == 403 + + response = client.post( + "/commands", + headers={"Authorization": f"Bearer {token}"}, + json={"args": ["read", "path=Inbox/Allowed.md"]}, + ) + + # The authorization layer allows this. The fake Python CLI then fails because + # "read" is not a Python flag, which is fine for this policy test. + assert response.status_code == 200 + assert response.json()["command"] == ["python", "read", "path=Inbox/Allowed.md"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f61e04d --- /dev/null +++ b/uv.lock @@ -0,0 +1,357 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "anyio" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "obsidian-api" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "starlette" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "starlette", specifier = "==0.50.0" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.38.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = "==0.28.1" }, + { name = "pytest", specifier = "==9.0.2" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +]