diff --git a/.gitignore b/.gitignore index 48ba819f..87435fcb 100644 --- a/.gitignore +++ b/.gitignore @@ -117,8 +117,13 @@ eslint-typegen.d.ts # Workspace package build output packages/runtime/dist/ +packages/gateway/dist/ apps/action/dist/ # BEGIN oh-my-opencode-slim clonedeps .slim/clonedeps/repos/ # END oh-my-opencode-slim clonedeps + +# Deploy stack — secrets and local overrides +deploy/secrets/ +deploy/compose.override.yaml diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..f47b2056 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,142 @@ +# fro-bot Deploy Stack + +Docker Compose v2 stack for the fro-bot gateway. Runs three services: + +| Service | Role | +| ----------- | --------------------------------------------------------------------------------- | +| `gateway` | Discord gateway daemon — connects to Discord, handles slash commands and mentions | +| `workspace` | Workspace agent container (placeholder in v1; real agent wired in Unit 7) | +| `mitmproxy` | Egress proxy enforcing an allowlist of permitted outbound hosts | + +## Prerequisites + +- Docker 24+ with Compose v2 (`docker compose version`) +- Access to a Discord application (bot token + application ID) +- An S3-compatible object store (bucket, region, optional endpoint) + +## One-Time Setup + +### 1. Copy the override example + +```bash +cp deploy/compose.override.example.yaml deploy/compose.override.yaml +# Edit compose.override.yaml for your environment (e.g. expose mitmproxy web UI in dev) +``` + +`compose.override.yaml` is gitignored — never commit it. + +### 2. Create secrets + +Create one file per secret under `deploy/secrets/`. Files must be readable only by the owner (`chmod 0600`). + +```bash +mkdir -p deploy/secrets +echo -n 'YOUR_DISCORD_BOT_TOKEN' > deploy/secrets/discord-token +echo -n 'YOUR_DISCORD_APP_ID' > deploy/secrets/discord-application-id +echo -n 'your-s3-bucket-name' > deploy/secrets/s3-bucket +echo -n 'us-east-1' > deploy/secrets/s3-region +# Optional — omit for AWS S3; set for R2 or other S3-compatible stores: +echo -n 'https://your-endpoint.r2.dev' > deploy/secrets/s3-endpoint + +chmod 0600 deploy/secrets/* +``` + +`deploy/secrets/` is gitignored — never commit secret files. + +### 3. Bootstrap the mitmproxy CA + +Run once to generate the mitmproxy CA and place it in the shared Docker volume: + +```bash +bash deploy/init-certs.sh +``` + +This is idempotent — safe to run again; skips if the CA already exists. + +## Starting the Stack + +```bash +docker compose -f deploy/compose.yaml -f deploy/compose.override.yaml up -d +``` + +Or without an override file: + +```bash +docker compose -f deploy/compose.yaml up -d +``` + +## Viewing Logs + +```bash +# Follow gateway logs +docker compose -f deploy/compose.yaml logs -f gateway + +# Follow all services +docker compose -f deploy/compose.yaml logs -f +``` + +## Validating the Stack + +After `up -d`, run the smoke-test script: + +```bash +bash deploy/validate-stack.sh +``` + +This checks: + +- Compose YAML is valid +- Service status +- Recent log output +- Gateway exit code (fails if gateway crashed in the last cycle) + +## Stopping the Stack + +```bash +docker compose -f deploy/compose.yaml down +``` + +## mitmproxy CA Cert (Dev Only) + +If you want your host browser or tools to trust the mitmproxy CA (useful for inspecting proxied traffic in dev), extract and install it: + +```bash +# Extract the cert from the Docker volume +docker run --rm \ + -v fro-bot_mitmproxy-certs:/certs \ + alpine cat /certs/mitmproxy-ca-cert.pem \ + | sudo tee /usr/local/share/ca-certificates/mitmproxy-fro-bot.crt + +# Install (Linux) +sudo update-ca-certificates + +# Install (macOS) +sudo security add-trusted-cert -d -r trustRoot \ + -k /Library/Keychains/System.keychain \ + /usr/local/share/ca-certificates/mitmproxy-fro-bot.crt +``` + +**Do not install the mitmproxy CA on production hosts.** It is only needed on developer machines that want to inspect proxied traffic via the mitmproxy web UI. + +## Egress Allowlist + +The mitmproxy addon at `deploy/mitmproxy/allowlist.py` enforces a static allowlist of permitted outbound hosts. Changes require restarting the mitmproxy container. The allowlist covers: + +- GitHub API + raw content +- npm registry +- Discord API + gateway +- LLM providers (Anthropic, OpenAI, Google) + +Any host not on the list receives a 403 and the connection is dropped. Both HTTPS CONNECT tunnels and plain HTTP requests are enforced. + +### Object-store bucket scoping + +The allowlist does **not** include broad S3/R2 wildcards (`*.s3.amazonaws.com`, `*.r2.cloudflarestorage.com`). Instead, set the `OBJECT_STORE_HOSTS` environment variable on the `mitmproxy` service to the exact bucket host(s) your deployment uses: + +``` +OBJECT_STORE_HOSTS=my-bucket.s3.amazonaws.com,my-account.r2.cloudflarestorage.com +``` + +If `OBJECT_STORE_HOSTS` is unset or empty, all S3/R2 traffic is blocked (fail-closed default). This prevents workspace processes from exfiltrating data to attacker-controlled buckets in those clouds. + +Set the variable in your `.env` file or `compose.override.yaml` (see `compose.override.example.yaml` for an example). diff --git a/deploy/compose.override.example.yaml b/deploy/compose.override.example.yaml new file mode 100644 index 00000000..8cc9fa77 --- /dev/null +++ b/deploy/compose.override.example.yaml @@ -0,0 +1,39 @@ +# compose.override.example.yaml +# +# Copy this file to compose.override.yaml and edit for your local dev environment. +# compose.override.yaml is gitignored — never commit it. +# +# Usage: +# docker compose -f compose.yaml -f compose.override.yaml up -d +# +# This file is intentionally minimal. Only override what differs from compose.yaml. + +services: + mitmproxy: + # Expose the mitmproxy web UI on the host for dev inspection. + # Remove or comment out in production. + ports: + - '127.0.0.1:8081:8081' + command: > + mitmdump + -s /scripts/allowlist.py + --listen-host 0.0.0.0 + --listen-port 8080 + --web-host 0.0.0.0 + --web-port 8081 + --set confdir=/home/mitmproxy/.mitmproxy + --set ssl_insecure=false + # environment: + # OBJECT_STORE_HOSTS is intentionally omitted here so it falls through to + # compose.yaml's interpolation: OBJECT_STORE_HOSTS: ${OBJECT_STORE_HOSTS:-} + # Set it in your .env file or shell environment instead. + # Example: "my-bucket.s3.amazonaws.com,my-account.r2.cloudflarestorage.com" + # If unset or empty, all S3/R2 traffic is blocked (fail-closed default). + + gateway: + # Override the mitmproxy-certs volume path for dev if you want a local dir + # instead of the named volume (useful for inspecting the CA cert directly). + # volumes: + # - ./dev-certs:/etc/ssl/certs:ro + environment: + LOG_LEVEL: debug diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 00000000..01ce1548 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,108 @@ +name: fro-bot + +services: + gateway: + build: + context: .. + dockerfile: deploy/gateway.Dockerfile + depends_on: + # Wait until mitmproxy has actually written the CA cert into the shared + # volume, not just until its container has started. Without this the + # gateway can launch with NODE_EXTRA_CA_CERTS pointing at a missing + # file — Node silently ignores it and the egress allowlist is bypassed + # for any request the gateway makes during the boot window. + mitmproxy: + condition: service_healthy + environment: + # Secret file paths (read via readSecret helper) + DISCORD_TOKEN_FILE: /run/secrets/discord_token + DISCORD_APPLICATION_ID_FILE: /run/secrets/discord_application_id + S3_BUCKET_FILE: /run/secrets/s3_bucket + S3_REGION_FILE: /run/secrets/s3_region + S3_ENDPOINT_FILE: /run/secrets/s3_endpoint + # Egress proxy (regular proxy mode — NOT transparent) + HTTPS_PROXY: http://mitmproxy:8080 + HTTP_PROXY: http://mitmproxy:8080 + NO_PROXY: workspace,localhost,127.0.0.1 + NODE_EXTRA_CA_CERTS: /etc/ssl/certs/mitmproxy-ca-cert.pem + volumes: + # Credentials — bind-mounted as accepted-risk (no Docker Swarm secrets in v1) + - ./secrets/discord-token:/run/secrets/discord_token:ro + - ./secrets/discord-application-id:/run/secrets/discord_application_id:ro + - ./secrets/s3-bucket:/run/secrets/s3_bucket:ro + - ./secrets/s3-region:/run/secrets/s3_region:ro + - ./secrets/s3-endpoint:/run/secrets/s3_endpoint:ro + # mitmproxy CA cert (written by mitmproxy on first start, read here for trust) + - mitmproxy-certs:/etc/ssl/certs:ro + networks: + - gateway-net + - sandbox-net + restart: unless-stopped + healthcheck: + test: [CMD, node, -e, process.exit(0)] + interval: 30s + timeout: 5s + retries: 3 + + workspace: + build: + context: .. + dockerfile: deploy/workspace.Dockerfile + depends_on: + mitmproxy: + condition: service_healthy + environment: + # Egress proxy — same as gateway + HTTPS_PROXY: http://mitmproxy:8080 + HTTP_PROXY: http://mitmproxy:8080 + NO_PROXY: localhost,127.0.0.1,workspace + networks: + - sandbox-net + restart: unless-stopped + + mitmproxy: + image: mitmproxy/mitmproxy:11.0.2 + command: > + mitmdump + -s /scripts/allowlist.py + --listen-host 0.0.0.0 + --listen-port 8080 + --set confdir=/home/mitmproxy/.mitmproxy + --set ssl_insecure=false + environment: + # Comma-separated list of exact object-store hosts to allow (e.g. + # "my-bucket.s3.amazonaws.com,my-account.r2.cloudflarestorage.com"). + # The static allowlist does NOT include broad S3/R2 wildcards — set this + # to the exact bucket host(s) your deployment uses. If unset or empty, + # all S3/R2 traffic is blocked (fail-closed default). + OBJECT_STORE_HOSTS: ${OBJECT_STORE_HOSTS:-} + volumes: + - ./mitmproxy/allowlist.py:/scripts/allowlist.py:ro + # Named volume: mitmproxy writes CA here; gateway/workspace read it for trust + - mitmproxy-certs:/home/mitmproxy/.mitmproxy + networks: + - sandbox-net + restart: unless-stopped + healthcheck: + # Gate dependent services on the CA actually existing in the shared + # volume. mitmproxy generates ~/.mitmproxy/mitmproxy-ca-cert.pem on + # first start; it takes ~1-3 seconds on cold-volume boot. Until this + # check passes, gateway/workspace will not start. + test: [CMD-SHELL, test -f /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem] + interval: 5s + timeout: 3s + retries: 12 + start_period: 30s + +volumes: + # Shared CA cert volume: mitmproxy writes, gateway + workspace read + mitmproxy-certs: + +networks: + # External-facing network for gateway ↔ Discord + gateway-net: + driver: bridge + # Internal sandbox network: workspace + mitmproxy; gateway bridges both + sandbox-net: + driver: bridge + internal: true diff --git a/deploy/gateway.Dockerfile b/deploy/gateway.Dockerfile new file mode 100644 index 00000000..4b52cc40 --- /dev/null +++ b/deploy/gateway.Dockerfile @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1 + +# ── Stage 1: build ──────────────────────────────────────────────────────────── +FROM node:24-alpine AS build + +WORKDIR /workspace + +# Enable corepack for pnpm +RUN corepack enable + +# Copy workspace root manifests first (layer-cache friendly) +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ + +# Copy only the packages we need for the gateway build +COPY packages/runtime/ packages/runtime/ +COPY packages/gateway/ packages/gateway/ + +# Install dependencies for gateway and its workspace deps only +RUN pnpm install --frozen-lockfile --filter @fro-bot/gateway... + +# Build runtime first (gateway depends on it) +RUN pnpm --filter @fro-bot/runtime build + +# Build gateway +RUN pnpm --filter @fro-bot/gateway build + +# ── Stage 2: runtime ────────────────────────────────────────────────────────── +FROM node:24-alpine AS runtime + +WORKDIR /app + +# Copy production node_modules from build stage +COPY --from=build /workspace/node_modules ./node_modules +COPY --from=build /workspace/packages/runtime/package.json ./packages/runtime/package.json +COPY --from=build /workspace/packages/runtime/dist/ ./packages/runtime/dist/ +COPY --from=build /workspace/packages/gateway/package.json ./packages/gateway/package.json +COPY --from=build /workspace/packages/gateway/dist/ ./packages/gateway/dist/ + +WORKDIR /app/packages/gateway + +# Placeholder healthcheck — real HTTP health endpoint arrives in Unit 7 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD node -e 'process.exit(0)' + +CMD ["node", "dist/main.mjs"] diff --git a/deploy/init-certs.sh b/deploy/init-certs.sh new file mode 100644 index 00000000..47e4fde1 --- /dev/null +++ b/deploy/init-certs.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# init-certs.sh — Bootstrap the mitmproxy CA into the named Docker volume. +# +# Run this once before starting the stack for the first time. Idempotent: +# skips generation if the CA cert already exists in the volume. +# +# Usage (from repo root): +# bash deploy/init-certs.sh +set -euo pipefail + +VOLUME_NAME="fro-bot_mitmproxy-certs" +CERT_FILE="mitmproxy-ca-cert.pem" + +echo "==> Checking for existing mitmproxy CA in volume '${VOLUME_NAME}'..." + +# Check if the cert already exists in the volume +if docker run --rm \ + -v "${VOLUME_NAME}:/certs" \ + --entrypoint sh \ + mitmproxy/mitmproxy:11.0.2 \ + -c "test -f /certs/${CERT_FILE}"; then + echo "==> CA cert already present — skipping generation." + exit 0 +fi + +echo "==> Generating mitmproxy CA (this may take a moment)..." + +# Run mitmdump with -n (no-server mode) so it initialises the confdir and exits. +# The CA key + cert are written to the volume at /home/mitmproxy/.mitmproxy. +docker run --rm \ + -v "${VOLUME_NAME}:/home/mitmproxy/.mitmproxy" \ + mitmproxy/mitmproxy:11.0.2 \ + mitmdump --set confdir=/home/mitmproxy/.mitmproxy --quiet -n + +echo "==> CA generated successfully." +echo " Volume: ${VOLUME_NAME}" +echo " Cert: ${CERT_FILE} (inside volume at /home/mitmproxy/.mitmproxy/)" +echo "" +echo " To add the CA to your host trust store (optional, dev only):" +echo " docker run --rm -v ${VOLUME_NAME}:/certs alpine cat /certs/${CERT_FILE} \\" +echo " | sudo tee /usr/local/share/ca-certificates/mitmproxy-fro-bot.crt" +echo " sudo update-ca-certificates" diff --git a/deploy/mitmproxy/.gitignore b/deploy/mitmproxy/.gitignore new file mode 100644 index 00000000..7a60b85e --- /dev/null +++ b/deploy/mitmproxy/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/deploy/mitmproxy/allowlist.py b/deploy/mitmproxy/allowlist.py new file mode 100644 index 00000000..e4629786 --- /dev/null +++ b/deploy/mitmproxy/allowlist.py @@ -0,0 +1,130 @@ +""" +mitmproxy egress allowlist addon — fro-bot gateway v1. + +Enforces a static allowlist of permitted CONNECT destinations and plain HTTP +request hosts. Any host not on the list receives a synthetic 403 short-circuit +response. + +Changes to the allowlist require restarting the mitmproxy container (no runtime +YAML override in v1 — the list is intentionally a code-level constant so that +changes go through review). + +Environment variables +--------------------- +OBJECT_STORE_HOSTS + Comma-separated list of additional hosts to allow for object-store access. + Example: "my-bucket.s3.amazonaws.com,my-account.r2.cloudflarestorage.com" + + The static allowlist intentionally omits the broad *.s3.amazonaws.com and + *.r2.cloudflarestorage.com wildcards to prevent data exfiltration to + attacker-controlled buckets in those clouds. Set this variable to the exact + bucket host(s) your deployment uses. + + If unset or empty, all S3/R2 traffic is blocked (fail-closed default). +""" + +import os +import sys +from mitmproxy import http + +# --------------------------------------------------------------------------- +# Allowlist — production-safe defaults for fro-bot v1. +# Entries starting with "*." match any subdomain of the given domain. +# --------------------------------------------------------------------------- +ALLOWLIST: list[str] = [ + # GitHub + "api.github.com", + "objects.githubusercontent.com", + "uploads.github.com", + "raw.githubusercontent.com", + "github.com", + # npm registry + "registry.npmjs.org", + # NOTE: S3/R2 wildcards removed — use OBJECT_STORE_HOSTS env var to scope + # to the exact bucket host(s) your deployment uses. See module docstring. + # Discord + "discord.com", + "gateway.discord.gg", + "*.discord.com", + "*.discord.gg", + # LLM providers + "api.anthropic.com", + "api.openai.com", + "generativelanguage.googleapis.com", +] + +# --------------------------------------------------------------------------- +# Merge OBJECT_STORE_HOSTS env var into the allowlist at module import time. +# --------------------------------------------------------------------------- +_object_store_hosts_raw = os.environ.get("OBJECT_STORE_HOSTS", "") +_object_store_hosts: list[str] = [ + h.strip() for h in _object_store_hosts_raw.split(",") if h.strip() +] +ALLOWLIST = ALLOWLIST + _object_store_hosts + + +def _is_allowed(host: str) -> bool: + """Return True if *host* matches any entry in ALLOWLIST. + + Wildcard semantics: "*.example.com" matches both the bare apex + (example.com) and any subdomain (api.example.com). This is intentional + — most providers expose both an apex and subdomain surface, and listing + them separately doubles the allowlist without any security benefit. + """ + host = host.lower() + for entry in ALLOWLIST: + if entry.startswith("*."): + # Wildcard: match the apex domain OR any subdomain of it. + base = entry[2:] # strip "*." + if host == base or host.endswith("." + base): + return True + else: + if host == entry: + return True + return False + + +class AllowlistAddon: + def _enforce(self, flow: http.HTTPFlow, kind: str) -> None: + """Apply the allowlist to *flow*. *kind* is 'connect' or 'request' for logging.""" + host = flow.request.host + if not _is_allowed(host): + print( + f"[allowlist] BLOCKED {kind} host={host} " + f"client={flow.client_conn.peername}", + file=sys.stderr, + flush=True, + ) + # Setting flow.response short-circuits the request before mitmproxy + # establishes the upstream tunnel. We intentionally do NOT call + # flow.kill() — that would also produce a redundant "killed" log + # line in some mitmproxy versions. + flow.response = http.Response.make( + 403, + f"Blocked by fro-bot egress allowlist: {host}", + {"Content-Type": "text/plain"}, + ) + else: + print( + f"[allowlist] ALLOWED {kind} host={host}", + file=sys.stderr, + flush=True, + ) + + def http_connect(self, flow: http.HTTPFlow) -> None: + """Enforce allowlist for HTTPS CONNECT tunnels.""" + self._enforce(flow, "connect") + + def request(self, flow: http.HTTPFlow) -> None: + """Enforce allowlist for plain HTTP requests. + + The http_connect hook only fires for HTTPS CONNECT tunnels. Plain HTTP + requests routed through HTTP_PROXY flow through this hook instead. + Without this check, a workspace container could bypass the allowlist + entirely by using http:// URLs. + """ + self._enforce(flow, "request") + + + +addons = [AllowlistAddon()] diff --git a/deploy/mitmproxy/test_allowlist.py b/deploy/mitmproxy/test_allowlist.py new file mode 100644 index 00000000..270fbb08 --- /dev/null +++ b/deploy/mitmproxy/test_allowlist.py @@ -0,0 +1,294 @@ +""" +Tests for deploy/mitmproxy/allowlist.py. + +Run with: + pytest deploy/mitmproxy/test_allowlist.py -v + +Or without pytest installed: + python3 deploy/mitmproxy/test_allowlist.py + +The mitmproxy package is NOT required — it is mocked via sys.modules injection +below so this suite can run in any standard Python 3.11+ environment. +""" + +import sys +from unittest.mock import MagicMock + +# --------------------------------------------------------------------------- +# Mock mitmproxy before importing allowlist.py — the test env does not have +# mitmproxy installed. +# --------------------------------------------------------------------------- +mitmproxy_http_mock = MagicMock() +sys.modules["mitmproxy"] = MagicMock(http=mitmproxy_http_mock) +sys.modules["mitmproxy.http"] = mitmproxy_http_mock + +# --------------------------------------------------------------------------- +# Now we can safely import the module under test. +# --------------------------------------------------------------------------- +import importlib +import os +import types + + +def _load_allowlist(extra_env: dict | None = None): + """Import (or re-import) allowlist with optional env overrides. + + Because ALLOWLIST is built at module import time from the env var, we need + to reload the module for each env-var test scenario. + """ + env_backup = os.environ.copy() + try: + if extra_env: + os.environ.update(extra_env) + else: + os.environ.pop("OBJECT_STORE_HOSTS", None) + + # Remove cached module so import runs fresh. + sys.modules.pop("allowlist", None) + + import importlib.util + import pathlib + + spec = importlib.util.spec_from_file_location( + "allowlist", + pathlib.Path(__file__).parent / "allowlist.py", + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + finally: + os.environ.clear() + os.environ.update(env_backup) + + +def _make_flow(host: str, peername: str = "10.0.0.1:12345"): + """Build a minimal mock flow object.""" + flow = MagicMock() + flow.request.host = host + flow.client_conn.peername = peername + flow.response = None # will be set by addon on block + return flow + + +# =========================================================================== +# _is_allowed tests +# =========================================================================== + + +def test_is_allowed_exact_match(): + mod = _load_allowlist() + assert mod._is_allowed("api.github.com") is True + + +def test_is_allowed_exact_no_match(): + mod = _load_allowlist() + assert mod._is_allowed("evil.example") is False + + +def test_is_allowed_wildcard_apex(): + """*.discord.com should match the bare apex discord.com.""" + mod = _load_allowlist() + assert mod._is_allowed("discord.com") is True + + +def test_is_allowed_wildcard_subdomain(): + """*.discord.com should match cdn.discord.com.""" + mod = _load_allowlist() + assert mod._is_allowed("cdn.discord.com") is True + + +def test_is_allowed_case_normalization(): + """Host matching must be case-insensitive.""" + mod = _load_allowlist() + assert mod._is_allowed("API.GitHub.COM") is True + + +def test_is_allowed_non_match(): + mod = _load_allowlist() + assert mod._is_allowed("attacker.example.com") is False + + +def test_is_allowed_empty_allowlist(): + """With an empty allowlist, nothing is allowed.""" + mod = _load_allowlist() + original = mod.ALLOWLIST[:] + mod.ALLOWLIST.clear() + try: + assert mod._is_allowed("api.github.com") is False + finally: + mod.ALLOWLIST.extend(original) + + +# =========================================================================== +# OBJECT_STORE_HOSTS env-var merging +# =========================================================================== + + +def test_object_store_hosts_included_when_set(): + mod = _load_allowlist( + {"OBJECT_STORE_HOSTS": "my-bucket.s3.amazonaws.com,my-account.r2.cloudflarestorage.com"} + ) + assert "my-bucket.s3.amazonaws.com" in mod.ALLOWLIST + assert "my-account.r2.cloudflarestorage.com" in mod.ALLOWLIST + + +def test_object_store_hosts_not_in_static_list(): + """Without env var, broad S3/R2 wildcards must NOT be present.""" + mod = _load_allowlist() + assert "*.s3.amazonaws.com" not in mod.ALLOWLIST + assert "*.r2.cloudflarestorage.com" not in mod.ALLOWLIST + + +def test_object_store_hosts_empty_string(): + """Empty env var → only static list, no extra entries.""" + mod = _load_allowlist({"OBJECT_STORE_HOSTS": ""}) + assert "*.s3.amazonaws.com" not in mod.ALLOWLIST + assert "*.r2.cloudflarestorage.com" not in mod.ALLOWLIST + + +def test_object_store_hosts_whitespace_padded(): + """Whitespace around entries must be stripped.""" + mod = _load_allowlist({"OBJECT_STORE_HOSTS": " my-bucket.s3.amazonaws.com , "}) + assert "my-bucket.s3.amazonaws.com" in mod.ALLOWLIST + # Ensure no whitespace-padded entry snuck in + assert all(h == h.strip() for h in mod.ALLOWLIST) + + +def test_object_store_hosts_is_allowed(): + """A host from OBJECT_STORE_HOSTS must pass _is_allowed.""" + mod = _load_allowlist({"OBJECT_STORE_HOSTS": "my-bucket.s3.amazonaws.com"}) + assert mod._is_allowed("my-bucket.s3.amazonaws.com") is True + + +def test_object_store_hosts_other_bucket_blocked(): + """A different bucket must still be blocked even when env var is set.""" + mod = _load_allowlist({"OBJECT_STORE_HOSTS": "my-bucket.s3.amazonaws.com"}) + assert mod._is_allowed("attacker-bucket.s3.amazonaws.com") is False + + +# =========================================================================== +# AllowlistAddon.http_connect +# =========================================================================== + + +def _reset_response_mock(): + """Reset the shared Response.make mock to prevent cross-test contamination.""" + mitmproxy_http_mock.Response.make.reset_mock() + + +def test_http_connect_allowed_no_response(): + """Allowed CONNECT → flow.response must remain unset (pass-through).""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("api.github.com") + addon.http_connect(flow) + assert flow.response is None + mitmproxy_http_mock.Response.make.assert_not_called() + + +def test_http_connect_blocked_sets_403(): + """Blocked CONNECT → flow.response set to 403 text/plain.""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("evil.example.com") + addon.http_connect(flow) + mitmproxy_http_mock.Response.make.assert_called_once_with( + 403, + "Blocked by fro-bot egress allowlist: evil.example.com", + {"Content-Type": "text/plain"}, + ) + # flow.response was assigned (not None) + assert flow.response is not None + + +# =========================================================================== +# AllowlistAddon.request ← proves the plain-HTTP bypass is closed +# =========================================================================== + + +def test_request_allowed_no_response(): + """Allowed plain HTTP request → flow.response must remain unset.""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("api.github.com") + addon.request(flow) + assert flow.response is None + mitmproxy_http_mock.Response.make.assert_not_called() + + +def test_request_blocked_sets_403(): + """Blocked plain HTTP request → flow.response set to 403 text/plain.""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("evil.example.com") + addon.request(flow) + mitmproxy_http_mock.Response.make.assert_called_once_with( + 403, + "Blocked by fro-bot egress allowlist: evil.example.com", + {"Content-Type": "text/plain"}, + ) + assert flow.response is not None + + +def test_request_blocked_does_not_kill_flow(): + """Blocked request must set response, not call flow.kill().""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("evil.example.com") + addon.request(flow) + flow.kill.assert_not_called() + + +def test_request_s3_blocked_without_env_var(): + """Without OBJECT_STORE_HOSTS, any S3 host must be blocked via request hook.""" + _reset_response_mock() + mod = _load_allowlist() + addon = mod.AllowlistAddon() + flow = _make_flow("attacker-bucket.s3.amazonaws.com") + addon.request(flow) + mitmproxy_http_mock.Response.make.assert_called_once() + assert flow.response is not None + + +def test_request_s3_allowed_with_env_var(): + """With OBJECT_STORE_HOSTS set, the configured bucket passes the request hook.""" + _reset_response_mock() + mod = _load_allowlist({"OBJECT_STORE_HOSTS": "my-bucket.s3.amazonaws.com"}) + addon = mod.AllowlistAddon() + flow = _make_flow("my-bucket.s3.amazonaws.com") + addon.request(flow) + assert flow.response is None + mitmproxy_http_mock.Response.make.assert_not_called() + + +# =========================================================================== +# Standalone runner (no pytest required) +# =========================================================================== + +if __name__ == "__main__": + import traceback + + tests = [ + obj for name, obj in sorted(globals().items()) + if name.startswith("test_") and callable(obj) + ] + + passed = 0 + failed = 0 + for t in tests: + try: + t() + print(f" PASS {t.__name__}") + passed += 1 + except Exception: + print(f" FAIL {t.__name__}") + traceback.print_exc() + failed += 1 + + print(f"\n{passed} passed, {failed} failed") + sys.exit(0 if failed == 0 else 1) diff --git a/deploy/validate-stack.sh b/deploy/validate-stack.sh new file mode 100644 index 00000000..ce20c60f --- /dev/null +++ b/deploy/validate-stack.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# validate-stack.sh — Smoke-test the fro-bot compose stack. +# +# Assumes the stack is already running: +# docker compose -f deploy/compose.yaml up -d +# +# Usage (from repo root): +# bash deploy/validate-stack.sh +set -euo pipefail + +COMPOSE_FILE="deploy/compose.yaml" + +echo "==> Validating compose config..." +docker compose -f "${COMPOSE_FILE}" config > /dev/null +echo " OK" + +echo "==> Service status:" +docker compose -f "${COMPOSE_FILE}" ps + +echo "" +echo "==> Recent logs (last 20 lines per service):" +docker compose -f "${COMPOSE_FILE}" logs --tail=20 mitmproxy gateway workspace + +echo "" +echo "==> Checking gateway exit status (last 30s)..." + +# Determine if the gateway container exited non-zero in the last 30 seconds. +GATEWAY_STATE=$(docker compose -f "${COMPOSE_FILE}" ps --format json gateway 2>/dev/null || echo "{}") + +EXIT_CODE=$(echo "${GATEWAY_STATE}" | \ + python3 -c " +import json, sys +data = sys.stdin.read().strip() +if not data or data == '{}': + print('unknown') + sys.exit(0) +# docker compose ps --format json may return a list or single object +try: + obj = json.loads(data) + if isinstance(obj, list): + obj = obj[0] if obj else {} + code = obj.get('ExitCode', -1) + state = obj.get('State', '') + if state == 'exited' and code != 0: + print(str(code)) + else: + print('ok') +except Exception as e: + print('unknown') +" 2>/dev/null || echo "unknown") + +if [[ "${EXIT_CODE}" != "ok" && "${EXIT_CODE}" != "unknown" ]]; then + echo "ERROR: gateway service exited with code ${EXIT_CODE}" >&2 + exit 1 +fi + +echo " Gateway state: ${EXIT_CODE}" +echo "" +echo "==> Stack validation passed." diff --git a/deploy/workspace.Dockerfile b/deploy/workspace.Dockerfile new file mode 100644 index 00000000..cb1c4d02 --- /dev/null +++ b/deploy/workspace.Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +# Workspace agent build is implemented in Unit 7. This Dockerfile is a +# placeholder that builds an idle container so the compose stack composes +# cleanly. +# +# Unit 7 will replace this with a real build that installs: +# - OpenCode CLI pinned to 1.14.41 +# - oMo (oh-my-opencode) pinned to the version in src/shared/constants.ts +# - @fro.bot/systematic plugin (pinned to DEFAULT_SYSTEMATIC_VERSION in constants.ts) +# - mitmproxy CA injected into /usr/local/share/ca-certificates/mitmproxy.crt +# followed by update-ca-certificates so all outbound TLS goes through the proxy + +FROM node:24-alpine + +CMD ["sh", "-c", "echo 'workspace placeholder — Unit 7 will replace this'; sleep infinity"] diff --git a/dist/licenses.txt b/dist/licenses.txt index 50842a0f..b461f602 100644 --- a/dist/licenses.txt +++ b/dist/licenses.txt @@ -8012,6 +8012,1176 @@ MIT Apache-2.0 Apache-2.0 +@discordjs/builders@1.14.1 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Noel Buechler + Copyright 2021 Vlad Frangu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +@discordjs/collection@2.1.1 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Noel Buechler + Copyright 2015 Amish Shah + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +@discordjs/formatters@0.6.2 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 Noel Buechler + Copyright 2021 Vlad Frangu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +@discordjs/rest@2.6.1 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2021 Noel Buechler +Copyright 2021 Vlad Frangu +Copyright 2021 Aura Román + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +@discordjs/util@1.2.0 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 Noel Buechler + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +@discordjs/ws@1.2.3 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 Noel Buechler + Copyright 2022 Denis Cristea + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + @isaacs/cliui@8.0.2 ISC Copyright (c) 2015, Contributors @@ -9125,6 +10295,42 @@ Apache-2.0 of your accepting any such warranty or additional liability. +@sapphire/async-queue@1.5.5 +MIT +MIT + +@sapphire/shapeshift@4.0.0 +MIT +# The MIT License (MIT) + +Copyright © `2021` `The Sapphire Community and its contributors` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +@sapphire/snowflake@3.5.5 +MIT +MIT + @smithy/chunked-blob-reader@5.2.2 Apache-2.0 Apache License @@ -19329,19 +20535,93 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +@standard-schema/spec@1.1.0 +MIT +MIT License + +Copyright (c) 2024 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +@types/node@24.12.2 +MIT + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +@types/ws@8.18.1 +MIT + MIT License + + Copyright (c) Microsoft Corporation. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: - http://www.apache.org/licenses/LICENSE-2.0 + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. @typescript/vfs@1.6.4 MIT @@ -19389,6 +20669,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +@vladfrangu/async_event_emitter@2.4.7 +MIT +# The MIT License (MIT) + +Copyright © `2022` `Vlad Frangu` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + abort-controller@3.0.0 MIT MIT License @@ -20784,16 +22092,246 @@ Apache-2.0 you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +bare-url@2.4.0 +Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +base64-js@1.5.1 +MIT +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. -bare-url@2.4.0 +before-after-hook@4.0.0 Apache-2.0 Apache License Version 2.0, January 2004 @@ -20975,7 +22513,7 @@ Apache-2.0 APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -20983,7 +22521,7 @@ Apache-2.0 same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2018 Gregor Martynus and other contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20998,11 +22536,107 @@ Apache-2.0 limitations under the License. -base64-js@1.5.1 +binary@0.3.0 +MIT +MIT + +bottleneck@2.19.5 MIT The MIT License (MIT) -Copyright (c) 2014 Jameson Little +Copyright (c) 2014 Simon Grondin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +bowser@2.14.1 +MIT +Copyright 2015, Dustin Diaz (the "Original Author") +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Distributions of all or part of the Software intended to be used +by the recipients as they would use the unmodified Software, +containing modifications that substantially alter, remove, or +disable functionality of the Software, outside of the documented +configuration mechanisms provided by the Software, shall be +modified such that the Original Author's bug reporting email +addresses and urls are either replaced with the contact information +of the parties responsible for the changes, or removed entirely. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +Except where noted, this license applies to any and all software +programs and associated documentation files created by the +Original Author, when distributed with the Software. + + +brace-expansion@2.1.0 +MIT +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +buffer@6.0.3 +MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21023,7 +22657,140 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -before-after-hook@4.0.0 +buffer-crc32@1.0.0 +MIT +The MIT License + +Copyright (c) 2013-2024 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +chainsaw@0.1.0 +MIT/X11 +MIT/X11 + +color-convert@2.0.1 +MIT +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +color-name@1.1.4 +MIT +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +compress-commons@6.0.2 +MIT +Copyright (c) 2014 Chris Talkington, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +concat-map@0.0.1 +MIT +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +core-util-is@1.0.3 +MIT +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +crc-32@1.2.2 Apache-2.0 Apache License Version 2.0, January 2004 @@ -21213,7 +22980,7 @@ Apache-2.0 same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018 Gregor Martynus and other contributors. + Copyright (C) 2014-present SheetJS LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21228,40 +22995,9 @@ Apache-2.0 limitations under the License. -binary@0.3.0 -MIT -MIT - -bottleneck@2.19.5 -MIT -The MIT License (MIT) - -Copyright (c) 2014 Simon Grondin - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -bowser@2.14.1 +crc32-stream@6.0.0 MIT -Copyright 2015, Dustin Diaz (the "Original Author") -All rights reserved. - -MIT License +Copyright (c) 2014 Chris Talkington, contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -21275,15 +23011,6 @@ conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -Distributions of all or part of the Software intended to be used -by the recipients as they would use the unmodified Software, -containing modifications that substantially alter, remove, or -disable functionality of the Software, outside of the documented -configuration mechanisms provided by the Software, shall be -modified such that the Original Author's bug reporting email -addresses and urls are either replaced with the contact information -of the parties responsible for the changes, or removed entirely. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -21293,42 +23020,11 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Except where noted, this license applies to any and all software -programs and associated documentation files created by the -Original Author, when distributed with the Software. - - -brace-expansion@2.1.0 -MIT -MIT License - -Copyright (c) 2013 Julian Gruber - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -buffer@6.0.3 +cross-spawn@7.0.6 MIT The MIT License (MIT) -Copyright (c) Feross Aboukhadijeh, and other contributors. +Copyright (c) 2018 Made With MOXY Lda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21349,140 +23045,56 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -buffer-crc32@1.0.0 +debug@4.4.3 MIT -The MIT License +(The MIT License) -Copyright (c) 2013-2024 Brian J. Brennan +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -chainsaw@0.1.0 -MIT/X11 -MIT/X11 - -color-convert@2.0.1 -MIT -Copyright (c) 2011-2016 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - -color-name@1.1.4 -MIT -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -compress-commons@6.0.2 -MIT -Copyright (c) 2014 Chris Talkington, contributors. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. -concat-map@0.0.1 +discord-api-types@0.38.47 MIT -This software is released under the MIT license: - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +MIT License -core-util-is@1.0.3 -MIT -Copyright Node.js contributors. All rights reserved. +Copyright (c) 2020 vladfrangu Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -crc-32@1.2.2 +discord.js@14.26.4 Apache-2.0 Apache License Version 2.0, January 2004 @@ -21661,18 +23273,8 @@ Apache-2.0 END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright (C) 2014-present SheetJS LLC + Copyright 2021 Noel Buechler + Copyright 2015 Amish Shah Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21681,42 +23283,17 @@ Apache-2.0 http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -crc32-stream@6.0.0 -MIT -Copyright (c) 2014 Chris Talkington, contributors. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. -cross-spawn@7.0.6 +effect@3.21.2 MIT -The MIT License (MIT) +MIT License -Copyright (c) 2018 Made With MOXY Lda +Copyright (c) 2023 Effectful Technologies Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21725,40 +23302,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -debug@4.4.3 -MIT -(The MIT License) - -Copyright (c) 2014-2017 TJ Holowaychuk -Copyright (c) 2018-2021 Josh Junon - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the 'Software'), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. emoji-regex@8.0.0 @@ -22042,6 +23595,31 @@ Apache-2.0 limitations under the License. +fast-check@3.23.2 +MIT +MIT License + +Copyright (c) 2017 Nicolas DUBIEN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + fast-content-type-parse@3.0.0 MIT MIT License @@ -22069,6 +23647,31 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +fast-deep-equal@3.1.3 +MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + fast-fifo@1.3.2 MIT The MIT License (MIT) @@ -22536,6 +24139,57 @@ licenses; we recommend you read them, as their terms may differ from the terms above. +lodash.snakecase@4.1.1 +MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + lru-cache@10.4.3 ISC The ISC License @@ -22555,6 +24209,31 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +magic-bytes.js@1.13.0 +MIT +MIT License + +Copyright (c) 2022 Lars Kölpin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + minimatch@9.0.9 ISC The ISC License @@ -22943,6 +24622,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** +pure-rand@6.1.0 +MIT +MIT License + +Copyright (c) 2018 Nicolas DUBIEN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + readable-stream@4.7.0 MIT Node.js is licensed for use as follows: @@ -23723,6 +25427,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +ts-mixer@6.0.4 +MIT +MIT License + +Copyright (c) 2024 Tanner Nielsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + tslib@2.8.1 0BSD Copyright (c) Microsoft Corporation. @@ -23847,6 +25576,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +undici-types@7.16.0 +MIT +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + universal-github-app-jwt@2.2.2 MIT The MIT License @@ -23968,6 +25722,30 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +ws@8.20.1 +MIT +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + xml-naming@0.1.0 MIT MIT diff --git a/docs/plans/2026-04-18-001-feat-fro-bot-gateway-discord-v1-plan.md b/docs/plans/2026-04-18-001-feat-fro-bot-gateway-discord-v1-plan.md index ed9a623e..332f2398 100644 --- a/docs/plans/2026-04-18-001-feat-fro-bot-gateway-discord-v1-plan.md +++ b/docs/plans/2026-04-18-001-feat-fro-bot-gateway-discord-v1-plan.md @@ -603,7 +603,7 @@ The Action and gateway both import from `@fro-bot/runtime` (the name is internal --- -- [ ] **Unit 4: Gateway daemon skeleton + Docker Compose stack** +- [x] **Unit 4: Gateway daemon skeleton + Docker Compose stack** **Goal:** Stand up the gateway container, the workspace container, and the mitmproxy container as a 3-service Compose stack. Gateway connects to Discord, registers slash commands, responds to `@mention` with a minimal "pong". Workspace container boots with mitmproxy CA installed and `http_proxy` env vars set. No agent execution yet — this unit verifies plumbing. diff --git a/packages/gateway/AGENTS.md b/packages/gateway/AGENTS.md new file mode 100644 index 00000000..477c50d5 --- /dev/null +++ b/packages/gateway/AGENTS.md @@ -0,0 +1,63 @@ +# Gateway Package — Agent Notes + +The Discord-first gateway daemon. Wraps `@fro-bot/runtime` with Effect 3.x as the composition layer. + +## Effect / Result<> boundary + +This package is the **only** place in the monorepo that uses `effect`. The Action and the runtime package stay on hand-rolled `Result` from `@bfra.me/es`. + +### Why a boundary + +- The Action runs in a GitHub Actions runner where cold-start time matters. Adding Effect to the runtime bundle would inflate every Action invocation. +- Runtime APIs are stable and well-tested with `Result<>`. Rewriting them is gratuitous churn. +- The gateway has many composing async error paths (Discord webhook handlers, queue dispatch, S3 ops via runtime adapter, approval flow). Effect's `pipe` + `flatMap` + `gen` make those compose cleanly. The runtime doesn't have that density. + +### Where the boundary lives + +`packages/gateway/src/runtime-effect.ts` is the single adapter file. It wraps every `@fro-bot/runtime` function the gateway uses (`acquireLock`, `releaseLock`, `renewLease`, `forceReleaseLock`, `createRun`, `transitionRun`, `findStaleRuns`, `validateProviderSemantics`, S3 sync helpers). + +Each wrapper takes the same shape: + +```ts +Effect.tryPromise(() => runtimeFn(args)) // catches promise rejections + .pipe( + Effect.flatMap(result => + result.success === true + ? Effect.succeed(result.data) + : Effect.fail(result.error), + ), + ) +``` + +All gateway code outside `runtime-effect.ts` works exclusively in Effect — `Effect.Effect` everywhere. Subagents asked to add a new runtime call should add the wrapper to `runtime-effect.ts` first, never import directly from `@fro-bot/runtime` outside that adapter. + +### Effect surface used (Unit 4) + +- **Core** (`Effect.Effect`, `pipe`, `Effect.tryPromise`, `Effect.flatMap`, `Effect.gen`, `Effect.runPromise`, `Effect.try`, `Effect.succeed`, `Effect.fail`, `Effect.either`, `Effect.void`, `Effect.catchAll`) — composing async error paths + +Not used in Unit 4 (planned for Unit 6+): +- **Schedule** (`Schedule.exponential`, `Schedule.recurs`) — retry policies; not yet wired +- **Schema** (`Schema.Struct`, `Schema.decodeUnknown`) — payload validation; not yet wired + +Not used at this scope: +- Effect runtime / Layer / Context (overkill for v1; revisit when DI complexity warrants) +- STM (no shared mutable state at this scope) +- Streams (Discord.js handles its own event stream) + +## Package layout + +- `src/main.ts` — entry point. Wires the Discord client, registers slash commands, installs SIGTERM handler. Runs an `Effect.runPromise` at the top level. +- `src/config.ts` — env + secret reading. `readSecret(name)` checks `${NAME}_FILE` first, falls back to `process.env[name]`. +- `src/runtime-effect.ts` — the Result<>→Effect boundary. +- `src/discord/` — Discord.js integration. Client construction with safe `allowedMentions` defaults, command registry, mention handler. +- `src/shutdown.ts` — SIGTERM handler with 25s drain. + +## Build + +```bash +pnpm --filter @fro-bot/gateway build +pnpm --filter @fro-bot/gateway test +pnpm --filter @fro-bot/gateway lint +``` + +The build runs `tsc --noEmit` for type checking, then `tsdown` to bundle `src/main.ts` into `dist/main.js` (single ESM file). Production deployment runs that bundle inside the container image — see `deploy/gateway.Dockerfile`. diff --git a/packages/gateway/package.json b/packages/gateway/package.json new file mode 100644 index 00000000..7c55bc3b --- /dev/null +++ b/packages/gateway/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fro-bot/gateway", + "version": "0.0.0-development", + "private": true, + "type": "module", + "main": "src/main.ts", + "scripts": { + "build": "pnpm exec tsc -p tsconfig.json --noEmit && pnpm exec tsdown src/main.ts --format esm --out-dir dist", + "check-types": "pnpm exec tsc -p tsconfig.json --noEmit", + "fix": "pnpm --dir ../.. exec eslint --fix packages/gateway/src", + "lint": "pnpm --dir ../.. exec eslint packages/gateway/src", + "test": "pnpm exec vitest run --passWithNoTests src" + }, + "dependencies": { + "@fro-bot/runtime": "workspace:*", + "discord.js": "14.26.4", + "effect": "3.21.2" + } +} diff --git a/packages/gateway/src/config.test.ts b/packages/gateway/src/config.test.ts new file mode 100644 index 00000000..3c0d2f22 --- /dev/null +++ b/packages/gateway/src/config.test.ts @@ -0,0 +1,300 @@ +import type {GatewayConfig} from './config.js' + +import {mkdtempSync, rmSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {afterEach, beforeEach, describe, expect, it} from 'vitest' + +import {loadGatewayConfig, readOptionalSecret, readSecret} from './config.js' + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +let tmpDir: string +let savedEnv: NodeJS.ProcessEnv + +beforeEach(() => { + // #given save and isolate process.env + savedEnv = {...process.env} + // Clear all relevant env vars so tests start clean + for (const key of [ + 'DISCORD_TOKEN', + 'DISCORD_TOKEN_FILE', + 'DISCORD_APPLICATION_ID', + 'DISCORD_APPLICATION_ID_FILE', + 'DISCORD_GUILD_ID', + 'DISCORD_GUILD_ID_FILE', + 'S3_BUCKET', + 'S3_BUCKET_FILE', + 'S3_REGION', + 'S3_REGION_FILE', + 'S3_ENDPOINT', + 'S3_PREFIX', + 'S3_SSE', + 'GATEWAY_IDENTITY', + 'LOG_LEVEL', + 'TOKEN', + 'TOKEN_FILE', + 'MISSING', + 'MISSING_FILE', + ]) { + delete process.env[key] + } + tmpDir = mkdtempSync(join(tmpdir(), 'gateway-config-test-')) +}) + +afterEach(() => { + // Restore original env + process.env = savedEnv + // Clean up temp dir + rmSync(tmpDir, {recursive: true, force: true}) +}) + +// --------------------------------------------------------------------------- +// readSecret +// --------------------------------------------------------------------------- + +describe('readSecret', () => { + it('reads from TOKEN_FILE when that file exists', () => { + // #given a temp file with a secret value + const secretFile = join(tmpDir, 'token.txt') + writeFileSync(secretFile, 'file-secret-value') + process.env.TOKEN_FILE = secretFile + + // #when + const result = readSecret('TOKEN') + + // #then + expect(result).toBe('file-secret-value') + }) + + it('falls back to process.env.TOKEN when TOKEN_FILE is unset', () => { + // #given only the env var is set + process.env.TOKEN = 'env-secret-value' + + // #when + const result = readSecret('TOKEN') + + // #then + expect(result).toBe('env-secret-value') + }) + + it('trims trailing newlines and whitespace from file contents', () => { + // #given a file with trailing whitespace + const secretFile = join(tmpDir, 'token-ws.txt') + writeFileSync(secretFile, 'trimmed-value\n \n') + process.env.TOKEN_FILE = secretFile + + // #when + const result = readSecret('TOKEN') + + // #then + expect(result).toBe('trimmed-value') + }) + + it('throws with a clear message when neither variant is set', () => { + // #given nothing is set for TOKEN + + // #when / #then + expect(() => readSecret('TOKEN')).toThrow( + 'Missing required secret: TOKEN (set TOKEN env var or TOKEN_FILE pointing to a file)', + ) + }) + + it('prefers file content over env var when both are set', () => { + // #given both TOKEN_FILE and TOKEN are set + const secretFile = join(tmpDir, 'token-both.txt') + writeFileSync(secretFile, 'from-file') + process.env.TOKEN_FILE = secretFile + process.env.TOKEN = 'from-env' + + // #when + const result = readSecret('TOKEN') + + // #then file takes precedence + expect(result).toBe('from-file') + }) + + it('falls through to process.env.TOKEN when TOKEN_FILE points to a non-existent file', () => { + // #given TOKEN_FILE points to a missing path, TOKEN is set + process.env.TOKEN_FILE = join(tmpDir, 'does-not-exist.txt') + process.env.TOKEN = 'fallback-env-value' + + // #when + const result = readSecret('TOKEN') + + // #then falls through to env var + expect(result).toBe('fallback-env-value') + }) +}) + +// --------------------------------------------------------------------------- +// readOptionalSecret +// --------------------------------------------------------------------------- + +describe('readOptionalSecret', () => { + it('returns null when neither variant is set', () => { + // #given nothing set for MISSING + + // #when + const result = readOptionalSecret('MISSING') + + // #then + expect(result).toBeNull() + }) + + it('returns file content when MISSING_FILE is set and file exists', () => { + // #given + const secretFile = join(tmpDir, 'optional.txt') + writeFileSync(secretFile, 'optional-value') + process.env.MISSING_FILE = secretFile + + // #when + const result = readOptionalSecret('MISSING') + + // #then + expect(result).toBe('optional-value') + }) +}) + +// --------------------------------------------------------------------------- +// loadGatewayConfig +// --------------------------------------------------------------------------- + +function setRequiredEnv(): void { + process.env.DISCORD_TOKEN = 'test-token' + process.env.DISCORD_APPLICATION_ID = 'test-app-id' + process.env.S3_BUCKET = 'test-bucket' + process.env.S3_REGION = 'us-east-1' +} + +describe('loadGatewayConfig', () => { + it('returns valid config when all required env vars are set', () => { + // #given + setRequiredEnv() + + // #when + const config: GatewayConfig = loadGatewayConfig() + + // #then + expect(config.discordToken).toBe('test-token') + expect(config.discordApplicationId).toBe('test-app-id') + expect(config.discordGuildId).toBeNull() + expect(config.objectStore.bucket).toBe('test-bucket') + expect(config.objectStore.region).toBe('us-east-1') + expect(config.objectStore.enabled).toBe(true) + }) + + it('uses default identity "discord-gateway" when GATEWAY_IDENTITY is unset', () => { + // #given + setRequiredEnv() + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.identity).toBe('discord-gateway') + }) + + it('uses provided GATEWAY_IDENTITY when set', () => { + // #given + setRequiredEnv() + process.env.GATEWAY_IDENTITY = 'my-custom-gateway' + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.identity).toBe('my-custom-gateway') + }) + + it('uses default log level "info" when LOG_LEVEL is unset', () => { + // #given + setRequiredEnv() + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.logLevel).toBe('info') + }) + + it('uses default S3 prefix "fro-bot-state" when S3_PREFIX is unset', () => { + // #given + setRequiredEnv() + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.objectStore.prefix).toBe('fro-bot-state') + }) + + it('throws with a clear message when DISCORD_TOKEN is missing', () => { + // #given only partial env + process.env.DISCORD_APPLICATION_ID = 'test-app-id' + process.env.S3_BUCKET = 'test-bucket' + process.env.S3_REGION = 'us-east-1' + + // #when / #then + expect(() => loadGatewayConfig()).toThrow('Missing required secret: DISCORD_TOKEN') + }) + + it('throws with a clear message when S3_BUCKET is missing', () => { + // #given + process.env.DISCORD_TOKEN = 'test-token' + process.env.DISCORD_APPLICATION_ID = 'test-app-id' + process.env.S3_REGION = 'us-east-1' + + // #when / #then + expect(() => loadGatewayConfig()).toThrow('Missing required secret: S3_BUCKET') + }) + + it('throws with a clear message for invalid LOG_LEVEL', () => { + // #given + setRequiredEnv() + process.env.LOG_LEVEL = 'verbose' + + // #when / #then + expect(() => loadGatewayConfig()).toThrow( + 'Invalid LOG_LEVEL value: "verbose" (valid values: debug, info, warn, error)', + ) + }) + + it('includes DISCORD_GUILD_ID when set', () => { + // #given + setRequiredEnv() + process.env.DISCORD_GUILD_ID = '123456789' + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.discordGuildId).toBe('123456789') + }) + + it('includes S3_ENDPOINT in objectStore when set', () => { + // #given + setRequiredEnv() + process.env.S3_ENDPOINT = 'https://my-minio.example.com' + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.objectStore.endpoint).toBe('https://my-minio.example.com') + }) + + it('omits S3_ENDPOINT from objectStore when not set', () => { + // #given + setRequiredEnv() + + // #when + const config = loadGatewayConfig() + + // #then + expect(config.objectStore.endpoint).toBeUndefined() + }) +}) diff --git a/packages/gateway/src/config.ts b/packages/gateway/src/config.ts new file mode 100644 index 00000000..7344de9e --- /dev/null +++ b/packages/gateway/src/config.ts @@ -0,0 +1,96 @@ +import type {ObjectStoreConfig} from '@fro-bot/runtime' + +import {existsSync, readFileSync} from 'node:fs' +import process from 'node:process' + +const DEFAULT_S3_PREFIX = 'fro-bot-state' +const DEFAULT_GATEWAY_IDENTITY = 'discord-gateway' +const DEFAULT_LOG_LEVEL = 'info' as const +const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const + +export interface GatewayConfig { + readonly discordToken: string + readonly discordApplicationId: string + readonly discordGuildId: string | null + readonly objectStore: ObjectStoreConfig + readonly identity: string + readonly logLevel: 'debug' | 'info' | 'warn' | 'error' +} + +/** + * Read a required secret by name. + * + * Precedence: + * 1. If `${name}_FILE` env var is set AND that file exists → read file contents, trim trailing whitespace + * 2. Else if `process.env[name]` is set → return it + * 3. Else throw with a clear message + */ +export function readSecret(name: string): string { + const value = readOptionalSecret(name) + if (value === null) { + throw new Error(`Missing required secret: ${name} (set ${name} env var or ${name}_FILE pointing to a file)`) + } + return value +} + +/** + * Read an optional secret by name. + * + * Same precedence as `readSecret` but returns `null` instead of throwing. + */ +export function readOptionalSecret(name: string): string | null { + const filePath = process.env[`${name}_FILE`] + if (filePath !== undefined && existsSync(filePath)) { + return readFileSync(filePath, 'utf8').trimEnd() + } + + const value = process.env[name] + if (value !== undefined) { + return value + } + + return null +} + +/** + * Load and validate the gateway configuration from environment variables and secrets. + * + * Throws if any required secret is missing or if a value fails validation. + */ +export function loadGatewayConfig(): GatewayConfig { + const discordToken = readSecret('DISCORD_TOKEN') + const discordApplicationId = readSecret('DISCORD_APPLICATION_ID') + const discordGuildId = readOptionalSecret('DISCORD_GUILD_ID') + + const s3Bucket = readSecret('S3_BUCKET') + const s3Region = readSecret('S3_REGION') + const s3Endpoint = readOptionalSecret('S3_ENDPOINT') ?? undefined + const s3Prefix = readOptionalSecret('S3_PREFIX') ?? DEFAULT_S3_PREFIX + const s3Sse = readOptionalSecret('S3_SSE') ?? undefined + + const identity = readOptionalSecret('GATEWAY_IDENTITY') ?? DEFAULT_GATEWAY_IDENTITY + + const rawLogLevel = readOptionalSecret('LOG_LEVEL') ?? DEFAULT_LOG_LEVEL + if (!(VALID_LOG_LEVELS as readonly string[]).includes(rawLogLevel)) { + throw new Error(`Invalid LOG_LEVEL value: "${rawLogLevel}" (valid values: ${VALID_LOG_LEVELS.join(', ')})`) + } + const logLevel = rawLogLevel as GatewayConfig['logLevel'] + + const objectStore: ObjectStoreConfig = { + enabled: true, + bucket: s3Bucket, + region: s3Region, + prefix: s3Prefix, + ...(s3Endpoint === undefined ? {} : {endpoint: s3Endpoint}), + ...(s3Sse === undefined ? {} : {sseEncryption: s3Sse as ObjectStoreConfig['sseEncryption']}), + } + + return { + discordToken, + discordApplicationId, + discordGuildId, + objectStore, + identity, + logLevel, + } +} diff --git a/packages/gateway/src/discord/client.test.ts b/packages/gateway/src/discord/client.test.ts new file mode 100644 index 00000000..895d9553 --- /dev/null +++ b/packages/gateway/src/discord/client.test.ts @@ -0,0 +1,65 @@ +import type {EventEmitter} from 'node:events' + +import {GatewayIntentBits} from 'discord.js' +import {describe, expect, it, vi} from 'vitest' + +import {createDiscordClient, DEFAULT_INTENTS} from './client.js' + +// discord.js Client constructor makes no network calls — safe to instantiate in tests. + +describe('createDiscordClient', () => { + it('returns a Client with allowedMentions locked to users-only', () => { + // #when the client is created + const client = createDiscordClient() + + // #then allowedMentions prevents @everyone / @here + expect(client.options.allowedMentions).toEqual({parse: ['users'], repliedUser: false}) + }) + + it('default intents include MessageContent (required to read mention text)', () => { + // #when + const client = createDiscordClient() + + // #then + const intents = client.options.intents + // discord.js stores intents as a BitField; check via DEFAULT_INTENTS constant + expect(DEFAULT_INTENTS).toContain(GatewayIntentBits.MessageContent) + expect(intents).toBeDefined() + }) + + it('optional intent override merges with defaults (dedup via Set)', () => { + // #given a custom intent list including one not in defaults + const customIntents = [GatewayIntentBits.DirectMessages, GatewayIntentBits.Guilds] // Guilds is already default + + // #when + const client = createDiscordClient({intents: customIntents}) + + // #then the BitField is the union of defaults + extras (no duplicates) + const expected = [...new Set([...DEFAULT_INTENTS, ...customIntents])] + const expectedBitfield = new ( + client.options.intents as unknown as {constructor: new (bits: GatewayIntentBits[]) => {bitfield: number}} + ).constructor(expected).bitfield + expect((client.options.intents as unknown as {bitfield: number}).bitfield).toBe(expectedBitfield) + }) + + it('wires shard events to logger when logger is provided', () => { + // #given a mock logger + const logger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } + + // #when + const client = createDiscordClient({logger}) + + // #then shard events emit log calls + const emitter = client as unknown as EventEmitter + emitter.emit('shardReady', 0) + expect(logger.info).toHaveBeenCalledWith({shardId: 0}, 'discord shard ready') + + emitter.emit('shardReconnecting', 0) + expect(logger.info).toHaveBeenCalledWith({shardId: 0}, 'discord shard reconnecting') + }) +}) diff --git a/packages/gateway/src/discord/client.ts b/packages/gateway/src/discord/client.ts new file mode 100644 index 00000000..420bf020 --- /dev/null +++ b/packages/gateway/src/discord/client.ts @@ -0,0 +1,68 @@ +import {Client, GatewayIntentBits} from 'discord.js' + +export interface GatewayLogger { + readonly debug: (context: Record, message: string) => void + readonly info: (context: Record, message: string) => void + readonly warn: (context: Record, message: string) => void + readonly error: (context: Record, message: string) => void +} + +const DEFAULT_INTENTS: GatewayIntentBits[] = [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, +] + +export interface DiscordClientOptions { + readonly intents?: GatewayIntentBits[] + readonly logger?: GatewayLogger +} + +/** + * Create a Discord.js Client with safe defaults. + * + * - `allowedMentions` is locked to `{ parse: ['users'], repliedUser: false }` to prevent + * accidental @everyone / @here pings. + * - Shard lifecycle events are wired to structured log lines when a logger is provided. + * - Does NOT call `client.login()` — the caller (main.ts) is responsible for that. + */ +export function createDiscordClient(options: DiscordClientOptions = {}): Client { + // Merge caller's intents with defaults — dedupe via Set since intents are numeric bitfield values. + const intents = + options.intents === undefined + ? DEFAULT_INTENTS + : [...new Set([...DEFAULT_INTENTS, ...options.intents])] + const logger = options.logger + + const client = new Client({ + intents, + allowedMentions: {parse: ['users'], repliedUser: false}, + }) + + if (logger) { + client.on('shardReady', (shardId: number) => { + logger.info({shardId}, 'discord shard ready') + }) + + client.on('shardDisconnect', (event, shardId: number) => { + logger.warn({shardId, code: event.code, reason: event.reason}, 'discord shard disconnected') + }) + + client.on('shardReconnecting', (shardId: number) => { + logger.info({shardId}, 'discord shard reconnecting') + }) + + client.on('shardError', (error: Error, shardId: number) => { + logger.error({shardId, err: error}, 'discord shard error') + }) + + client.on('shardResume', (shardId: number, replayedEvents: number) => { + logger.info({shardId, replayedEvents}, 'discord shard resumed') + }) + } + + return client +} + +export {DEFAULT_INTENTS} diff --git a/packages/gateway/src/discord/commands/index.test.ts b/packages/gateway/src/discord/commands/index.test.ts new file mode 100644 index 00000000..33feef14 --- /dev/null +++ b/packages/gateway/src/discord/commands/index.test.ts @@ -0,0 +1,87 @@ +import type {ChatInputCommandInteraction} from 'discord.js' +import {Effect} from 'effect' +import {describe, expect, it, vi} from 'vitest' + +import {dispatchCommand, getCommandRegistry} from './index.js' + +describe('getCommandRegistry', () => { + it('includes the ping command', () => { + // #given / #when + const registry = getCommandRegistry() + + // #then + const ping = registry.find(c => c.data.name === 'fro-bot') + expect(ping).toBeDefined() + }) +}) + +describe('dispatchCommand', () => { + it('routes to the matching command and runs it', async () => { + // #given a registry with a mock command + const execute = vi.fn().mockReturnValue(Effect.void) + const registry = [ + { + data: {name: 'test-cmd'} as unknown as import('discord.js').SlashCommandBuilder, + execute, + }, + ] + const interaction = {commandName: 'test-cmd'} as unknown as ChatInputCommandInteraction + + // #when + await Effect.runPromise(dispatchCommand(interaction, registry)) + + // #then + expect(execute).toHaveBeenCalledExactlyOnceWith(interaction) + }) + + it('returns Effect.fail on unknown command name with clear error message AND replies ephemerally', async () => { + // #given a registry that does not contain the requested command + const reply = vi.fn().mockResolvedValue(undefined) + const registry = getCommandRegistry() + const interaction = {commandName: 'nonexistent', reply} as unknown as ChatInputCommandInteraction + + // #when + const result = await Effect.runPromise(Effect.either(dispatchCommand(interaction, registry))) + + // #then — the Effect still fails with a clear error + expect(result._tag).toBe('Left') + expect((result as {_tag: 'Left'; left: unknown}).left).toBeInstanceOf(Error) + expect(((result as {_tag: 'Left'; left: unknown}).left as Error).message).toContain('nonexistent') + // #and — Discord receives an ephemeral acknowledgement within the 3-second window + // so the user sees an actual response instead of "This interaction failed" + const contentMatcher: unknown = expect.stringContaining('nonexistent') + expect(reply).toHaveBeenCalledExactlyOnceWith({content: contentMatcher, ephemeral: true}) + }) + + it('still fails with the original error when the ephemeral ack itself fails', async () => { + // #given a reply() that rejects (e.g. interaction token already expired) + const reply = vi.fn().mockRejectedValue(new Error('Interaction has already been acknowledged')) + const registry = getCommandRegistry() + const interaction = {commandName: 'nonexistent', reply} as unknown as ChatInputCommandInteraction + + // #when + const result = await Effect.runPromise(Effect.either(dispatchCommand(interaction, registry))) + + // #then — the original "unknown command" error wins, not "ack-failed" + expect(result._tag).toBe('Left') + expect(((result as {_tag: 'Left'; left: unknown}).left as Error).message).toContain('nonexistent') + expect(((result as {_tag: 'Left'; left: unknown}).left as Error).message).not.toContain('ack-failed') + expect(reply).toHaveBeenCalledOnce() + }) + + it('dispatches the real ping command successfully', async () => { + // #given + const reply = vi.fn().mockResolvedValue(undefined) + const interaction = { + commandName: 'fro-bot', + reply, + } as unknown as ChatInputCommandInteraction + const registry = getCommandRegistry() + + // #when + await Effect.runPromise(dispatchCommand(interaction, registry)) + + // #then + expect(reply).toHaveBeenCalledWith({content: 'pong', ephemeral: true}) + }) +}) diff --git a/packages/gateway/src/discord/commands/index.ts b/packages/gateway/src/discord/commands/index.ts new file mode 100644 index 00000000..32967597 --- /dev/null +++ b/packages/gateway/src/discord/commands/index.ts @@ -0,0 +1,84 @@ +import type {ChatInputCommandInteraction, SlashCommandBuilder} from 'discord.js' +import {REST, Routes} from 'discord.js' +import {Effect} from 'effect' + +import pingCommand from './ping.js' + +export interface SlashCommand { + readonly data: SlashCommandBuilder + readonly execute: (interaction: ChatInputCommandInteraction) => Effect.Effect +} + +/** + * Returns the full registry of registered slash commands. + */ +export function getCommandRegistry(): SlashCommand[] { + return [pingCommand] +} + +/** + * Find the matching command in the registry and run it. + * + * When the command name is not in the registry, the interaction is + * acknowledged with an ephemeral reply BEFORE the Effect fails. Discord + * gives interaction tokens a 3-second response window; without an ack the + * user sees "This interaction failed" even though our handler logged the + * error correctly. Common cause: a stale global command that was removed + * from the registry but still exists in Discord, or an interaction from + * another bot in the same guild reaching this dispatcher. + * + * If the ack itself fails (e.g. token already expired) we swallow that + * inner error so the outer caller still sees the original "unknown + * command" failure, not a misleading reply-failed message. + */ +export function dispatchCommand( + interaction: ChatInputCommandInteraction, + registry: SlashCommand[], +): Effect.Effect { + const commandName = interaction.commandName + const command = registry.find(c => c.data.name === commandName) + + if (command === undefined) { + return Effect.gen(function* () { + yield* Effect.tryPromise({ + try: async () => + interaction.reply({ + content: `Unknown command: \`${commandName}\``, + ephemeral: true, + }), + catch: () => new Error('ack-failed'), + }).pipe(Effect.catchAll(() => Effect.void)) + return yield* Effect.fail(new Error(`Unknown command: ${commandName}`)) + }) + } + + return command.execute(interaction) +} + +/** + * Register slash commands via Discord REST API. + * + * `token` must be passed explicitly. The client's `.token` field is null + * until `client.login()` resolves, and slash-command registration runs + * BEFORE login in the gateway boot sequence — passing the token as a plain + * parameter avoids coupling this module to a stale third-party-private + * field on the Client object. + * + * - Guild-scoped when `guildId` is provided (instant propagation, good for dev). + * - Global when `guildId` is null (up to 1h propagation, for production). + */ +export async function registerSlashCommands( + token: string, + applicationId: string, + guildId: string | null, + registry: SlashCommand[], +): Promise { + const rest = new REST().setToken(token) + const body = registry.map(cmd => cmd.data.toJSON()) + + if (guildId === null) { + await rest.put(Routes.applicationCommands(applicationId), {body}) + } else { + await rest.put(Routes.applicationGuildCommands(applicationId, guildId), {body}) + } +} diff --git a/packages/gateway/src/discord/commands/ping.test.ts b/packages/gateway/src/discord/commands/ping.test.ts new file mode 100644 index 00000000..476b30e8 --- /dev/null +++ b/packages/gateway/src/discord/commands/ping.test.ts @@ -0,0 +1,44 @@ +import type {ChatInputCommandInteraction} from 'discord.js' +import {Effect} from 'effect' +import {describe, expect, it, vi} from 'vitest' + +import pingCommand from './ping.js' + +describe('ping command', () => { + it('calls interaction.reply with content "pong" and ephemeral: true', async () => { + // #given a mock interaction + const reply = vi.fn().mockResolvedValue(undefined) + const interaction = {reply} as unknown as ChatInputCommandInteraction + + // #when the command is executed + await Effect.runPromise(pingCommand.execute(interaction)) + + // #then reply was called with the correct args + expect(reply).toHaveBeenCalledExactlyOnceWith({content: 'pong', ephemeral: true}) + }) + + it('returns Effect.fail when reply throws', async () => { + // #given a failing interaction + const reply = vi.fn().mockRejectedValue(new Error('Discord API error')) + const interaction = {reply} as unknown as ChatInputCommandInteraction + + // #when + const result = await Effect.runPromise(Effect.either(pingCommand.execute(interaction))) + + // #then + expect(result._tag).toBe('Left') + expect((result as {_tag: 'Left'; left: unknown}).left).toBeInstanceOf(Error) + expect(((result as {_tag: 'Left'; left: unknown}).left as Error).message).toBe('Discord API error') + }) + + it('has command name "fro-bot" with subcommand "ping"', () => { + // #given / #when + const json = pingCommand.data.toJSON() + + // #then + expect(json.name).toBe('fro-bot') + expect(json.options).toBeDefined() + const sub = json.options?.find((o: {name: string}) => o.name === 'ping') + expect(sub).toBeDefined() + }) +}) diff --git a/packages/gateway/src/discord/commands/ping.ts b/packages/gateway/src/discord/commands/ping.ts new file mode 100644 index 00000000..dc5fb324 --- /dev/null +++ b/packages/gateway/src/discord/commands/ping.ts @@ -0,0 +1,24 @@ +import type {ChatInputCommandInteraction} from 'discord.js' +import type {SlashCommand} from './index.js' + +import {SlashCommandBuilder} from 'discord.js' +import {Effect} from 'effect' + +/** + * `/fro-bot ping` — smoke-test command. + * Responds with an ephemeral "pong" reply. + */ +const pingCommand: SlashCommand = { + data: new SlashCommandBuilder() + .setName('fro-bot') + .setDescription('fro-bot commands') + .addSubcommand(sub => sub.setName('ping').setDescription('Check if fro-bot is alive')) as SlashCommandBuilder, + + execute: (interaction: ChatInputCommandInteraction): Effect.Effect => + Effect.tryPromise({ + try: async () => interaction.reply({content: 'pong', ephemeral: true}), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.asVoid), +} + +export default pingCommand diff --git a/packages/gateway/src/discord/mentions.test.ts b/packages/gateway/src/discord/mentions.test.ts new file mode 100644 index 00000000..762f276c --- /dev/null +++ b/packages/gateway/src/discord/mentions.test.ts @@ -0,0 +1,66 @@ +import type {Message, TextChannel, ThreadChannel} from 'discord.js' +import {Effect} from 'effect' +import {describe, expect, it, vi} from 'vitest' + +const BOT_USER_ID = 'bot-user-123' + +function makeMessage( + overrides: Partial<{isThread: boolean; mentionsBot: boolean; sendFn: ReturnType}>, +): Message { + const {isThread = false, mentionsBot = true, sendFn = vi.fn().mockResolvedValue(undefined)} = overrides + + const startThread = vi.fn().mockResolvedValue({send: sendFn} as unknown as ThreadChannel) + + return { + channel: { + isThread: () => isThread, + } as unknown as TextChannel, + mentions: { + has: (id: string) => mentionsBot && id === BOT_USER_ID, + }, + startThread, + _startThread: startThread, // expose for assertions + } as unknown as Message +} + +describe('handleMention', () => { + it('creates a thread and replies "pong" when bot is mentioned in a non-thread channel', async () => { + // #given + const {handleMention} = await import('./mentions.js') + const sendFn = vi.fn().mockResolvedValue(undefined) + const message = makeMessage({isThread: false, mentionsBot: true, sendFn}) + + // #when + await Effect.runPromise(handleMention(message, BOT_USER_ID)) + + // #then + expect((message as unknown as {_startThread: ReturnType})._startThread).toHaveBeenCalledWith({ + name: 'fro-bot session', + }) + expect(sendFn).toHaveBeenCalledWith('pong') + }) + + it('skips when message is already in a thread', async () => { + // #given + const {handleMention} = await import('./mentions.js') + const message = makeMessage({isThread: true, mentionsBot: true}) + + // #when + await Effect.runPromise(handleMention(message, BOT_USER_ID)) + + // #then — no thread created + expect((message as unknown as {_startThread: ReturnType})._startThread).not.toHaveBeenCalled() + }) + + it('skips when bot is not actually mentioned (reply-chain only)', async () => { + // #given + const {handleMention} = await import('./mentions.js') + const message = makeMessage({isThread: false, mentionsBot: false}) + + // #when + await Effect.runPromise(handleMention(message, BOT_USER_ID)) + + // #then — no thread created + expect((message as unknown as {_startThread: ReturnType})._startThread).not.toHaveBeenCalled() + }) +}) diff --git a/packages/gateway/src/discord/mentions.ts b/packages/gateway/src/discord/mentions.ts new file mode 100644 index 00000000..65877251 --- /dev/null +++ b/packages/gateway/src/discord/mentions.ts @@ -0,0 +1,34 @@ +import type {Message} from 'discord.js' + +import {Effect} from 'effect' + +/** + * Handle a direct `@fro-bot` mention in a guild channel. + * + * Behaviour: + * - If the message is already inside a thread → skip (log and return). + * - If the bot user is not actually mentioned (e.g. reply-chain only) → skip. + * - Otherwise: create a thread on the message and reply "pong" inside it. + * + * Thread naming is intentionally minimal for v1 ("fro-bot session"). + * Proper session-aware naming arrives in Unit 6. + */ +export function handleMention(message: Message, botUserId: string): Effect.Effect { + // Skip if already in a thread + if (message.channel.isThread()) { + return Effect.void + } + + // Skip if bot is not actually mentioned (e.g. reply-chain only) + if (!message.mentions.has(botUserId)) { + return Effect.void + } + + return Effect.tryPromise({ + try: async () => { + const thread = await message.startThread({name: 'fro-bot session'}) + await thread.send('pong') + }, + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) +} diff --git a/packages/gateway/src/main.ts b/packages/gateway/src/main.ts new file mode 100644 index 00000000..b236a331 --- /dev/null +++ b/packages/gateway/src/main.ts @@ -0,0 +1,111 @@ +import type {ChatInputCommandInteraction, Message} from 'discord.js' +import type {GatewayLogger} from './discord/client.js' + +import process from 'node:process' + +import {Effect} from 'effect' + +import {loadGatewayConfig} from './config.js' +import {createDiscordClient} from './discord/client.js' +import {dispatchCommand, getCommandRegistry, registerSlashCommands} from './discord/commands/index.js' +import {handleMention} from './discord/mentions.js' +import {installShutdownHandlers} from './shutdown.js' + +// --------------------------------------------------------------------------- +// Minimal structured logger — pino can replace this in a later unit. +// warn and error use the lint-permitted console channels directly. +// debug and info use console.log with scoped eslint-disable because they are +// informational, not warnings — routing them through console.warn would +// poison log-aggregator severity classification. +// --------------------------------------------------------------------------- + +function makeLogger(level: 'debug' | 'info' | 'warn' | 'error'): GatewayLogger { + const levels = {debug: 0, info: 1, warn: 2, error: 3} as const + const minLevel = levels[level] + + return { + debug: (ctx, msg) => { + // eslint-disable-next-line no-console + if (minLevel <= levels.debug) console.log(JSON.stringify({level: 'debug', ...ctx, msg})) + }, + info: (ctx, msg) => { + // eslint-disable-next-line no-console + if (minLevel <= levels.info) console.log(JSON.stringify({level: 'info', ...ctx, msg})) + }, + warn: (ctx, msg) => { + if (minLevel <= levels.warn) console.warn(JSON.stringify({level: 'warn', ...ctx, msg})) + }, + error: (ctx, msg) => { + if (minLevel <= levels.error) console.error(JSON.stringify({level: 'error', ...ctx, msg})) + }, + } +} + +// --------------------------------------------------------------------------- +// Main Effect program +// --------------------------------------------------------------------------- + +const program = Effect.gen(function* () { + // a. Load config + const config = yield* Effect.try({ + try: () => loadGatewayConfig(), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + + // b. Create logger + const logger = makeLogger(config.logLevel) + + // c. Create Discord client + const client = createDiscordClient({logger}) + + // d. Build command registry + const registry = getCommandRegistry() + + // e. Register slash commands + yield* Effect.tryPromise({ + try: async () => + registerSlashCommands(config.discordToken, config.discordApplicationId, config.discordGuildId, registry), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + + // f. Wire client events + client.on('interactionCreate', interaction => { + if (!interaction.isChatInputCommand()) return + // isChatInputCommand() narrows to ChatInputCommandInteraction — cast is safe. + const cmd = interaction as unknown as ChatInputCommandInteraction + Effect.runPromise(dispatchCommand(cmd, registry)).catch((error: unknown) => { + logger.error({err: error, commandName: cmd.commandName}, 'command dispatch failed') + }) + }) + + client.on('messageCreate', (message: Message) => { + if (message.author.bot) return + if (client.user === null) return + if (!message.mentions.has(client.user.id)) return + Effect.runPromise(handleMention(message, client.user.id)).catch((error: unknown) => { + logger.error({err: error}, 'mention handler failed') + }) + }) + + // g. Install shutdown handlers + installShutdownHandlers(client, logger) + + // h. Login + yield* Effect.tryPromise({ + try: async () => client.login(config.discordToken), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + + // i. Log startup + logger.info({applicationId: config.discordApplicationId}, 'gateway started') +}) + +// --------------------------------------------------------------------------- +// Top-level runner — logger may not exist yet if config load fails, so we +// fall back to console.error for the startup-failure path. +// --------------------------------------------------------------------------- + +Effect.runPromise(program).catch((error: unknown) => { + console.error(JSON.stringify({level: 'error', err: String(error), msg: 'gateway startup failed'})) + process.exit(1) +}) diff --git a/packages/gateway/src/runtime-effect.test.ts b/packages/gateway/src/runtime-effect.test.ts new file mode 100644 index 00000000..d87c5046 --- /dev/null +++ b/packages/gateway/src/runtime-effect.test.ts @@ -0,0 +1,500 @@ +/** + * Tests for runtime-effect.ts — Effect adapters over @fro-bot/runtime functions. + * + * Strategy: vi.mock('@fro-bot/runtime') so we never touch real S3/coordination + * infrastructure. Each wrapper is tested for: + * 1. Happy path — Result{success:true} resolves the Effect to data + * 2. Error path — Result{success:false} fails the Effect with the error + * 3. Promise rejection — thrown error is caught and wrapped as Effect failure + * + * S3 sync helpers (plain Promise, no Result tag) are tested for (1) and (3) only. + * + * We use Effect.runPromiseExit to inspect Exit without throwing. + */ + +import { + acquireLock, + createRun, + findStaleRuns, + forceReleaseLock, + releaseLock, + renewLease, + syncArtifactsToStore, + syncMetadataToStore, + syncSessionsFromStore, + syncSessionsToStore, + transitionRun, + validateProviderSemantics, +} from '@fro-bot/runtime' +import {Effect, Exit} from 'effect' +import {beforeEach, describe, expect, it, vi} from 'vitest' + +import { + acquireLockEffect, + createRunEffect, + findStaleRunsEffect, + forceReleaseLockEffect, + releaseLockEffect, + renewLeaseEffect, + syncArtifactsToStoreEffect, + syncMetadataToStoreEffect, + syncSessionsFromStoreEffect, + syncSessionsToStoreEffect, + transitionRunEffect, + validateProviderSemanticsEffect, +} from './runtime-effect.js' + +// vi.mock is hoisted by vitest to the top of the module at runtime, +// so this intercepts @fro-bot/runtime before any test code runs. +vi.mock('@fro-bot/runtime', () => ({ + acquireLock: vi.fn(), + createRun: vi.fn(), + findStaleRuns: vi.fn(), + forceReleaseLock: vi.fn(), + releaseLock: vi.fn(), + renewLease: vi.fn(), + syncArtifactsToStore: vi.fn(), + syncMetadataToStore: vi.fn(), + syncSessionsFromStore: vi.fn(), + syncSessionsToStore: vi.fn(), + transitionRun: vi.fn(), + validateProviderSemantics: vi.fn(), +})) + +// --------------------------------------------------------------------------- +// Shared fixtures +// --------------------------------------------------------------------------- + +const noop = () => {} +const logger = {debug: noop, info: noop, warning: noop, error: noop} +const coordLogger = {debug: noop} + +// Minimal CoordinationConfig stub — only shape matters for mocked calls +const config = {} as Parameters[0] +const adapter = {} as Parameters[0] +const storeConfig = {} as Parameters[1] + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function ok(data: T) { + return {success: true as const, data} +} + +function err(error: Error) { + return {success: false as const, error} +} + +// --------------------------------------------------------------------------- +// acquireLockEffect +// --------------------------------------------------------------------------- + +describe('acquireLockEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + // #given underlying returns success + // #when Effect runs + // #then resolves to data + it('resolves to LockAcquisitionResult on success', async () => { + const data = {acquired: true, etag: 'abc', holder: null} + vi.mocked(acquireLock).mockResolvedValue(ok(data)) + + const exit = await Effect.runPromiseExit( + acquireLockEffect(config, 'repo', 'holder', 'discord', 'run-1', coordLogger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: data}) + }) + + // #given underlying returns failure Result + // #when Effect runs + // #then fails with the error + it('fails with error when Result is failure', async () => { + const error = new Error('lock conflict') + vi.mocked(acquireLock).mockResolvedValue(err(error)) + + const exit = await Effect.runPromiseExit( + acquireLockEffect(config, 'repo', 'holder', 'discord', 'run-1', coordLogger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + // #given underlying throws + // #when Effect runs + // #then fails with wrapped error + it('fails when underlying function throws', async () => { + vi.mocked(acquireLock).mockRejectedValue(new Error('network error')) + + const exit = await Effect.runPromiseExit( + acquireLockEffect(config, 'repo', 'holder', 'discord', 'run-1', coordLogger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// releaseLockEffect +// --------------------------------------------------------------------------- + +describe('releaseLockEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to void on success', async () => { + vi.mocked(releaseLock).mockResolvedValue(ok(undefined)) + + const exit = await Effect.runPromiseExit(releaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success'}) + }) + + it('fails with error when Result is failure', async () => { + const error = new Error('delete failed') + vi.mocked(releaseLock).mockResolvedValue(err(error)) + + const exit = await Effect.runPromiseExit(releaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(releaseLock).mockRejectedValue(new Error('boom')) + + const exit = await Effect.runPromiseExit(releaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// renewLeaseEffect +// --------------------------------------------------------------------------- + +describe('renewLeaseEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + const lockRecord = { + repo: 'repo', + holder_id: 'holder', + surface: 'discord' as const, + acquired_at: new Date().toISOString(), + ttl_seconds: 60, + run_id: 'run-1', + } + + it('resolves to {etag} on success', async () => { + vi.mocked(renewLease).mockResolvedValue(ok({etag: 'new-etag'})) + + const exit = await Effect.runPromiseExit(renewLeaseEffect(config, 'repo', lockRecord, 'old-etag', coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success', value: {etag: 'new-etag'}}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(renewLease).mockResolvedValue(err(new Error('precondition failed'))) + + const exit = await Effect.runPromiseExit(renewLeaseEffect(config, 'repo', lockRecord, 'old-etag', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(renewLease).mockRejectedValue('string error') + + const exit = await Effect.runPromiseExit(renewLeaseEffect(config, 'repo', lockRecord, 'old-etag', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// forceReleaseLockEffect +// --------------------------------------------------------------------------- + +describe('forceReleaseLockEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to void on success', async () => { + vi.mocked(forceReleaseLock).mockResolvedValue(ok(undefined)) + + const exit = await Effect.runPromiseExit(forceReleaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success'}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(forceReleaseLock).mockResolvedValue(err(new Error('force delete failed'))) + + const exit = await Effect.runPromiseExit(forceReleaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(forceReleaseLock).mockRejectedValue(new Error('boom')) + + const exit = await Effect.runPromiseExit(forceReleaseLockEffect(config, 'repo', 'etag-1', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// createRunEffect +// --------------------------------------------------------------------------- + +describe('createRunEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + const runState = { + run_id: 'run-1', + surface: 'discord' as const, + thread_id: 'thread-1', + entity_ref: 'ref-1', + phase: 'PENDING' as const, + started_at: new Date().toISOString(), + last_heartbeat: new Date().toISOString(), + holder_id: 'holder', + details: {}, + } + + it('resolves to {etag} on success', async () => { + vi.mocked(createRun).mockResolvedValue(ok({etag: 'etag-1'})) + + const exit = await Effect.runPromiseExit(createRunEffect(config, 'identity', 'repo', runState, coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success', value: {etag: 'etag-1'}}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(createRun).mockResolvedValue(err(new Error('already exists'))) + + const exit = await Effect.runPromiseExit(createRunEffect(config, 'identity', 'repo', runState, coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(createRun).mockRejectedValue(new Error('network')) + + const exit = await Effect.runPromiseExit(createRunEffect(config, 'identity', 'repo', runState, coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// transitionRunEffect +// --------------------------------------------------------------------------- + +describe('transitionRunEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + const nextState = { + run_id: 'run-1', + surface: 'discord' as const, + thread_id: 'thread-1', + entity_ref: 'ref-1', + phase: 'ACKNOWLEDGED' as const, + started_at: new Date().toISOString(), + last_heartbeat: new Date().toISOString(), + holder_id: 'holder', + details: {}, + } + + it('resolves to {etag, state} on success', async () => { + vi.mocked(transitionRun).mockResolvedValue(ok({etag: 'etag-2', state: nextState})) + + const exit = await Effect.runPromiseExit( + transitionRunEffect(config, 'identity', 'repo', 'run-1', 'ACKNOWLEDGED', 'etag-1', coordLogger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: {etag: 'etag-2', state: {phase: 'ACKNOWLEDGED'}}}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(transitionRun).mockResolvedValue(err(new Error('invalid transition'))) + + const exit = await Effect.runPromiseExit( + transitionRunEffect(config, 'identity', 'repo', 'run-1', 'ACKNOWLEDGED', 'etag-1', coordLogger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(transitionRun).mockRejectedValue(new Error('timeout')) + + const exit = await Effect.runPromiseExit( + transitionRunEffect(config, 'identity', 'repo', 'run-1', 'ACKNOWLEDGED', 'etag-1', coordLogger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// findStaleRunsEffect +// --------------------------------------------------------------------------- + +describe('findStaleRunsEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to RunState[] on success', async () => { + vi.mocked(findStaleRuns).mockResolvedValue(ok([])) + + const exit = await Effect.runPromiseExit(findStaleRunsEffect(config, 'identity', 'repo', coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success', value: []}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(findStaleRuns).mockResolvedValue(err(new Error('list failed'))) + + const exit = await Effect.runPromiseExit(findStaleRunsEffect(config, 'identity', 'repo', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(findStaleRuns).mockRejectedValue(new Error('boom')) + + const exit = await Effect.runPromiseExit(findStaleRunsEffect(config, 'identity', 'repo', coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// validateProviderSemanticsEffect +// --------------------------------------------------------------------------- + +describe('validateProviderSemanticsEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to void on success', async () => { + vi.mocked(validateProviderSemantics).mockResolvedValue(ok(undefined)) + + const exit = await Effect.runPromiseExit(validateProviderSemanticsEffect(config, coordLogger)) + + expect(exit).toMatchObject({_tag: 'Success'}) + }) + + it('fails with error when Result is failure', async () => { + vi.mocked(validateProviderSemantics).mockResolvedValue(err(new Error('semantics check failed'))) + + const exit = await Effect.runPromiseExit(validateProviderSemanticsEffect(config, coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(validateProviderSemantics).mockRejectedValue(new Error('provider unreachable')) + + const exit = await Effect.runPromiseExit(validateProviderSemanticsEffect(config, coordLogger)) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// S3 sync helpers — plain Promise (no Result tag), test (1) and (3) only +// --------------------------------------------------------------------------- + +describe('syncSessionsToStoreEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to {uploaded, failed} on success', async () => { + vi.mocked(syncSessionsToStore).mockResolvedValue({uploaded: 2, failed: 0}) + + const exit = await Effect.runPromiseExit( + syncSessionsToStoreEffect(adapter, storeConfig, 'identity', 'repo', '/sessions', logger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: {uploaded: 2, failed: 0}}) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(syncSessionsToStore).mockRejectedValue(new Error('upload error')) + + const exit = await Effect.runPromiseExit( + syncSessionsToStoreEffect(adapter, storeConfig, 'identity', 'repo', '/sessions', logger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +describe('syncSessionsFromStoreEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to {downloaded, failed, mainDbRestored} on success', async () => { + vi.mocked(syncSessionsFromStore).mockResolvedValue({downloaded: 3, failed: 0, mainDbRestored: true}) + + const exit = await Effect.runPromiseExit( + syncSessionsFromStoreEffect(adapter, storeConfig, 'identity', 'repo', '/sessions', logger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: {downloaded: 3, failed: 0, mainDbRestored: true}}) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(syncSessionsFromStore).mockRejectedValue(new Error('download error')) + + const exit = await Effect.runPromiseExit( + syncSessionsFromStoreEffect(adapter, storeConfig, 'identity', 'repo', '/sessions', logger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +describe('syncArtifactsToStoreEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to {uploaded, failed} on success', async () => { + vi.mocked(syncArtifactsToStore).mockResolvedValue({uploaded: 1, failed: 0}) + + const exit = await Effect.runPromiseExit( + syncArtifactsToStoreEffect(adapter, storeConfig, 'identity', 'repo', 'run-1', '/logs', logger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: {uploaded: 1, failed: 0}}) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(syncArtifactsToStore).mockRejectedValue(new Error('artifact error')) + + const exit = await Effect.runPromiseExit( + syncArtifactsToStoreEffect(adapter, storeConfig, 'identity', 'repo', 'run-1', '/logs', logger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) + +describe('syncMetadataToStoreEffect', () => { + beforeEach(() => vi.resetAllMocks()) + + it('resolves to {success} on success', async () => { + vi.mocked(syncMetadataToStore).mockResolvedValue({success: true}) + + const exit = await Effect.runPromiseExit( + syncMetadataToStoreEffect(adapter, storeConfig, 'identity', 'repo', 'run-1', {key: 'val'}, logger), + ) + + expect(exit).toMatchObject({_tag: 'Success', value: {success: true}}) + }) + + it('fails when underlying function throws', async () => { + vi.mocked(syncMetadataToStore).mockRejectedValue(new Error('metadata error')) + + const exit = await Effect.runPromiseExit( + syncMetadataToStoreEffect(adapter, storeConfig, 'identity', 'repo', 'run-1', {key: 'val'}, logger), + ) + + expect(Exit.isFailure(exit)).toBe(true) + }) +}) diff --git a/packages/gateway/src/runtime-effect.ts b/packages/gateway/src/runtime-effect.ts new file mode 100644 index 00000000..e6cefa43 --- /dev/null +++ b/packages/gateway/src/runtime-effect.ts @@ -0,0 +1,205 @@ +/** + * Effect adapter wrapping @fro-bot/runtime async-Result-returning functions. + * + * This is the SINGLE file in the gateway package that imports from @fro-bot/runtime. + * All other gateway code must import coordination/sync helpers from here. + */ + +import type { + CoordinationConfig, + LockAcquisitionResult, + LockRecord, + Logger, + ObjectStoreAdapter, + ObjectStoreConfig, + RunPhase, + RunState, + Surface, +} from '@fro-bot/runtime' + +import { + acquireLock, + createRun, + findStaleRuns, + forceReleaseLock, + releaseLock, + renewLease, + syncArtifactsToStore, + syncMetadataToStore, + syncSessionsFromStore, + syncSessionsToStore, + transitionRun, + validateProviderSemantics, +} from '@fro-bot/runtime' +import {Effect} from 'effect' + +// --------------------------------------------------------------------------- +// Shared logger type used by all coordination functions +// --------------------------------------------------------------------------- + +export interface CoordinationLogger { + readonly debug: (message: string, context?: Record) => void +} + +// --------------------------------------------------------------------------- +// Lock operations +// --------------------------------------------------------------------------- + +export const acquireLockEffect = ( + config: CoordinationConfig, + repo: string, + holderId: string, + surface: Surface, + runId: string, + logger: CoordinationLogger, +): Effect.Effect => + Effect.tryPromise({ + try: async () => acquireLock(config, repo, holderId, surface, runId, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +export const releaseLockEffect = ( + config: CoordinationConfig, + repo: string, + etag: string, + logger: CoordinationLogger, +): Effect.Effect => + Effect.tryPromise({ + try: async () => releaseLock(config, repo, etag, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +export const renewLeaseEffect = ( + config: CoordinationConfig, + repo: string, + lockRecord: LockRecord, + etag: string, + logger: CoordinationLogger, +): Effect.Effect<{etag: string}, Error> => + Effect.tryPromise({ + try: async () => renewLease(config, repo, lockRecord, etag, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +export const forceReleaseLockEffect = ( + config: CoordinationConfig, + repo: string, + etag: string, + logger: CoordinationLogger, +): Effect.Effect => + Effect.tryPromise({ + try: async () => forceReleaseLock(config, repo, etag, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +// --------------------------------------------------------------------------- +// Run-state operations +// --------------------------------------------------------------------------- + +export const createRunEffect = ( + config: CoordinationConfig, + identity: string, + repo: string, + runState: RunState, + logger: CoordinationLogger, +): Effect.Effect<{etag: string}, Error> => + Effect.tryPromise({ + try: async () => createRun(config, identity, repo, runState, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +export const transitionRunEffect = ( + config: CoordinationConfig, + identity: string, + repo: string, + runId: string, + newPhase: RunPhase, + etag: string, + logger: CoordinationLogger, +): Effect.Effect<{etag: string; state: RunState}, Error> => + Effect.tryPromise({ + try: async () => transitionRun(config, identity, repo, runId, newPhase, etag, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +export const findStaleRunsEffect = ( + config: CoordinationConfig, + identity: string, + repo: string, + logger: CoordinationLogger, +): Effect.Effect => + Effect.tryPromise({ + try: async () => findStaleRuns(config, identity, repo, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +// --------------------------------------------------------------------------- +// Self-test / provider semantics +// --------------------------------------------------------------------------- + +export const validateProviderSemanticsEffect = ( + config: CoordinationConfig, + logger: CoordinationLogger, +): Effect.Effect => + Effect.tryPromise({ + try: async () => validateProviderSemantics(config, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }).pipe(Effect.flatMap(result => (result.success === true ? Effect.succeed(result.data) : Effect.fail(result.error)))) + +// --------------------------------------------------------------------------- +// S3 sync helpers — plain Promise returns (no Result tag) +// --------------------------------------------------------------------------- + +export const syncSessionsToStoreEffect = ( + adapter: ObjectStoreAdapter, + config: ObjectStoreConfig, + identity: string, + repo: string, + sessionStoragePath: string, + logger: Logger, +): Effect.Effect<{uploaded: number; failed: number}, Error> => + Effect.tryPromise({ + try: async () => syncSessionsToStore(adapter, config, identity, repo, sessionStoragePath, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + +export const syncSessionsFromStoreEffect = ( + adapter: ObjectStoreAdapter, + config: ObjectStoreConfig, + identity: string, + repo: string, + sessionStoragePath: string, + logger: Logger, +): Effect.Effect<{downloaded: number; failed: number; mainDbRestored: boolean}, Error> => + Effect.tryPromise({ + try: async () => syncSessionsFromStore(adapter, config, identity, repo, sessionStoragePath, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + +export const syncArtifactsToStoreEffect = ( + adapter: ObjectStoreAdapter, + config: ObjectStoreConfig, + identity: string, + repo: string, + runId: string, + logPath: string, + logger: Logger, +): Effect.Effect<{uploaded: number; failed: number}, Error> => + Effect.tryPromise({ + try: async () => syncArtifactsToStore(adapter, config, identity, repo, runId, logPath, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) + +export const syncMetadataToStoreEffect = ( + adapter: ObjectStoreAdapter, + config: ObjectStoreConfig, + identity: string, + repo: string, + runId: string, + metadata: unknown, + logger: Logger, +): Effect.Effect<{success: boolean}, Error> => + Effect.tryPromise({ + try: async () => syncMetadataToStore(adapter, config, identity, repo, runId, metadata, logger), + catch: error => (error instanceof Error ? error : new Error(String(error))), + }) diff --git a/packages/gateway/src/shutdown.test.ts b/packages/gateway/src/shutdown.test.ts new file mode 100644 index 00000000..ede0d19f --- /dev/null +++ b/packages/gateway/src/shutdown.test.ts @@ -0,0 +1,160 @@ +import type {Client} from 'discord.js' +import type {GatewayLogger} from './discord/client.js' + +import process from 'node:process' + +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' + +import {DEFAULT_DRAIN_MS, installShutdownHandlers} from './shutdown.js' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeLogger(): {logger: GatewayLogger; calls: {method: string; ctx: Record; msg: string}[]} { + const calls: {method: string; ctx: Record; msg: string}[] = [] + const logger: GatewayLogger = { + debug: (ctx, msg) => calls.push({method: 'debug', ctx, msg}), + info: (ctx, msg) => calls.push({method: 'info', ctx, msg}), + warn: (ctx, msg) => calls.push({method: 'warn', ctx, msg}), + error: (ctx, msg) => calls.push({method: 'error', ctx, msg}), + } + return {logger, calls} +} + +function makeClient(destroyDelay = 0): Client { + return { + destroy: vi.fn().mockImplementation(async () => new Promise(resolve => setTimeout(resolve, destroyDelay))), + } as unknown as Client +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('installShutdownHandlers', () => { + let exitCodes: (number | string | null | undefined)[] + let exitSpy: ReturnType + + beforeEach(() => { + vi.useFakeTimers() + exitCodes = [] + exitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null) => { + exitCodes.push(code) + // Do NOT throw — throwing from inside Promise.then() causes the .catch() + // branch to fire process.exit(1) again, masking the real exit code. + return undefined as never + }) + }) + + afterEach(() => { + vi.useRealTimers() + ;(exitSpy as {mockRestore: () => void}).mockRestore() + }) + + it('registers SIGTERM and SIGINT listeners', () => { + // #given + const {logger} = makeLogger() + const client = makeClient() + const beforeSigterm = process.listenerCount('SIGTERM') + const beforeSigint = process.listenerCount('SIGINT') + + // #when + const cleanup = installShutdownHandlers(client, logger) + + // #then + expect(process.listenerCount('SIGTERM')).toBe(beforeSigterm + 1) + expect(process.listenerCount('SIGINT')).toBe(beforeSigint + 1) + + cleanup() + }) + + it('cleanup function removes both listeners', () => { + // #given + const {logger} = makeLogger() + const client = makeClient() + const beforeSigterm = process.listenerCount('SIGTERM') + const beforeSigint = process.listenerCount('SIGINT') + const cleanup = installShutdownHandlers(client, logger) + + // #when + cleanup() + + // #then + expect(process.listenerCount('SIGTERM')).toBe(beforeSigterm) + expect(process.listenerCount('SIGINT')).toBe(beforeSigint) + }) + + it('logs shutdown clean and exits 0 when destroy resolves within drainMs', async () => { + // #given + const {logger, calls} = makeLogger() + const client = makeClient(0) // resolves immediately + const drainMs = 1_000 + const cleanup = installShutdownHandlers(client, logger, drainMs) + + // #when — emit SIGTERM and flush all timers so Promise.race resolves + process.emit('SIGTERM') + await vi.runAllTimersAsync() + cleanup() + + // #then + expect(calls.some(c => c.method === 'info' && c.msg === 'shutdown initiated')).toBe(true) + expect(calls.some(c => c.method === 'info' && c.msg === 'shutdown clean')).toBe(true) + expect(exitCodes).toContain(0) + expect(exitCodes).not.toContain(1) + }) + + it('logs shutdown timeout and exits 1 when destroy hangs longer than drainMs', async () => { + // #given + const {logger, calls} = makeLogger() + const drainMs = 500 + const client = makeClient(drainMs * 10) // hangs well past drain window + const cleanup = installShutdownHandlers(client, logger, drainMs) + + // #when — emit SIGTERM then advance past drain window + process.emit('SIGTERM') + await vi.advanceTimersByTimeAsync(drainMs + 1) + cleanup() + + // #then + expect(calls.some(c => c.method === 'info' && c.msg === 'shutdown initiated')).toBe(true) + expect(calls.some(c => c.method === 'warn' && c.msg === 'shutdown timeout')).toBe(true) + expect(exitCodes).toContain(1) + }) + + it('ignores duplicate signals while a shutdown is already in flight', async () => { + // #given + const {logger, calls} = makeLogger() + const client = makeClient(50) + const cleanup = installShutdownHandlers(client, logger, 5_000) + + // #when — SIGTERM and SIGINT arrive in rapid succession (orchestrator + // escalates after a few seconds; both signals reach the same handler) + process.emit('SIGTERM') + process.emit('SIGINT') + await vi.advanceTimersByTimeAsync(100) + cleanup() + + // #then — only ONE shutdown initiated message, only ONE clean exit + const initiated = calls.filter(c => c.method === 'info' && c.msg === 'shutdown initiated') + expect(initiated).toHaveLength(1) + const cleanExits = calls.filter(c => c.method === 'info' && c.msg === 'shutdown clean') + expect(cleanExits).toHaveLength(1) + // process.exit called exactly once with code 0 + expect(exitCodes.filter(c => c === 0)).toHaveLength(1) + expect(exitCodes).not.toContain(1) + // client.destroy called exactly once — no concurrent destroy chain + // eslint-disable-next-line @typescript-eslint/unbound-method + const destroyMock = client.destroy as unknown as ReturnType + expect(destroyMock).toHaveBeenCalledOnce() + // The ignored signal is logged at debug so operators can see what happened + const ignored = calls.filter(c => c.method === 'debug' && c.msg.includes('already shutting down')) + expect(ignored).toHaveLength(1) + }) + + it('default drain ms is 25000', () => { + // #given the module default export + // #then the drain timeout is 25 seconds + expect(DEFAULT_DRAIN_MS).toBe(25_000) + }) +}) diff --git a/packages/gateway/src/shutdown.ts b/packages/gateway/src/shutdown.ts new file mode 100644 index 00000000..1b4b440f --- /dev/null +++ b/packages/gateway/src/shutdown.ts @@ -0,0 +1,80 @@ +import type {Client} from 'discord.js' + +import type {GatewayLogger} from './discord/client.js' + +import process from 'node:process' + +export const DEFAULT_DRAIN_MS = 25_000 + +/** + * Install SIGTERM and SIGINT handlers that gracefully drain the Discord client. + * + * On signal: + * 1. Log 'shutdown initiated' at info. + * 2. Race `client.destroy()` against a drain timer (default 25 s). + * 3. If destroy wins → log 'shutdown clean', exit 0. + * 4. If timer wins → log 'shutdown timeout', exit 1. + * + * Returns a cleanup function that removes both signal listeners (useful in tests). + */ +export function installShutdownHandlers( + client: Client, + logger: GatewayLogger, + drainMs: number = DEFAULT_DRAIN_MS, +): () => void { + // SIGTERM and SIGINT can arrive in quick succession (e.g. a container + // orchestrator that escalates after a few seconds). Without this guard + // both signals would race two concurrent destroy chains and call + // process.exit twice. The first call wins; subsequent calls log and + // return without scheduling new work. + let shuttingDown = false + + const handler = (signal: string) => { + if (shuttingDown) { + logger.debug({signal}, 'shutdown signal received while already shutting down — ignoring') + return + } + shuttingDown = true + + logger.info({signal}, 'shutdown initiated') + + let drainTimer: ReturnType | undefined + + const drainTimeout = new Promise<'timeout'>(resolve => { + drainTimer = setTimeout(() => resolve('timeout'), drainMs) + }) + + const destroyPromise = client + .destroy() + .then(() => 'clean' as const) + .catch((error: unknown) => { + logger.warn({err: error}, 'client.destroy() rejected during shutdown') + return 'clean' as const + }) + + Promise.race([destroyPromise, drainTimeout]) + .then(result => { + if (drainTimer !== undefined) { + clearTimeout(drainTimer) + } + if (result === 'timeout') { + logger.warn({}, 'shutdown timeout') + process.exit(1) + } else { + logger.info({}, 'shutdown clean') + process.exit(0) + } + }) + .catch(() => { + process.exit(1) + }) + } + + process.on('SIGTERM', handler) + process.on('SIGINT', handler) + + return () => { + process.off('SIGTERM', handler) + process.off('SIGINT', handler) + } +} diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json new file mode 100644 index 00000000..7dff43bb --- /dev/null +++ b/packages/gateway/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.cache/tsconfig.tsbuildinfo", + "rootDir": "../../", + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "include": ["src/**/*.ts", "../runtime/src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/gateway/tsdown.config.ts b/packages/gateway/tsdown.config.ts new file mode 100644 index 00000000..68cac3d0 --- /dev/null +++ b/packages/gateway/tsdown.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'tsdown' + +export default defineConfig({ + entry: ['src/main.ts'], + format: 'esm', + outDir: 'dist', +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a96a502..8e5405ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,18 @@ importers: specifier: workspace:* version: link:../../packages/runtime + packages/gateway: + dependencies: + '@fro-bot/runtime': + specifier: workspace:* + version: link:../runtime + discord.js: + specifier: 14.26.4 + version: 14.26.4 + effect: + specifier: 3.21.2 + version: 3.21.2 + packages/runtime: dependencies: '@bfra.me/es': @@ -475,6 +487,34 @@ packages: peerDependencies: commander: ~14.0.0 + '@discordjs/builders@1.14.1': + resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.1': + resolution: {integrity: sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -1196,6 +1236,22 @@ packages: cpu: [x64] os: [win32] + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.3': + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1558,6 +1614,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.58.1': resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1824,6 +1883,10 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + abbrev@4.0.0: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2326,6 +2389,13 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + discord-api-types@0.38.47: + resolution: {integrity: sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==} + + discord.js@14.26.4: + resolution: {integrity: sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==} + engines: {node: '>=18'} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -2345,6 +2415,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + effect@3.21.2: + resolution: {integrity: sha512-rXd2FGDM8KdjSIrc+mqEELo7ScW7xTVxEf1iInmPSpIde9/nyGuFM710cjTo7/EreGXiUX2MOonPpprbz2XHCg==} + electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} @@ -2652,6 +2725,10 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -3192,6 +3269,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash.uniqby@4.7.0: resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} @@ -3216,6 +3296,9 @@ packages: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3885,6 +3968,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -4339,6 +4425,9 @@ packages: peerDependencies: typescript: '>=4.0.0' + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + tsdown@0.21.10: resolution: {integrity: sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==} engines: {node: '>=20.19.0'} @@ -4666,6 +4755,18 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-naming@0.1.0: resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} engines: {node: '>=16.0.0'} @@ -5470,6 +5571,55 @@ snapshots: dependencies: commander: 14.0.3 + '@discordjs/builders@1.14.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.47 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.47 + + '@discordjs/rest@2.6.1': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.5 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.47 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.24.1 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.47 + + '@discordjs/ws@1.2.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.47 + tslib: 2.8.1 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6117,6 +6267,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.18.1 + + '@sapphire/snowflake@3.5.3': {} + + '@sapphire/snowflake@3.5.5': {} + '@sec-ant/readable-stream@0.4.1': {} '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.3(typescript@6.0.3))': @@ -6657,6 +6818,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.12.2 + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -6952,6 +7117,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vladfrangu/async_event_emitter@2.4.7': {} + abbrev@4.0.0: {} abort-controller@3.0.0: @@ -7416,6 +7583,27 @@ snapshots: dependencies: path-type: 4.0.0 + discord-api-types@0.38.47: {} + + discord.js@14.26.4: + dependencies: + '@discordjs/builders': 1.14.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.1 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.38.47 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.24.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -7428,6 +7616,11 @@ snapshots: eastasianwidth@0.2.0: {} + effect@3.21.2: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.313: {} emoji-regex@10.6.0: {} @@ -7856,6 +8049,10 @@ snapshots: exsolve@1.0.8: {} + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -8339,6 +8536,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.snakecase@4.1.1: {} + lodash.uniqby@4.7.0: {} lodash@4.18.1: {} @@ -8362,6 +8561,8 @@ snapshots: lru-cache@11.2.7: {} + magic-bytes.js@1.13.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9181,6 +9382,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + quansync@0.2.11: {} quansync@1.0.0: {} @@ -9726,6 +9929,8 @@ snapshots: picomatch: 4.0.4 typescript: 6.0.3 + ts-mixer@6.0.4: {} + tsdown@0.21.10(synckit@0.11.12)(typescript@6.0.3): dependencies: ansis: 4.2.0 @@ -10010,6 +10215,8 @@ snapshots: dependencies: signal-exit: 4.1.0 + ws@8.20.1: {} + xml-naming@0.1.0: {} xtend@4.0.2: {}