diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml
new file mode 100644
index 0000000000..f6b67b08ff
--- /dev/null
+++ b/.github/workflows/ansible-deploy.yml
@@ -0,0 +1,86 @@
+name: Ansible Deployment
+
+on:
+ push:
+ branches: [ master, lab06 ]
+ paths:
+ - 'ansible/**'
+ - '.github/workflows/ansible-deploy.yml'
+ pull_request:
+ branches: [ master, lab06 ]
+ paths:
+ - 'ansible/**'
+ - '.github/workflows/ansible-deploy.yml'
+ workflow_dispatch: # Позволяет запускать вручную
+
+jobs:
+ lint:
+ name: Ansible Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install Ansible and ansible-lint
+ run: |
+ pip install ansible ansible-lint
+
+ - name: Run ansible-lint
+ run: |
+ cd ansible
+ ansible-lint playbooks/*.yml roles/*/tasks/*.yml || true
+ continue-on-error: true
+
+ deploy:
+ name: Deploy Application
+ needs: lint
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install Ansible and dependencies
+ run: |
+ pip install ansible
+ ansible-galaxy collection install community.docker
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
+ chmod 600 ~/.ssh/id_rsa
+ ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts
+
+ - name: Test SSH connection
+ run: |
+ ssh -i ~/.ssh/id_rsa ${{ secrets.VM_USER }}@${{ secrets.VM_HOST }} "echo 'SSH connection successful'"
+
+ - name: Deploy with Ansible
+ env:
+ ANSIBLE_HOST_KEY_CHECKING: 'False'
+ run: |
+ cd ansible
+ echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
+ ansible-playbook playbooks/deploy.yml \
+ -i inventory/hosts.ini \
+ --vault-password-file /tmp/vault_pass
+ rm /tmp/vault_pass
+
+ - name: Verify deployment
+ run: |
+ sleep 10
+ curl -f http://${{ secrets.VM_HOST }}:5000/health || exit 1
+ echo "✅ Application is healthy!"
\ No newline at end of file
diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
new file mode 100644
index 0000000000..4bdf2edf8a
--- /dev/null
+++ b/.github/workflows/python-ci.yml
@@ -0,0 +1,86 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ - lab03
+ paths:
+ - 'app_python/**'
+ - '.github/workflows/python-ci.yml'
+ pull_request:
+ branches:
+ - master
+ paths:
+ - 'app_python/**'
+ - '.github/workflows/python-ci.yml'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+ cache: "pip"
+
+ - name: Install dependencies
+ run: |
+ pip install -r app_python/requirements.txt
+ pip install -r app_python/requirements-dev.txt
+
+ - name: Run linter
+ run: |
+ cd app_python
+ flake8 app.py
+
+ - name: Run tests
+ run: |
+ cd app_python
+ pytest -v
+
+ - name: Install Snyk CLI
+ run: |
+ npm install -g snyk
+
+ - name: Authenticate Snyk
+ run: |
+ snyk auth ${{ secrets.SYNK_TOKEN }}
+
+ - name: Run Snyk security scan
+ run: |
+ cd app_python
+ snyk test --severity-threshold=high
+
+
+
+
+ docker:
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Generate version
+ run: echo "VERSION=$(date +%Y.%m)" >> $GITHUB_ENV
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: app_python
+ push: true
+ tags: |
+ ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }}
+ ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest
diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml
new file mode 100644
index 0000000000..8770390c10
--- /dev/null
+++ b/.github/workflows/terraform-ci.yml
@@ -0,0 +1,124 @@
+name: Terraform CI/CD
+
+on:
+ push:
+ branches:
+ - master
+ - lab04
+ paths:
+ - 'terraform/**'
+ - '.github/workflows/terraform-ci.yml'
+ pull_request:
+ branches:
+ - master
+ - lab04
+ paths:
+ - 'terraform/**'
+ - '.github/workflows/terraform-ci.yml'
+
+jobs:
+ terraform-validation:
+ name: Terraform Validation
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: ./terraform
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v3
+ with:
+ terraform_version: latest
+
+ - name: Terraform Format Check
+ id: fmt
+ run: terraform fmt -check -recursive
+ continue-on-error: true
+
+ - name: Terraform Init
+ id: init
+ run: terraform init -backend=false
+
+ - name: Terraform Validate
+ id: validate
+ run: terraform validate -no-color
+
+ - name: Setup TFLint
+ uses: terraform-linters/setup-tflint@v4
+ with:
+ tflint_version: v0.61.0
+
+ - name: Show TFLint version
+ run: tflint --version
+
+ - name: Initialize TFLint
+ run: tflint --init
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Run TFLint
+ id: tflint
+ run: tflint --format compact --recursive
+ continue-on-error: true
+
+ - name: Comment PR with Results
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const output = `#### Terraform CI Results 🔍
+
+ | Check | Status |
+ |-------|--------|
+ | **Terraform Format** | \`${{ steps.fmt.outcome }}\` |
+ | **Terraform Init** | \`${{ steps.init.outcome }}\` |
+ | **Terraform Validate** | \`${{ steps.validate.outcome }}\` |
+ | **TFLint** | \`${{ steps.tflint.outcome }}\` |
+
+ 📋 Show Details
+
+ #### Terraform Format Check
+ ${{ steps.fmt.outcome == 'success' && '✅ All files are properly formatted' || '❌ Some files need formatting. Run: `terraform fmt -recursive`' }}
+
+ #### Terraform Init
+ ${{ steps.init.outcome == 'success' && '✅ Initialization successful' || '❌ Initialization failed' }}
+
+ #### Terraform Validate
+ ${{ steps.validate.outcome == 'success' && '✅ Configuration is valid' || '❌ Configuration has syntax errors' }}
+
+ #### TFLint
+ ${{ steps.tflint.outcome == 'success' && '✅ No linting issues found' || '⚠️ Linting issues detected (see logs)' }}
+
+
+
+ ---
+ *Pusher: @${{ github.actor }} | Workflow: \`${{ github.workflow }}\`*`;
+
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: output
+ });
+
+
+ - name: Fail workflow if critical validation fails
+ if: steps.init.outcome == 'failure' || steps.validate.outcome == 'failure'
+ run: |
+ echo "❌ Critical validation failed!"
+ echo "Init status: ${{ steps.init.outcome }}"
+ echo "Validate status: ${{ steps.validate.outcome }}"
+ exit 1
+
+ - name: Warning if format or lint fails
+ if: steps.fmt.outcome == 'failure' || steps.tflint.outcome == 'failure'
+ run: |
+ echo "⚠️ Non-critical checks failed (format or lint)"
+ echo "Format status: ${{ steps.fmt.outcome }}"
+ echo "TFLint status: ${{ steps.tflint.outcome }}"
+ echo "Please fix these issues, but workflow will not fail."
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 30d74d2584..30c3d2285a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,19 @@
-test
\ No newline at end of file
+test
+
+# Terraform
+terraform/.terraform/
+terraform/.terraform.lock.hcl
+terraform/terraform.tfstate
+terraform/terraform.tfstate.backup
+terraform/key.json
+terraform/*.tfvars
+
+# Pulumi
+.pulumi/
+venv/
+__pycache__/
+*.pyc
+.env
+key.json
+Pulumi.*.yaml
+!Pulumi.yaml
\ No newline at end of file
diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg
new file mode 100644
index 0000000000..0b4a898088
--- /dev/null
+++ b/ansible/ansible.cfg
@@ -0,0 +1,11 @@
+[defaults]
+inventory = inventory/hosts.ini
+roles_path = roles
+host_key_checking = False
+retry_files_enabled = False
+remote_user = ubuntu
+
+[privilege_escalation]
+become = True
+become_method = sudo
+become_user = root
diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md
new file mode 100644
index 0000000000..b5036926fd
--- /dev/null
+++ b/ansible/docs/LAB05.md
@@ -0,0 +1,519 @@
+# LAB05 — Ansible Fundamentals
+
+**Author:** Sofia Palkina
+**Date:** 2026-02-26
+**Course:** DevOps Core Course
+
+---
+
+## 1. Architecture Overview
+
+### Ansible Version
+
+
+**Version:** `ansible [core 2.20.3]`
+**Python:** `3.12.3`
+**Platform:** Ubuntu 24.04 LTS (WSL2)
+
+---
+
+### Target VM Configuration
+
+| Parameter | Value |
+|-----------|-------|
+| **Cloud Provider** | Yandex Cloud |
+| **Provisioning Tool** | Terraform |
+| **OS** | Ubuntu 22.04 LTS |
+| **Public IP** | `89.169.158.252` |
+| **Internal IP** | `192.168.10.30` |
+| **SSH User** | `ubuntu` |
+| **Connection** | SSH with key authentication |
+
+---
+
+### Role Structure
+
+```
+ansible/
+├── ansible.cfg # Ansible configuration
+├── inventory/
+│ ├── hosts.ini # Static inventory
+│ └── group_vars/
+│ └── all.yml # Encrypted variables (Vault)
+├── roles/
+│ ├── common/ # System provisioning
+│ │ ├── tasks/
+│ │ │ └── main.yml
+│ │ └── defaults/
+│ │ └── main.yml
+│ ├── docker/ # Docker installation
+│ │ ├── tasks/
+│ │ │ └── main.yml
+│ │ ├── handlers/
+│ │ │ └── main.yml
+│ │ └── defaults/
+│ │ └── main.yml
+│ └── app_deploy/ # Application deployment
+│ ├── tasks/
+│ │ └── main.yml
+│ ├── handlers/
+│ │ └── main.yml
+│ └── defaults/
+│ └── main.yml
+├── playbooks/
+│ ├── provision.yml # System provisioning playbook
+│ └── deploy.yml # Application deployment playbook
+└── docs/
+ ├── LAB05.md # This documentation
+ └── screenshots_l5/ # Screenshots
+```
+
+---
+
+### Why Roles Instead of Monolithic Playbooks?
+
+**Roles provide:**
+
+1. **Modularity** — Each role has a single responsibility
+ - `common`: System packages
+ - `docker`: Docker installation
+ - `app_deploy`: Application deployment
+
+2. **Reusability** — Roles can be shared across projects
+ - Docker role works on any Ubuntu server
+ - No code duplication
+
+3. **Maintainability** — Changes isolated to specific roles
+ - Update Docker version in one place
+ - Clear separation of concerns
+
+4. **Testability** — Test roles independently
+ - Verify Docker installation separately
+ - Gradual deployment
+---
+
+## 2. Roles Documentation
+
+### Role: `common`
+
+#### Purpose
+
+Performs basic system provisioning for Ubuntu servers:
+- Updates apt package cache
+- Installs essential system packages
+- Configures timezone (optional)
+
+This role prepares any Ubuntu server for further automation by ensuring common tools are available.
+
+#### Tasks Implementation
+
+**File:** `roles/common/tasks/main.yml`
+
+
+#### Variables
+
+**File:** `roles/common/defaults/main.yml`
+
+
+**Variable Explanation:**
+- `common_packages`: List of essential packages
+- Can be overridden in inventory or playbook
+
+#### Handlers
+
+None required — package installation doesn't need service restarts.
+
+#### Dependencies
+
+None — this is typically the first role executed.
+
+#### Idempotency
+
+- `apt: state=present` — installs only if missing
+- `update_cache` with `cache_valid_time` — caches for 1 hour
+- Multiple runs don't change system if packages exist
+
+---
+
+### Role: `docker`
+
+#### Purpose
+
+Installs and configures Docker Engine on Ubuntu:
+1. Adds Docker official GPG key
+2. Adds Docker APT repository
+3. Installs Docker CE packages
+4. Configures Docker service (enable, start)
+5. Adds user to `docker` group
+6. Installs Python Docker SDK for Ansible modules
+
+#### Tasks Implementation
+
+**File:** `roles/docker/tasks/main.yml`
+
+#### Variables
+
+**File:** `roles/docker/defaults/main.yml`
+
+**Variable Explanation:**
+- `docker_user`: User to add to docker group (enables non-root Docker commands)
+
+#### Handlers
+
+**File:** `roles/docker/handlers/main.yml`
+
+**Handler Explanation:**
+- Triggered when Docker repository is added
+- Ensures Docker service uses new configuration
+- Only runs when needed (efficiency!)
+
+#### Dependencies
+
+Should run after `common` role (requires `curl`, `apt-transport-https`).
+
+#### Idempotency
+
+- `apt_key: state=present` — adds key only if missing
+- `apt_repository: state=present` — adds repo only if missing
+- `service: state=started` — starts only if stopped
+- `user: append=yes` — adds to group without removing other groups
+
+---
+
+### Role: `app_deploy`
+
+#### Purpose
+
+Deploys containerized Python application from Docker Hub:
+1. Authenticates with Docker Hub (using Vault credentials)
+2. Pulls latest Docker image
+3. Stops and removes old container (if exists)
+4. Runs new container with proper configuration
+5. Verifies deployment health
+
+#### Tasks Implementation
+
+**File:** `roles/app_deploy/tasks/main.yml`
+
+#### Variables
+
+**Encrypted Variables** (from Vault):
+
+**File:** `inventory/group_vars/all.yml` (encrypted)
+
+```yaml
+---
+# Docker Hub credentials
+dockerhub_username: spalkkina
+dockerhub_password:
+
+# Application configuration
+app_name: devops-info-service
+docker_image: "{{ dockerhub_username }}/{{ app_name }}"
+docker_image_tag: "1.0"
+app_port: 5000
+app_port_2: 6000
+app_container_name: "{{ app_name }}"
+```
+
+**Default Variables:**
+
+**File:** `roles/app_deploy/defaults/main.yml`
+
+**Variable Explanation:**
+- `dockerhub_username/password`: Docker Hub authentication (from Vault)
+- `docker_image`: Full image name
+- `docker_image_tag`: Image version to deploy
+- `app_port`: Host port (external)
+- `app_port_2`: Container port
+- `docker_restart_policy`: `unless-stopped` (auto-restart on reboot)
+- `app_environment_vars`: Custom environment variables (empty by default)
+
+#### Handlers
+
+**File:** `roles/app_deploy/handlers/main.yml`
+
+**Handler Explanation:**
+- Restarts container when configuration changes
+- Used for config updates without redeployment
+
+#### Dependencies
+
+**Requires:**
+- `docker` role executed first
+- Docker daemon running
+- Python Docker SDK installed
+
+#### Security Considerations
+
+- `no_log: true` on Docker login (prevents credentials in logs)
+- Credentials stored in Ansible Vault
+- Vault password not committed to repository
+
+#### Idempotency
+
+- `docker_container: state=started` — starts only if not running
+- Image pull only downloads if newer version exists
+- Container recreated only if config changes
+
+---
+
+## 3. Idempotency Demonstration
+
+### Concept
+
+**Idempotency:** Running the same operation multiple times produces the same result.
+
+In Ansible: Re-running a playbook should only make changes if system state has drifted from desired state.
+
+---
+
+### First Run
+
+
+
+**Analysis:**
+- **9 tasks changed** (yellow)
+- System was in initial state
+- All packages installed
+- Docker service started
+- User added to group
+
+---
+
+### Second Run
+
+
+
+**Analysis:**
+- **0 tasks changed** (green "ok")
+- Desired state already achieved
+- No unnecessary operations
+- Idempotency
+
+---
+
+### What Changed First Time?
+
+| Task | Why Changed? |
+|------|--------------|
+| **Update apt cache** | Cache was outdated |
+| **Install packages** | Packages not installed |
+| **Add GPG key** | Key didn't exist |
+| **Add repository** | Repository not configured |
+| **Install Docker** | Docker not present |
+| **Start Docker service** | Service not running |
+| **Add user to group** | User not in docker group |
+| **Install Docker SDK** | Python package missing |
+
+---
+
+### Why Nothing Changed Second Time?
+
+Ansible **detected current state = desired state** for all resources:
+
+- Packages already installed (`state=present` satisfied)
+- Docker service already running (`state=started` satisfied)
+- User already in group (`groups: docker` satisfied)
+- Repository already exists
+
+**Key Insight:** Ansible compares current vs desired state **before** applying changes.
+
+---
+
+### What Makes Tasks Idempotent?
+
+**1. State-based modules:**
+```yaml
+apt:
+ name: docker-ce
+ state: present # ← Declarative, not imperative
+```
+
+**2. Service management:**
+```yaml
+service:
+ name: docker
+ state: started # ← Starts only if stopped
+```
+
+**3. User management:**
+```yaml
+user:
+ name: ubuntu
+ groups: docker
+ append: yes # ← Doesn't remove other groups
+```
+
+**4. Conditional operations:**
+```yaml
+apt:
+ update_cache: yes
+ cache_valid_time: 3600 # ← Skip if cache fresh
+```
+
+---
+
+## 4. Ansible Vault Usage
+
+### How Credentials Are Stored Securely
+
+**Encrypted file:**
+
+```
+inventory/group_vars/all.yml
+```
+
+**Created with:**
+
+```bash
+ansible-vault create inventory/group_vars/all.yml
+```
+
+**Encrypted content:**
+
+
+
+---
+
+### Vault Password Management Strategy
+
+**Password file:** `.vault_pass`
+
+```bash
+# Create password file (do ONCE)
+echo "your-secure-password" > .vault_pass
+chmod 600 .vault_pass
+```
+
+**Added to `.gitignore`:**
+
+```
+# Ansible
+.vault_pass
+*.retry
+__pycache__/
+```
+
+**Usage with playbooks:**
+
+**Option 1: Prompt for password**
+```bash
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+```
+
+---
+
+### Why Ansible Vault is Important
+
+1. **Prevents credential leaks** — No plaintext secrets in git
+2. **Safe version control** — Encrypted files can be committed
+3. **Team collaboration** — Share playbooks without exposing secrets
+4. **Compliance** — Meets security best practices
+5. **Audit trail** — Track changes to encrypted files
+
+---
+
+## 5. Deployment Verification
+
+### Deployment Execution
+
+
+
+
+### Container Status
+
+
+
+
+### Health Check and Main Endpoint Verification
+
+
+
+
+### Handler Execution
+
+Handlers were **not triggered** in this deployment because:
+- Container didn't exist before (no config change)
+- First deployment uses `state=started`
+
+**Handlers would trigger when:**
+- Updating image tag
+- Changing environment variables
+- Modifying port mappings
+
+---
+
+## 7. Key Decisions
+
+### Why use roles instead of plain playbooks?
+
+Roles provide **modular architecture** with clear **separation of concerns**. Each role has a single responsibility:
+- `common` → system packages
+- `docker` → Docker installation
+- `app_deploy` → application deployment
+
+
+### How do roles improve reusability?
+
+Roles **encapsulate logic and variables**, making them portable across projects:
+
+1. **Same role, different projects:**
+ - `docker` role works on any Ubuntu server
+ - No code duplication
+
+2. **Override variables per environment:**
+ ```yaml
+ # dev environment
+ docker_user: devuser
+
+ # prod environment
+ docker_user: ubuntu
+ ```
+
+3. **Share via Ansible Galaxy:**
+ - Publish roles for community
+ - Import roles from others
+
+4. **Version control roles independently:**
+ - Update docker role without touching app_deploy
+ - Clear change history
+
+---
+
+### What makes a task idempotent?
+
+A task is **idempotent** when:
+> Running it multiple times produces the same final state, regardless of initial state.
+
+
+### How do handlers improve efficiency?
+
+**Answer:**
+
+Handlers **execute only when notified** and **only once per playbook run**, preventing unnecessary service restarts:
+
+
+### Why is Ansible Vault necessary?
+
+Ansible Vault is **essential for secure credential (encrypted at rest) management**:
+
+---
+
+## 8. Challenges
+
+### Challenge 1: WSL2 File Permissions
+
+**Problem:**
+```
+[WARNING]: Ansible is being run in a world writable directory
+```
+
+**Cause:** Windows filesystem (`/mnt/c/`) has open permissions incompatible with Ansible security requirements.
+
+**Solution:** Copied project to WSL native filesystem:
+```bash
+cp -r /mnt/c/.../ansible ~/ansible-lab05
+```
+
+**Learning:** Always work in WSL native filesystem for Ansible projects.
diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md
new file mode 100644
index 0000000000..7d7f4a4104
--- /dev/null
+++ b/ansible/docs/LAB06.md
@@ -0,0 +1,295 @@
+# Lab 6: Advanced Ansible & CI/CD - Submission
+
+**Name:** Sofia Palkina
+**Date:** 2026-03-05
+
+## Task 1: Blocks & Tags
+
+
+### Tag Strategy
+
+**Available tags:**
+- `common` - entire common role
+- `packages` - package installation tasks
+- `config` - system configuration tasks
+- `docker` - entire docker role
+- `docker_install` - Docker installation only
+- `docker_config` - Docker configuration only
+
+### Testing Results
+
+**List all tags:**
+
+```bash
+ansible-playbook playbooks/provision.yml --list-tags --ask-vault-pass
+```
+![alt text]()
+
+
+**Selective execution with `--tags "packages"`:**
+
+![alt text]()
+
+**Result:** Only package installation tasks executed.
+
+**Selective execution with `--tags "docker_install"`:**
+
+![alt text]()
+
+
+**Result:** Only Docker installation tasks executed, configuration skipped.
+
+**Idempotency check:**
+
+First run: `changed=2`
+Second run: `changed=0` ✅
+
+
+
+### Research Questions
+
+**Q: What happens if rescue block also fails?**
+A: Ansible marks the task as failed and stops playbook execution (unless `ignore_errors: yes` is set). The `always` block still executes.
+
+**Q: Can you have nested blocks?**
+A: Yes, blocks can be nested inside other blocks for more complex error handling logic.
+
+**Q: How do tags inherit to tasks within blocks?**
+A: Tags applied to a block automatically apply to all tasks within that block, but tasks can also have their own additional tags.
+
+---
+
+## Task 2: Docker Compose
+
+### Role Rename
+
+**Action taken:**
+```bash
+mv roles/app_deploy roles/web_app
+```
+
+**Updated references in:**
+- `playbooks/deploy.yml`
+- All documentation
+
+### Docker Compose Template
+
+**File:** `roles/web_app/templates/docker-compose.yml.j2`
+
+```yaml
+services:
+ {{ app_name }}:
+ image: {{ docker_image }}:{{ docker_image_tag }}
+ container_name: {{ app_name }}
+ ports:
+ - "{{ app_port }}:{{ app_internal_port }}"
+ environment:
+ - APP_NAME={{ app_name }}
+ - ENVIRONMENT=production
+ restart: {{ restart_policy }}
+ networks:
+ - app_network
+
+networks:
+ app_network:
+ driver: bridge
+```
+
+### Role Dependencies
+
+**File:** `roles/web_app/meta/main.yml`
+
+```yaml
+---
+dependencies:
+ - role: docker
+ tags:
+ - docker
+```
+
+### Deployment Results
+
+**First deployment:**
+
+![alt text]()
+
+**Verification and Health check:**
+
+```bash
+ansible webservers -a "docker ps" --ask-vault-pass
+ansible webservers -a "curl -s http://localhost:5000/health" --ask-vault-pass
+```
+
+**Output:**
+
+
+
+### Idempotency Verification
+
+**Second deployment run:**
+
+![alt text]()
+
+**Result:** Idempotent - no unnecessary changes on repeated execution.
+
+### Research Questions
+
+**Q: What's the difference between `restart: always` and `restart: unless-stopped`?**
+A: `always` restarts container even after Docker daemon restarts. `unless-stopped` doesn't restart if container was manually stopped.
+
+**Q: How do Docker Compose networks differ from Docker bridge networks?**
+A: Compose creates isolated networks per project with automatic DNS resolution by service name. Bridge networks are shared across all containers.
+
+**Q: Can you reference Ansible Vault variables in the template?**
+A: Yes, Vault variables are decrypted before templating, so they can be used in Jinja2 templates.
+
+
+## Task 3: Wipe Logic
+
+### Testing Scenarios
+
+#### Scenario 1: Normal Deployment (wipe skipped)
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks skipped
+- ✅ Application deployed
+- ✅ Container running
+
+![alt text]()
+![alt text]()
+#### Scenario 2: Wipe Only
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --tags web_app_wipe \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks executed
+- ✅ Deployment tasks skipped
+- ✅ Container removed
+- ✅ Directory deleted
+
+![alt text]()
+![alt text]()
+
+#### Scenario 3: Clean Reinstall (wipe → deploy)
+
+**Command:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks executed (old removed)
+- ✅ Deployment tasks executed (new installed)
+- ✅ Fresh container running
+
+![alt text]()
+
+#### Scenario 4: Safety Check (tag without variable)
+
+**Command 4a:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ --tags web_app_wipe \
+ --ask-vault-pass
+```
+
+**Result:**
+- ✅ Wipe tasks skipped (`when` condition blocks)
+- ✅ Application NOT removed
+- ✅ Double-gating protection works!
+
+![alt text]()
+
+**Command 4b:**
+```bash
+ansible-playbook playbooks/deploy.yml \
+ -e "web_app_wipe=true" \
+ --tags web_app_wipe
+```
+![alt text]()
+
+
+### Research Questions
+
+**Q: Why use both variable AND tag?**
+A: Double protection against accidental deletion. Tag allows wipe-only execution without deployment, variable requires explicit confirmation.
+
+**Q: What's the difference between `never` tag and this approach?**
+A: `never` tag requires explicit tag specification to run. Our approach adds additional variable check for extra safety.
+
+**Q: Why must wipe logic come BEFORE deployment in main.yml?**
+A: To support clean reinstallation scenario where we wipe old installation and deploy fresh in single playbook run.
+
+**Q: When would you want clean reinstallation vs. rolling update?**
+A: Clean reinstall for testing or state corruption issues. Rolling update for production with zero downtime.
+
+**Q: How would you extend this to wipe Docker images and volumes too?**
+A: Add tasks with `docker_image` module (state: absent) and `docker_volume` module to remove associated volumes.
+
+## Task 4: CI/CD
+
+### GitHub Secrets Configuration
+
+**Configured secrets:**
+- `ANSIBLE_VAULT_PASSWORD` - Vault decryption password
+- `SSH_PRIVATE_KEY` - SSH private key for VM access
+- `VM_HOST` - `89.169.158.252`
+- `VM_USER` - `ubuntu`
+
+### Workflow Results
+
+**Status Badge:**
+
+[](https://github.com/angel-palkina/DevOps-Core-Course/actions/workflows/ansible-deploy.yml)
+
+**Successful workflow run:**
+
+![alt text]()
+![alt text]()
+
+**Lint job output:**
+
+
+
+**Deploy job output:**
+
+
+
+
+**Verification output:**
+
+
+
+### Research Questions
+
+**Q: What are the security implications of storing SSH keys in GitHub Secrets?**
+A: GitHub Secrets are encrypted at rest and only exposed during workflow runs. Risks include GitHub account compromise. Mitigations: use 2FA, limit SSH key permissions, regular key rotation.
+
+**Q: How would you implement a staging → production deployment pipeline?**
+A: Create separate workflows for staging and production with different triggers (staging on push, production on release/tag). Use GitHub Environments with approval gates.
+
+**Q: What would you add to make rollbacks possible?**
+A: Tag Docker images with Git commit SHA, store image tags in Git, create rollback workflow accepting version parameter, use wipe logic before deploying previous version.
+
+**Q: How does self-hosted runner improve security compared to GitHub-hosted?**
+A: Direct infrastructure access (no SSH needed), controlled environment, secrets never leave infrastructure, faster for large files, compliance requirements.
+
+### Challenges & Solutions
+
+**Challenge:** Health check failing due to incorrect port mapping
+**Solution:** Fixed port mapping from `5000:6000` to `5000:5000` to match application configuration
+
+
diff --git a/ansible/docs/READMY.md b/ansible/docs/READMY.md
new file mode 100644
index 0000000000..d46947d65d
--- /dev/null
+++ b/ansible/docs/READMY.md
@@ -0,0 +1,19 @@
+# Ansible Automation
+
+[](https://github.com/angel-palkina/DevOps-Core-Course/actions/workflows/ansible-deploy.yml)
+
+## Lab 06 - Advanced Ansible & CI/CD
+
+Automated deployment with GitHub Actions.
+
+### Quick Start
+
+```bash
+# Deploy application
+ansible-playbook playbooks/deploy.yml --ask-vault-pass
+
+# Provision servers
+ansible-playbook playbooks/provision.yml --ask-vault-pass
+
+# Clean reinstall
+ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" --ask-vault-pass
\ No newline at end of file
diff --git a/ansible/docs/Screenshot 2026-03-05 191756.png b/ansible/docs/Screenshot 2026-03-05 191756.png
new file mode 100644
index 0000000000..95a308cbbb
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 191756.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 192337.png b/ansible/docs/Screenshot 2026-03-05 192337.png
new file mode 100644
index 0000000000..ad0089373d
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 192337.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 194004.png b/ansible/docs/Screenshot 2026-03-05 194004.png
new file mode 100644
index 0000000000..c19dd65877
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 194004.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 220630.png b/ansible/docs/Screenshot 2026-03-05 220630.png
new file mode 100644
index 0000000000..b83ac31998
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 220630.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 221207.png b/ansible/docs/Screenshot 2026-03-05 221207.png
new file mode 100644
index 0000000000..8293e88b58
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 221207.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222221-1.png b/ansible/docs/Screenshot 2026-03-05 222221-1.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222221-1.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222221.png b/ansible/docs/Screenshot 2026-03-05 222221.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222221.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222252.png b/ansible/docs/Screenshot 2026-03-05 222252.png
new file mode 100644
index 0000000000..3f369395f4
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222252.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222432.png b/ansible/docs/Screenshot 2026-03-05 222432.png
new file mode 100644
index 0000000000..bb901abbe2
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222432.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 222742.png b/ansible/docs/Screenshot 2026-03-05 222742.png
new file mode 100644
index 0000000000..085764b918
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 222742.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225003.png b/ansible/docs/Screenshot 2026-03-05 225003.png
new file mode 100644
index 0000000000..279c18f87c
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225003.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225036-1.png b/ansible/docs/Screenshot 2026-03-05 225036-1.png
new file mode 100644
index 0000000000..6b53d60841
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225036-1.png differ
diff --git a/ansible/docs/Screenshot 2026-03-05 225036.png b/ansible/docs/Screenshot 2026-03-05 225036.png
new file mode 100644
index 0000000000..6b53d60841
Binary files /dev/null and b/ansible/docs/Screenshot 2026-03-05 225036.png differ
diff --git a/ansible/docs/images/3.png b/ansible/docs/images/3.png
new file mode 100644
index 0000000000..5dc8c9893a
Binary files /dev/null and b/ansible/docs/images/3.png differ
diff --git a/ansible/docs/images/4.png b/ansible/docs/images/4.png
new file mode 100644
index 0000000000..e0b76459a6
Binary files /dev/null and b/ansible/docs/images/4.png differ
diff --git a/ansible/docs/images/5.png b/ansible/docs/images/5.png
new file mode 100644
index 0000000000..37090f3f62
Binary files /dev/null and b/ansible/docs/images/5.png differ
diff --git a/ansible/docs/images/7.png b/ansible/docs/images/7.png
new file mode 100644
index 0000000000..ca70ff0000
Binary files /dev/null and b/ansible/docs/images/7.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 163842.png b/ansible/docs/images/Screenshot 2026-02-26 163842.png
new file mode 100644
index 0000000000..16b664868b
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 163842.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 163855.png b/ansible/docs/images/Screenshot 2026-02-26 163855.png
new file mode 100644
index 0000000000..79862bcb8a
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 163855.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170120.png b/ansible/docs/images/Screenshot 2026-02-26 170120.png
new file mode 100644
index 0000000000..cddf5b5052
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170120.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170411.png b/ansible/docs/images/Screenshot 2026-02-26 170411.png
new file mode 100644
index 0000000000..94753dc4bd
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170411.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 170552.png b/ansible/docs/images/Screenshot 2026-02-26 170552.png
new file mode 100644
index 0000000000..f0f3a958a2
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 170552.png differ
diff --git a/ansible/docs/images/Screenshot 2026-02-26 192129.png b/ansible/docs/images/Screenshot 2026-02-26 192129.png
new file mode 100644
index 0000000000..476b0ebf4d
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-02-26 192129.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191610.png b/ansible/docs/images/Screenshot 2026-03-05 191610.png
new file mode 100644
index 0000000000..7b2ac65f3e
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191610.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191756.png b/ansible/docs/images/Screenshot 2026-03-05 191756.png
new file mode 100644
index 0000000000..95a308cbbb
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191756.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 191832.png b/ansible/docs/images/Screenshot 2026-03-05 191832.png
new file mode 100644
index 0000000000..599e5e9808
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 191832.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 192337.png b/ansible/docs/images/Screenshot 2026-03-05 192337.png
new file mode 100644
index 0000000000..ad0089373d
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 192337.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 194004.png b/ansible/docs/images/Screenshot 2026-03-05 194004.png
new file mode 100644
index 0000000000..c19dd65877
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 194004.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 220514.png b/ansible/docs/images/Screenshot 2026-03-05 220514.png
new file mode 100644
index 0000000000..35d03e6a18
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 220514.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 220630.png b/ansible/docs/images/Screenshot 2026-03-05 220630.png
new file mode 100644
index 0000000000..b83ac31998
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 220630.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 221207.png b/ansible/docs/images/Screenshot 2026-03-05 221207.png
new file mode 100644
index 0000000000..8293e88b58
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 221207.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222221.png b/ansible/docs/images/Screenshot 2026-03-05 222221.png
new file mode 100644
index 0000000000..51f0d32b03
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222221.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222252.png b/ansible/docs/images/Screenshot 2026-03-05 222252.png
new file mode 100644
index 0000000000..3f369395f4
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222252.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222432.png b/ansible/docs/images/Screenshot 2026-03-05 222432.png
new file mode 100644
index 0000000000..bb901abbe2
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222432.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 222742.png b/ansible/docs/images/Screenshot 2026-03-05 222742.png
new file mode 100644
index 0000000000..085764b918
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 222742.png differ
diff --git a/ansible/docs/images/Screenshot 2026-03-05 225003.png b/ansible/docs/images/Screenshot 2026-03-05 225003.png
new file mode 100644
index 0000000000..279c18f87c
Binary files /dev/null and b/ansible/docs/images/Screenshot 2026-03-05 225003.png differ
diff --git a/ansible/docs/images/image.png b/ansible/docs/images/image.png
new file mode 100644
index 0000000000..4ff28c8da2
Binary files /dev/null and b/ansible/docs/images/image.png differ
diff --git a/ansible/docs/images/image1.png b/ansible/docs/images/image1.png
new file mode 100644
index 0000000000..0245c17860
Binary files /dev/null and b/ansible/docs/images/image1.png differ
diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml
new file mode 100644
index 0000000000..976fa46fd5
--- /dev/null
+++ b/ansible/inventory/group_vars/all.yml
@@ -0,0 +1,18 @@
+$ANSIBLE_VAULT;1.1;AES256
+38613830353565626539316439343866313634616337313962636431613362656132333130653439
+6662613561323563623332323234333764653832623833610a363462653964396435323763356465
+35623562376538323838346239336666666361356162363132356438663364326433323430343730
+3338393138373936370a343337363161643233333861623161336563303831363636353736373439
+32303430306636393765323337663537363530653737323164663334373338363366306532633739
+64643237623465646230306635366565616435663533373434393265663962633031666137396638
+66396266353364373836343939613036316632346235323839376433306630656566636439303063
+30336562636634303731393035393662326565383936393436613264666431393862333930376666
+61643665613238393631616435373839303334386135666465366433393630353562626238646437
+66636365653666323562356236313863373933376539663462636530366131613538643433393665
+33643233643964336131623664663334376362663531346333363865383763326439623031373431
+62323932363864303832343866636562393463636331653164366136393530353964363335656439
+66663331633532333237663237646263346364656364616137353936653363613836326166313631
+32306331636261313136373661643166336464633732343562333763616638373931363230633565
+39383966386138633464636638666165336635656166626433363765623732313161613861313063
+35366163613265356338353961643065343262363838343232613738656566653337666630646465
+6662
diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini
new file mode 100644
index 0000000000..19bbe814b3
--- /dev/null
+++ b/ansible/inventory/hosts.ini
@@ -0,0 +1,5 @@
+[webservers]
+terraform-vm ansible_host=89.169.158.252 ansible_user=ubuntu
+
+[webservers:vars]
+ansible_python_interpreter=/usr/bin/python3
diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml
new file mode 100644
index 0000000000..cb92900093
--- /dev/null
+++ b/ansible/playbooks/deploy.yml
@@ -0,0 +1,7 @@
+---
+- name: Deploy web application
+ hosts: webservers
+ become: yes
+
+ roles:
+ - web_app
\ No newline at end of file
diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml
new file mode 100644
index 0000000000..0fafc4162d
--- /dev/null
+++ b/ansible/playbooks/provision.yml
@@ -0,0 +1,5 @@
+- name: Provision web servers
+ hosts: webservers
+ roles:
+ - common
+ - docker
diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml
new file mode 100644
index 0000000000..96b9736524
--- /dev/null
+++ b/ansible/roles/common/defaults/main.yml
@@ -0,0 +1,6 @@
+common_packages:
+ - python3-pip
+ - curl
+ - git
+ - vim
+ - htop
diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml
new file mode 100644
index 0000000000..1dbc528ab5
--- /dev/null
+++ b/ansible/roles/common/tasks/main.yml
@@ -0,0 +1,43 @@
+---
+# Common role with blocks, tags, and error handling
+- name: Package installation block
+ block:
+ - name: Update apt cache
+ apt:
+ update_cache: yes
+ cache_valid_time: 3600
+
+ - name: Install common packages
+ apt:
+ name: "{{ common_packages }}"
+ state: present
+
+ rescue:
+ - name: Handle apt cache update failure
+ debug:
+ msg: "Apt cache update failed, attempting fix-missing"
+
+ - name: Fix apt cache
+ command: apt-get update --fix-missing
+ changed_when: false
+
+ - name: Retry installing common packages
+ apt:
+ name: "{{ common_packages }}"
+ state: present
+
+ become: yes
+ tags:
+ - common
+ - packages
+
+- name: System configuration block
+ block:
+ - name: Set timezone
+ community.general.timezone:
+ name: Europe/Moscow
+
+ become: yes
+ tags:
+ - common
+ - config
\ No newline at end of file
diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml
new file mode 100644
index 0000000000..372575fd86
--- /dev/null
+++ b/ansible/roles/docker/defaults/main.yml
@@ -0,0 +1 @@
+docker_user: ubuntu
diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml
new file mode 100644
index 0000000000..1907c4cd1c
--- /dev/null
+++ b/ansible/roles/docker/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: restart docker
+ service:
+ name: docker
+ state: restarted
diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml
new file mode 100644
index 0000000000..8053d71a3d
--- /dev/null
+++ b/ansible/roles/docker/tasks/main.yml
@@ -0,0 +1,95 @@
+---
+# Docker role with blocks, tags, and error handling
+- name: Docker installation block
+ block:
+ - name: Install required system packages
+ apt:
+ name:
+ - ca-certificates
+ - gnupg
+ - lsb-release
+ - apt-transport-https
+ state: present
+
+ - name: Add Docker GPG key
+ apt_key:
+ url: https://download.docker.com/linux/ubuntu/gpg
+ state: present
+
+ - name: Add Docker repository
+ apt_repository:
+ repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+ notify: restart docker
+
+ - name: Install Docker packages
+ apt:
+ name:
+ - docker-ce
+ - docker-ce-cli
+ - containerd.io
+ state: present
+ update_cache: yes
+
+ rescue:
+ - name: Handle GPG key failure
+ debug:
+ msg: "Docker GPG key addition failed, retrying after delay"
+
+ - name: Wait before retry
+ wait_for:
+ timeout: 10
+
+ - name: Retry adding Docker GPG key
+ apt_key:
+ url: https://download.docker.com/linux/ubuntu/gpg
+ state: present
+
+ - name: Retry adding Docker repository
+ apt_repository:
+ repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+
+ - name: Retry Docker installation
+ apt:
+ name:
+ - docker-ce
+ - docker-ce-cli
+ - containerd.io
+ state: present
+ update_cache: yes
+
+ always:
+ - name: Ensure Docker service is enabled
+ service:
+ name: docker
+ enabled: yes
+
+ become: yes
+ tags:
+ - docker
+ - docker_install
+
+- name: Docker configuration block
+ block:
+ - name: Ensure Docker is running
+ service:
+ name: docker
+ state: started
+ enabled: true
+
+ - name: Add user to docker group
+ user:
+ name: "{{ docker_user }}"
+ groups: docker
+ append: yes
+
+ - name: Install Python Docker SDK
+ pip:
+ name: docker
+ state: present
+
+ become: yes
+ tags:
+ - docker
+ - docker_config
\ No newline at end of file
diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml
new file mode 100644
index 0000000000..223feed0ac
--- /dev/null
+++ b/ansible/roles/web_app/defaults/main.yml
@@ -0,0 +1,15 @@
+---
+# Web application defaults
+app_name: devops-info-service
+docker_image: "spalkkina/{{ app_name }}"
+docker_image_tag: "1.0"
+app_port: 5000 # External port (host)
+app_internal_port: 5000 # Internal port (container) ← ИЗМЕНЕНО
+restart_policy: unless-stopped
+
+# Docker Compose configuration
+compose_project_dir: "/opt/{{ app_name }}"
+compose_file_name: docker-compose.yml
+
+# Wipe control (default: do not wipe)
+web_app_wipe: false
\ No newline at end of file
diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml
new file mode 100644
index 0000000000..c56af2d69b
--- /dev/null
+++ b/ansible/roles/web_app/handlers/main.yml
@@ -0,0 +1,11 @@
+- name: restart app container
+ docker_container:
+ name: "{{ app_container_name }}"
+ state: started
+ restart: yes
+ image: "{{ docker_image }}:{{ docker_image_tag }}"
+ restart_policy: "{{ docker_restart_policy }}"
+ ports:
+ - "{{ app_port }}:{{ app_port }}"
+ env: "{{ app_environment_vars }}"
+ become: yes
\ No newline at end of file
diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml
new file mode 100644
index 0000000000..4aef9e68ed
--- /dev/null
+++ b/ansible/roles/web_app/meta/main.yml
@@ -0,0 +1,6 @@
+---
+# Role dependencies
+dependencies:
+ - role: docker
+ tags:
+ - docker
\ No newline at end of file
diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml
new file mode 100644
index 0000000000..a579e83662
--- /dev/null
+++ b/ansible/roles/web_app/tasks/main.yml
@@ -0,0 +1,90 @@
+---
+# Wipe logic runs first (when explicitly requested)
+- name: Include wipe tasks
+ include_tasks: wipe.yml
+ tags:
+ - web_app_wipe
+
+# Web app deployment with Docker Compose and blocks/tags
+- name: Deploy application with Docker Compose
+ block:
+ - name: Create application directory
+ file:
+ path: "{{ compose_project_dir }}"
+ state: directory
+ mode: '0755'
+
+ - name: Template docker-compose.yml file
+ template:
+ src: docker-compose.yml.j2
+ dest: "{{ compose_project_dir }}/{{ compose_file_name }}"
+ mode: '0644'
+
+ # - name: Login to Docker Hub
+ # community.docker.docker_login:
+ # username: "{{ dockerhub_username }}"
+ # password: "{{ dockerhub_password }}"
+ # no_log: true
+
+ # - name: Pull Docker image
+ # community.docker.docker_image:
+ # name: "{{ docker_image }}:{{ docker_image_tag }}"
+ # source: pull
+ #
+ # - name: Check if standalone container exists (from Lab 05)
+ # command: "docker ps -aq -f name=^{{ app_name }}$"
+ # register: standalone_container
+ # changed_when: false
+ # failed_when: false
+
+ # - name: Stop existing standalone container (from Lab 05)
+ # community.docker.docker_container:
+ # name: "{{ app_name }}"
+ # state: absent
+ # when:
+ # - standalone_container.stdout is defined
+ # - standalone_container.stdout | length > 0
+
+ - name: Deploy with Docker Compose
+ community.docker.docker_compose_v2:
+ project_src: "{{ compose_project_dir }}"
+ state: present
+ pull: always
+ # pull: "policy"
+ # remove_orphans: yes
+ # recreate: "auto" # ← ИСПРАВЛЕНО: auto вместо smart
+
+ # - name: Wait for application to be ready
+ # wait_for:
+ # host: localhost
+ # port: "{{ app_port }}"
+ # delay: 5
+ # timeout: 60
+
+ # - name: Verify application health
+ # uri:
+ # url: "http://localhost:{{ app_port }}/health"
+ # status_code: 200
+ # register: health_check
+ # retries: 3
+ # delay: 5
+
+ rescue:
+ - name: Handle deployment failure
+ debug:
+ msg: "Deployment failed. Check logs with: docker compose -f {{ compose_project_dir }}/{{ compose_file_name }} logs"
+
+ - name: Show deployment status
+ command: "docker compose -f {{ compose_project_dir }}/{{ compose_file_name }} ps"
+ register: compose_status
+ ignore_errors: yes
+
+ - name: Display compose status
+ debug:
+ var: compose_status.stdout_lines
+
+ become: yes
+ tags:
+ - web_app
+ - app_deploy
+ - compose
\ No newline at end of file
diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml
new file mode 100644
index 0000000000..68d2e376c1
--- /dev/null
+++ b/ansible/roles/web_app/tasks/wipe.yml
@@ -0,0 +1,29 @@
+---
+# Wipe logic - removes deployed application
+# Double-gated: requires variable=true AND tag
+- name: Wipe web application
+ block:
+ - name: Stop and remove containers with Docker Compose
+ community.docker.docker_compose_v2:
+ project_src: "{{ compose_project_dir }}"
+ state: absent
+ ignore_errors: yes
+
+ - name: Remove docker-compose.yml file
+ file:
+ path: "{{ compose_project_dir }}/{{ compose_file_name }}"
+ state: absent
+
+ - name: Remove application directory
+ file:
+ path: "{{ compose_project_dir }}"
+ state: absent
+
+ - name: Log wipe completion
+ debug:
+ msg: "Application {{ app_name }} wiped successfully"
+
+ when: web_app_wipe | bool
+ become: yes
+ tags:
+ - web_app_wipe
\ No newline at end of file
diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2
new file mode 100644
index 0000000000..ff5ca51cdd
--- /dev/null
+++ b/ansible/roles/web_app/templates/docker-compose.yml.j2
@@ -0,0 +1,16 @@
+services:
+ {{ app_name }}:
+ image: {{ docker_image }}:{{ docker_image_tag }}
+ container_name: {{ app_name }}
+ ports:
+ - "{{ app_port }}:{{ app_internal_port }}"
+ environment:
+ - APP_NAME={{ app_name }}
+ - ENVIRONMENT=production
+ restart: {{ restart_policy }}
+ networks:
+ - app_network
+
+networks:
+ app_network:
+ driver: bridge
\ No newline at end of file
diff --git a/app_python/.dockerignore b/app_python/.dockerignore
new file mode 100644
index 0000000000..44fac1397a
--- /dev/null
+++ b/app_python/.dockerignore
@@ -0,0 +1,61 @@
+# Python cache
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+*.py[cod]
+*.so
+*.dll
+*.egg
+*.egg-info/
+dist/
+build/
+
+# Virtual Environment
+venv/
+.venv/
+env/
+.env/
+
+# Testing & Coverage
+.pytest_cache/
+.coverage
+.coverage.*
+htmlcov/
+coverage.xml
+*.cover
+tests/
+docs/
+
+# Version Control
+.git/
+.gitignore
+.gitattributes
+.dockerignore
+README.md
+LICENSE
+*.md
+
+# IDE
+.vscode/
+.idea/
+*.swp
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Docker
+Dockerfile
+docker-compose*.yml
+.dockerignore
+
+# Secrets (ОЧЕНЬ ВАЖНО!)
+*.key
+*.pem
+.env
+.env.*
\ No newline at end of file
diff --git a/app_python/.gitignore b/app_python/.gitignore
new file mode 100644
index 0000000000..606bceb0a2
--- /dev/null
+++ b/app_python/.gitignore
@@ -0,0 +1,51 @@
+# Python
+__pycache__/
+*.py[cod]
+*.pyo
+*.pyd
+*.so
+*.dll
+*.egg
+*.egg-info/
+dist/
+build/
+
+# Virtual Environment
+venv/
+.venv/
+env/
+.env/
+
+# Testing
+.pytest_cache/
+.coverage
+.coverage.*
+*.cover
+htmlcov/
+coverage.xml
+.cache/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Docker
+.dockerignore
+Dockerfile
+docker-compose*.yml
+
+# Secrets (никогда не коммитить!)
+*.key
+*.pem
+.env.local
+.env.production
\ No newline at end of file
diff --git a/app_python/Dockerfile b/app_python/Dockerfile
new file mode 100644
index 0000000000..e327658846
--- /dev/null
+++ b/app_python/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.13-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+RUN useradd -m appuser
+USER appuser
+
+EXPOSE 5000
+
+CMD ["python", "app.py"]
\ No newline at end of file
diff --git a/app_python/README.md b/app_python/README.md
new file mode 100644
index 0000000000..64b84f025a
--- /dev/null
+++ b/app_python/README.md
@@ -0,0 +1,242 @@
+# DevOps Info Service
+
+## Overview
+A web application that provides detailed information about itself and its runtime environment. This service will evolve throughout the DevOps course into a comprehensive monitoring tool.
+
+## Project Structure
+```text
+app_python/
+├── app.py # Main application
+├── requirements.txt # Dependencies
+├── requirements-dev.txt # Development dependencies (testing, linting)
+├── .gitignore
+├── README.md # This file
+├── tests/
+│ ├── __init__.py
+│ └── test_app.py
+└── docs/ # Documentation
+ ├── LAB01.md
+ ├── LAB02.md
+ ├── LAB03.md
+ ├── ...
+ └── screenshots/
+```
+
+## Prerequisites
+- Python 3.11 or higher
+- pip (Python package manager)
+
+## Installation
+
+1. Clone the repository:
+ ```bash
+ git clone
+ cd app_python
+ ```
+
+
+2. Create a virtual environment:
+
+ ```bash
+ python -m venv venv
+ ```
+
+3. Activate the virtual environment:
+- Linux/Mac:
+
+ ```bash
+ source venv/bin/activate
+ ```
+- Windows:
+
+ ```bash
+ venv\Scripts\activate
+ ```
+
+4. Install dependencies:
+
+ ```bash
+ pip install -r requirements.txt
+
+ # or for testing
+ pip install -r requirements-dev.txt
+ ```
+
+## Running the Application
+
+```bash
+python app.py #Default Configuration The service will start at: http://0.0.0.0:5000
+
+# Custom Configuration on Linux/Mac:
+PORT=8080 python app.py # Change port
+HOST=127.0.0.1 PORT=3000 python app.py # Change host and port
+DEBUG=true python app.py # Enable debug mode
+
+# Custom Configuration on Windows PowerShell:
+$env:HOST="127.0.0.1"; $env:PORT=8080; python app.py
+```
+
+## API Endpoints
+
+### GET /
+
+- Returns comprehensive service and system information (endpoints, request, runtime, system info, service info).
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+### GET /health
+
+- Health check endpoint for monitoring systems and Kubernetes probes.
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/health
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+## Configuration
+
+The application is configured through environment variables:
+
+|Variable | Default | Description |
+|----------|-------|---------|
+|HOST | 0.0.0.0 | Host interface to bind the server|
+|PORT | 5000 | Port number to listen on|
+|DEBUG | false | Debug mode (true/false)|
+
+## Testing
+
+This project uses pytest for unit testing. Pytest was chosen for its:
+
+- Simple, Pythonic syntax
+
+- Powerful fixture system
+
+- Excellent plugin ecosystem (coverage, mocking)
+
+- Clear assertion reporting
+
+### What's Tested
+
+1) `GET /`
+
+ ✓ Status code 200
+
+ ✓ JSON response format
+
+ ✓ All required sections present
+
+ ✓ Data types validation
+
+ ✓ Service info correctness
+
+2) `GET /health`
+
+ ✓ Status code 200
+
+ ✓ Status = "healthy"
+
+ ✓ Timestamp format (ISO 8601)
+
+ ✓ Uptime tracking
+3) Error Handling
+
+ ✓ 404 Not Found response
+
+ ✓ JSON error format
+
+ ✓ Method not allowed
+
+### Running Tests Locally
+
+```bash
+# Run all tests
+pytest tests/ -v
+
+# Run tests with coverage report
+pytest tests/ --cov=app --cov-report=term
+
+# Run tests with HTML coverage report
+pytest tests/ --cov=app --cov-report=html
+# Then open htmlcov/index.html in your browser
+
+# Run specific test file
+pytest tests/test_app.py -v
+
+# Run tests matching a name pattern
+pytest tests/ -k "health" -v
+
+```
+
+### Expected Output
+
+
+
+### Test Coverage
+Current test coverage: ~92%
+
+
+### CI/CD Status
+
+
+
+
+## Docker
+
+This application is containerized using Docker for consistent and portable deployment.
+
+- Build the image locally
+
+ ```sh
+ docker build -t spalkkina/devops-info-service:1.0 .
+ ```
+- Run the container locally
+ ```
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+ The app will be accessible at http://localhost:5000.
+- Pull from Docker Hub
+ ```
+ docker pull spalkkina/devops-info-service:1.0
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+- You can run specific versions by adjusting the tag
+ ```
+ docker run -p 5000:5000 spalkkina/devops-info-service:latest
+ docker run -p 5000:5000 spalkkina/devops-info-service:1.0
+ ```
+## Future Development
+
+This service will evolve throughout the course:
+
+- Lab 8: Metrics endpoint for Prometheus
+
+- Lab 9: Kubernetes deployment
+
+- Lab 12: Persistence with file storage
+
+- Lab 13: Multi-environment deployment
+
+
diff --git a/app_python/app.py b/app_python/app.py
new file mode 100644
index 0000000000..1f2ada23dc
--- /dev/null
+++ b/app_python/app.py
@@ -0,0 +1,372 @@
+"""
+DevOps Info Service - Flask Application
+Main application module providing system and service information.
+"""
+
+import os
+import socket
+import platform
+import logging
+import json
+from datetime import datetime, timezone
+from flask import Flask, jsonify, request
+
+import time
+from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
+from threading import Lock
+
+# ========== JSON Formatter ==========
+class JSONFormatter(logging.Formatter):
+ """Custom JSON formatter for structured logging."""
+
+ def format(self, record):
+ log_data = {
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "level": record.levelname,
+ "logger": record.name,
+ "message": record.getMessage(),
+ "module": record.module,
+ "function": record.funcName,
+ }
+
+ # Add HTTP context if available
+ if hasattr(record, 'method'):
+ log_data['method'] = record.method
+ if hasattr(record, 'path'):
+ log_data['path'] = record.path
+ if hasattr(record, 'status'):
+ log_data['status'] = record.status
+ if hasattr(record, 'client_ip'):
+ log_data['client_ip'] = record.client_ip
+ if hasattr(record, 'host'):
+ log_data['host'] = record.host
+ if hasattr(record, 'port'):
+ log_data['port'] = record.port
+ if hasattr(record, 'debug'):
+ log_data['debug'] = record.debug
+
+ # Add exception info if present
+ if record.exc_info:
+ log_data['exception'] = self.formatException(record.exc_info)
+
+ return json.dumps(log_data)
+
+# ========== Logging Setup ==========
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+handler = logging.StreamHandler()
+handler.setFormatter(JSONFormatter())
+logger.addHandler(handler)
+
+# Disable werkzeug logger to avoid duplicate logs
+logging.getLogger('werkzeug').setLevel(logging.WARNING)
+
+# ========== Flask App Initialization ==========
+app = Flask(__name__)
+
+# ========== Configuration ==========
+HOST = os.getenv('HOST', '0.0.0.0')
+PORT = int(os.getenv('PORT', 5000))
+DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
+
+# ========== Visits Persistence ==========
+VISITS_FILE = os.getenv("VISITS_FILE", "/data/visits")
+VISITS_DIR = os.path.dirname(VISITS_FILE)
+VISITS_LOCK = Lock()
+
+def ensure_visits_file():
+ """Ensure visits directory and file exist."""
+ if VISITS_DIR:
+ os.makedirs(VISITS_DIR, exist_ok=True)
+ if not os.path.exists(VISITS_FILE):
+ with open(VISITS_FILE, "w", encoding="utf-8") as f:
+ f.write("0")
+
+def read_visits():
+ """Read visits count from file, fallback to 0."""
+ ensure_visits_file()
+ try:
+ with open(VISITS_FILE, "r", encoding="utf-8") as f:
+ raw = f.read().strip()
+ return int(raw) if raw else 0
+ except Exception:
+ logger.exception("Failed to read visits file, fallback to 0")
+ return 0
+
+def write_visits(value: int):
+ """Atomically write visits count to file."""
+ ensure_visits_file()
+ tmp_file = f"{VISITS_FILE}.tmp"
+ with open(tmp_file, "w", encoding="utf-8") as f:
+ f.write(str(value))
+ os.replace(tmp_file, VISITS_FILE)
+
+def increment_visits():
+ """Thread-safe increment of visits counter."""
+ with VISITS_LOCK:
+ current = read_visits()
+ new_value = current + 1
+ write_visits(new_value)
+ return new_value
+
+# ========== Application Start Time ==========
+START_TIME = datetime.now(timezone.utc)
+
+# Initialize visits file on startup
+ensure_visits_file()
+
+# ========== Prometheus Metrics ==========
+HTTP_REQUESTS_TOTAL = Counter(
+ "http_requests_total",
+ "Total HTTP requests",
+ ["method", "endpoint", "status_code"]
+)
+
+HTTP_REQUEST_DURATION_SECONDS = Histogram(
+ "http_request_duration_seconds",
+ "HTTP request duration in seconds",
+ ["method", "endpoint"]
+)
+
+HTTP_REQUESTS_IN_PROGRESS = Gauge(
+ "http_requests_in_progress",
+ "HTTP requests currently being processed"
+)
+
+# App-specific metrics
+ENDPOINT_CALLS = Counter(
+ "devops_info_endpoint_calls_total",
+ "DevOps info service endpoint calls",
+ ["endpoint"]
+)
+
+SYSTEM_INFO_DURATION_SECONDS = Histogram(
+ "devops_info_system_collection_seconds",
+ "Time spent collecting system info"
+)
+
+# ========== Request/Response Logging Middleware ==========
+@app.before_request
+def log_request_info():
+ """Log incoming HTTP request."""
+ request._start_time = time.perf_counter()
+ HTTP_REQUESTS_IN_PROGRESS.inc()
+ KNOWN_ENDPOINTS = {"/", "/health", "/metrics", "/visits"}
+ raw_endpoint = request.path
+ endpoint = raw_endpoint if raw_endpoint in KNOWN_ENDPOINTS else "__unknown__"
+
+ ENDPOINT_CALLS.labels(endpoint=endpoint).inc()
+ logger.info(
+ "Incoming request",
+ extra={
+ 'method': request.method,
+ 'path': endpoint,
+ 'client_ip': request.remote_addr
+ }
+ )
+
+@app.after_request
+def log_response_info(response):
+ """Log HTTP response."""
+ try:
+ duration = time.perf_counter() - getattr(request, "_start_time", time.perf_counter())
+ except Exception:
+ duration = 0.0
+
+ KNOWN_ENDPOINTS = {"/", "/health", "/metrics", "/visits"}
+
+ raw_endpoint = request.path
+ endpoint = raw_endpoint if raw_endpoint in KNOWN_ENDPOINTS else "__unknown__"
+ method = request.method
+ status_code = str(response.status_code)
+
+ HTTP_REQUEST_DURATION_SECONDS.labels(method=method, endpoint=endpoint).observe(duration)
+ HTTP_REQUESTS_TOTAL.labels(method=method, endpoint=endpoint, status_code=status_code).inc()
+ HTTP_REQUESTS_IN_PROGRESS.dec()
+ logger.info(
+ "Request completed",
+ extra={
+ 'method': request.method,
+ 'path': endpoint,
+ 'status': response.status_code,
+ 'client_ip': request.remote_addr
+ }
+ )
+ return response
+
+
+# ========== Helper Functions ==========
+def get_system_info():
+ """Collect system information."""
+ with SYSTEM_INFO_DURATION_SECONDS.time():
+ return {
+ 'hostname': socket.gethostname(),
+ 'platform': platform.system(),
+ 'platform_version': platform.platform(),
+ 'architecture': platform.machine(),
+ 'cpu_count': os.cpu_count(),
+ 'python_version': platform.python_version()
+ }
+
+
+def get_uptime():
+ """Calculate application uptime."""
+ delta = datetime.now(timezone.utc) - START_TIME
+ seconds = int(delta.total_seconds())
+ hours, remainder = divmod(seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ return {
+ 'seconds': int(delta.total_seconds()),
+ 'human': f"{hours} hours, {minutes} minutes"
+ }
+
+
+def get_runtime_info():
+ """Collect runtime information."""
+ uptime = get_uptime()
+ now = datetime.now(timezone.utc)
+ return {
+ 'uptime_seconds': uptime['seconds'],
+ 'uptime_human': uptime['human'],
+ 'current_time': now.isoformat(),
+ 'timezone': str(now.tzinfo)
+ }
+
+
+def get_request_info():
+ """Collect request information."""
+ return {
+ 'client_ip': request.remote_addr,
+ 'user_agent': request.headers.get('User-Agent', 'unknown'),
+ 'method': request.method,
+ 'path': request.path
+ }
+
+
+def get_service_info():
+ """Service metadata."""
+ return {
+ 'name': 'devops-info-service',
+ 'version': '2.1.0',
+ 'description': 'DevOps course info service with JSON logging and persistent visits counter',
+ 'framework': 'Flask'
+ }
+
+
+def get_endpoints_list():
+ """List available endpoints."""
+ return [
+ {'path': '/', 'method': 'GET', 'description': 'Service information and increment visits counter'},
+ {'path': '/health', 'method': 'GET', 'description': 'Health check'},
+ {'path': '/metrics', 'method': 'GET', 'description': 'Prometheus metrics'},
+ {'path': '/visits', 'method': 'GET', 'description': 'Current visits counter'}
+ ]
+
+
+# ========== Main Endpoint ==========
+@app.route('/')
+def index():
+ """Main endpoint - returns comprehensive service and system information."""
+ visits = increment_visits()
+ logger.info(
+ "Index endpoint accessed",
+ extra={
+ 'method': request.method,
+ 'path': request.path,
+ 'client_ip': request.remote_addr
+ }
+ )
+ response_data = {
+ 'service': get_service_info(),
+ 'system': get_system_info(),
+ 'runtime': get_runtime_info(),
+ 'request': get_request_info(),
+ 'visits': visits,
+ 'endpoints': get_endpoints_list()
+ }
+ return jsonify(response_data)
+
+
+# ========== Visits Endpoint ==========
+@app.route('/visits')
+def visits():
+ """Return current visits count without incrementing."""
+ count = read_visits()
+ return jsonify({
+ 'visits': count,
+ 'file': VISITS_FILE
+ })
+
+# ========== Health Check Endpoint ==========
+@app.route('/health')
+def health():
+ """Health check endpoint for monitoring."""
+ logger.info(
+ "Health check accessed",
+ extra={
+ 'method': request.method,
+ 'path': request.path,
+ 'client_ip': request.remote_addr
+ }
+ )
+ return jsonify({
+ 'status': 'healthy',
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'uptime_seconds': get_uptime()['seconds']
+ })
+
+# ========== Metrics Endpoint ==========
+@app.route('/metrics')
+def metrics():
+ """Prometheus metrics endpoint."""
+ return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST}
+
+
+# ========== Error Handlers ==========
+@app.errorhandler(404)
+def not_found(error):
+ logger.warning(
+ "404 Not Found",
+ extra={
+ 'method': request.method,
+ 'path': request.path,
+ 'client_ip': request.remote_addr,
+ 'status': 404
+ }
+ )
+ return jsonify({
+ 'error': 'Not Found',
+ 'message': 'Endpoint does not exist'
+ }), 404
+
+
+@app.errorhandler(500)
+def internal_error(error):
+ logger.error(
+ "Internal Server Error",
+ extra={
+ 'method': request.method,
+ 'path': request.path,
+ 'client_ip': request.remote_addr,
+ 'status': 500
+ },
+ exc_info=True
+ )
+ return jsonify({
+ 'error': 'Internal Server Error',
+ 'message': 'An unexpected error occurred'
+ }), 500
+
+
+# ========== Application Entry Point ==========
+if __name__ == '__main__':
+ logger.info(
+ "Starting DevOps Info Service",
+ extra={
+ 'host': HOST,
+ 'port': PORT,
+ 'debug': DEBUG
+ }
+ )
+ app.run(host=HOST, port=PORT, debug=DEBUG)
\ No newline at end of file
diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md
new file mode 100644
index 0000000000..2f2111cfd9
--- /dev/null
+++ b/app_python/docs/LAB01.md
@@ -0,0 +1,151 @@
+# Lab 1 — DevOps Info Service: Implementation Report
+
+## Framework Selection
+
+I selected **Flask** as the web framework for this project, because Flask provides the optimal balance of simplicity, flexibility, and learning opportunity. Flask's minimal approach allows us to focus on DevOps practices while still creating a production-ready service.
+
+
+| Framework | Pros | Cons | Decision Reason |
+|-----------|------|------|----------------|
+| **Flask** | - Lightweight and minimal
- Perfect for microservices
- Excellent for educational purposes | - Less built-in features compared to Django
- Manual setup for async operations | Ideal for simple API services, easier for beginners learning DevOps concepts, sufficient for our monitoring service needs |
+| FastAPI | - Native async support
- Auto-generated OpenAPI documentation
- High performance
- Type hints integration | - Steeper learning curve for Python beginners
- More overhead for a simple application | Considered for modern features but Flask's simplicity better fits educational goals |
+| Django | - Comprehensive security features
- Excellent for full-stack applications | - Heavyweight
- Overkill for a simple API service
- Longer setup time | Too complex for this microservice; would add unnecessary complexity |
+
+## Best Practices Applied
+
+### 1. Clean Code Organization
+- **Modular Structure:** Separated concerns into distinct functions (`get_system_info`, `get_runtime_info`, etc.) for better understanding
+- **Descriptive Naming:** Functions clearly indicate their purpose
+- **Import Grouping:** Organized imports in logical groups (standard library, third-party)
+- **Minimal Comments:** Comments only where business logic needs explanation
+- **PEP 8 Compliance:** Followed Python style guide for indentation, line length, and naming conventions
+- **Comprehensive Error Handling:** Implemented error handlers for common HTTP status codes
+- **Structured Logging:** Timestamps help debug timing issues
+- **Environment-Based Configuration:** No hardcoded values in source code, easy configuration for different environments
+
+**Code Example:**
+```python
+def get_system_info():
+ """Collect comprehensive system information."""
+ return {
+ 'hostname': socket.gethostname(),
+ 'platform': platform.system(),
+ 'platform_version': platform.platform(),
+ 'architecture': platform.machine(),
+ 'cpu_count': os.cpu_count(),
+ 'python_version': platform.python_version()
+ }
+```
+
+## API Documentation
+
+### GET /
+
+- Returns comprehensive service and system information (endpoints, request, runtime, system info, service info).
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+### GET /health
+
+- Health check endpoint for monitoring systems and Kubernetes probes.
+
+ **Request:**
+
+ ```bash
+ curl http://localhost:5000/health
+ ```
+
+ **Status Codes:**
+
+ - 200 OK: Service is healthy
+ - 4xx: Service is unhealthy (implemented in future labs)
+ - 5xx: Service is unhealthy (implemented in future labs)
+
+ **Response Example:**
+ 
+
+## Testing commands
+
+```bash
+python app.py #Default Configuration The service will start at: http://0.0.0.0:5000
+
+# Custom Configuration on Linux/Mac:
+PORT=8080 python app.py # Change port
+HOST=127.0.0.1 PORT=3000 python app.py # Change host and port
+DEBUG=true python app.py # Enable debug mode
+
+# Custom Configuration on Windows PowerShell:
+$env:HOST="127.0.0.1"; $env:PORT=8080; python app.py
+```
+
+## Terminal Output Examples
+
+Application Startup:
+
+```text
+2026-01-28 22:02:58,501 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:8813 (DEBUG=False)
+ * Serving Flask app 'app'
+ * Debug mode: off
+```
+
+Request Logging:
+
+```text
+2026-01-28 22:03:13,068 - __main__ - INFO - Request to / from 127.0.0.1
+2026-01-28 22:03:13,116 - werkzeug - INFO - 127.0.0.1 - - [28/Jan/2026 22:03:13] "GET / HTTP/1.1" 200 -
+```
+
+## Challenges & Solutions
+
+### Windows Port Access Restrictions
+
+**Problem:**
+When trying to bind to certain ports in Windows, received error: "An attempt was made to access a socket in a way forbidden by its access permissions". This occurred regardless of which port was used with the command $env:HOST="127.0.0.1"; $env:PORT=3000; python app.py.
+
+**Root Cause:**
+Windows has strict socket permissions, and ports below 1024 often require administrator privileges. Additionally, Windows Firewall or antivirus software can block socket creation.
+
+**Solution:**
+
+1. Use ports above 1024: Changed to port 5000 or 8080 which don't require admin rights
+2. Run PowerShell as Administrator: For ports that require elevated privileges
+
+## GitHub Community
+
+### Importance of Starring Repositories
+Starring repositories on GitHub is crucial in open source development for several reasons:
+
+- **Discovery & Visibility:** Stars help projects gain visibility in GitHub search and recommendations. More stars often indicate a trustworthy and useful project.
+
+- **Appreciation & Motivation:** Stars show appreciation to maintainers, encouraging them to continue development and support.
+
+- **Bookmarking:** Stars serve as personal bookmarks for interesting projects you might want to reference or contribute to later.
+
+- **Professional Profile:** Your starred repositories appear on your GitHub profile, showcasing your interests and engagement with the developer community.
+
+
+### Value of Following Developers
+Following classmates, professors, and industry professionals provides significant benefits:
+
+- **Learning Opportunities:** You can see how experienced developers solve problems, structure projects, and write code.
+
+- **Networking:** Building connections within the DevOps community can lead to collaboration opportunities, job prospects, and knowledge sharing.
+
+- **Project Discovery:** Following others helps you discover new tools, libraries, and best practices through their activity and starred repositories.
+
+- **Community Building:** In educational settings, following classmates creates a supportive learning environment where you can share knowledge and help each other.
+
+- **Career Growth:** Following industry leaders keeps you updated on trends and shows potential employers your active engagement in the field.
\ No newline at end of file
diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md
new file mode 100644
index 0000000000..9c3eb5ad4f
--- /dev/null
+++ b/app_python/docs/LAB02.md
@@ -0,0 +1,94 @@
+# Lab 2 — Docker Containerization Report
+
+## Docker Best Practices Applied
+
+- **Non-root user:**
+ `RUN useradd -m appuser` and `USER appuser` are used to avoid running the app as root, which improves security by following the principle of least privilege.This prevents container breakout attacks where an attacker could gain root access to the host system if a vulnerability is exploited
+
+- **Specific base image version:**
+ `FROM python:3.13-slim` ensures reproducibility and reduces image size by using a minimal Python image. Smaller attack surface, faster download/startup times, reduced storage costs.
+ ```
+ python:3.13-full: ~900MB
+ python:3.13-slim: ~195MB (79% reduction)
+ ```
+
+- **Minimal context and .dockerignore:**
+ The `.dockerignore` file excludes files unnecessary for the container (e.g., `.git/`, `__pycache__/`, `tests/`). This reduces build time and image size. Excluding ``.git/`` prevents source code history, API keys, or secrets from accidentally being baked into the image.
+
+- **Layer ordering for caching:**
+ `COPY requirements.txt .` and `RUN pip install ...` are placed before `COPY . .` so Docker caches dependencies installation if only the source code changes. In automated pipelines, proper layer caching can reduce build times from minutes to seconds, saving computational resources and speeding up deployments.
+
+- **Only necessary files are copied:**
+ The Dockerfile copies only files required to run the application. Smaller images transfer faster over networks — crucial for scaling across multiple nodes in production.
+
+- **`WORKDIR` instruction:** Setting `/app` as working directory improves organization. Ensures all subsequent commands (`COPY`, `RUN`, `CMD`) execute from /app rather than relying on relative paths which can break.
+
+- **No-cache pip install:** `--no-cache-dir` flag reduces image size by not storing pip cache. Pip's cache (typically 50-100MB) contains downloaded package wheels for faster reinstalls. This cache is useless in a production container but adds significant bloat.
+
+- **`EXPOSE` instruction:** Documents the port the application listens on (5000). Tools like Docker Compose and Kubernetes read `EXPOSE` to configure networking automatically.
+
+### Relevant Dockerfile snippets
+
+```dockerfile
+FROM python:3.13-slim
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY . .
+RUN useradd -m appuser
+USER appuser
+EXPOSE 5000
+CMD ["python", "app.py"]
+```
+
+## Image Information & Decisions
+
+- **Base image:** ``python:3.13-slim`` (Smaller, faster to load, fewer vulnerabilities).
+- Final image size: 195 MB
+- Compresed image size (in Dockerhub): 45.5 MB
+- Layer structure: Dependencies are installed before code is copied, which optimizes for build caching.
+ - Layer 1: python:3.13-slim base image (~195MB)
+ - Layer 2: WORKDIR /app
+ - Layer 3: COPY requirements.txt
+ - Layer 4: RUN pip install... (dependencies layer)
+ - Layer 5: COPY . . (application code)
+ - Layer 6: RUN useradd... (security layer)
+ - Layer 7: Metadata (EXPOSE, CMD)
+- Optimization choices: Kept the number of layers minimal, excluded unnecessary development files.
+
+## Build & Run Process
+
+- Build output
+ 
+- Run output
+ 
+- Sample endpoint test
+ 
+
+Docker Hub URL: https://hub.docker.com/r/spalkkina/devops-info-service
+
+## Technical Analysis
+- **Strategic Layer Design for Cache Efficiency:**
+
+ The Dockerfile follows a "least volatile → most volatile" layer ordering strategy. Dependencies (requirements.txt) change infrequently, while application code changes frequently. By isolating dependencies in early layers, we maximize Docker's build cache utilization. This is critical in CI/CD pipelines where 90% of builds involve only code changes.
+
+- **Layer order:**
+
+ Changing the order (e.g., copying all files before installing dependencies) would invalidate the cache when any source code changes, so `pip install` would run every build. If we moved COPY . . before RUN pip install, a 1-byte change in app.py would invalidate the pip install layer cache, adding 30+ seconds to every build
+- **Security:**
+
+ Non-root, minimal base image, no unnecessary files copied, Secrets Protection
+- **`.dockerignore` advantage:**
+
+ Prevents large/insecure/unneeded files from being included, which keeps the image smaller and more secure. Prevents credentials from being baked into the image. Reduces information available to attackers if image is compromised.
+
+
+
+## Challenges & Solutions
+
+**Problem:** Determining Actual Image Size. Initially confused about which size metric to report. Docker CLI shows uncompressed size, while Docker Hub shows compressed size.
+
+**Solution:** Learned to use multiple commands for comprehensive understanding
+
+**Learning:** The 195MB local image compresses to 45.5MB on Docker Hub due to layer deduplication and compression algorithms.
diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md
new file mode 100644
index 0000000000..a66a1198a5
--- /dev/null
+++ b/app_python/docs/LAB03.md
@@ -0,0 +1,148 @@
+# Lab 3 — Continuous Integration (CI/CD)
+
+## 1. Overview
+
+### Testing Framework: pytest
+I chose **pytest** for unit testing because:
+- **Simple syntax** — no boilerplate code, just `assert` statements
+- **Powerful fixtures** — easy to create test clients and shared resources
+- **Rich plugin ecosystem** — `pytest-cov` for coverage, integration with GitHub Actions
+- **Industry standard** — widely used in Python DevOps projects
+
+### Test Coverage
+| Endpoint | What is tested |
+|----------|----------------|
+| `GET /` | ✓ Status code 200
✓ JSON content type
✓ All 5 sections (service, system, runtime, request, endpoints)
✓ Service name, version, framework
✓ System fields presence and types
✓ Uptime format and validity
✓ Request info (method, path) |
+| `GET /health` | ✓ Status code 200
✓ Status = "healthy"
✓ Timestamp in ISO format
✓ Uptime seconds (positive integer) |
+| Error handling | ✓ 404 Not Found response structure
✓ Error message format |
+
+**Total tests:** 17 unit tests
+
+### CI Workflow Triggers
+The workflow runs on:
+- **Push** to `master` and `lab03` branches
+- **Pull request** targeting `master` branch
+
+**Why?**
+- Push triggers ensure every commit is tested
+- PR triggers catch issues before merging
+- Lab03 branch is included for active development
+
+### Versioning Strategy: Calendar Versioning (CalVer)
+I chose **CalVer** with format `YYYY.MM` (e.g., `2025.02`)
+
+**Rationale:**
+- This is a **service**, not a library — breaking changes don't require SemVer
+- Time-based versions are **easy to understand** and correlate with release dates
+- Perfect for **continuous deployment** — new version every month
+- Simple to implement in CI using `date` command
+
+**Docker tags:**
+- `spalkkina/devops-info-service:2026.02` — monthly version
+- `spalkkina/devops-info-service:latest` — most recent build
+
+---
+
+## 2. Workflow Evidence
+
+### Successful GitHub Actions Run
+🔗 [Link to workflow run](https://github.com/angel-palkina/DevOps-Core-Course/actions/runs/21921911777)
+
+### Tests Passing Locally
+
+
+### Docker Hub Image
+🔗 [Docker Hub repository](https://hub.docker.com/r/spalkkina/devops-info-service)
+
+| Tag | Size |
+|----------|-----------|
+| 2026.02 | 45.54 MB |
+| latest | 45.54 MB |
+
+### CI/CD Status
+
+
+
+## Best Practices Implemented
+- **Practice 1:** Dependency Caching
+
+ What: Caching pip packages using actions/setup-python@v5 built-in cache
+
+ Why: Speeds up workflow by ~104 seconds (no need to download packages every time)
+
+ Before: 2m 05s → After: 1m 01s
+
+- **Practice 2:** Job Dependencies (Fail Fast)
+
+ What: Docker job has needs: test — only runs if tests pass
+
+ Why: Prevents publishing broken images to Docker Hub
+
+- **Practice 3:** Conditional Execution
+
+ What: Docker job runs only on push, not on PRs
+
+ Why: Avoid pushing images from temporary PR branches
+
+- **Practice 4:** Security Scanning with Snyk
+
+ What: Integrated Snyk to scan dependencies for vulnerabilities
+
+ Why: Catch security issues before they reach production
+
+ Snyk Results: no vulnerable paths found.
+
+- **Practice 5:** Linting
+
+ What: flake8 runs on every commit
+
+ Why: Enforces code style consistency, catches syntax errors early
+
+## Key Decisions
+
+### Versioning Strategy: CalVer
+**Why CalVer?** This is a web service, not a shared library. Users don't need to know about breaking changes via version numbers — they just consume the latest API. CalVer clearly communicates when the image was built, which is more useful for operations teams.
+
+Alternative considered: SemVer — rejected because our app has no API consumers that pin versions.
+
+### Docker Tags
+What tags are created?
+
+- YYYY.MM (e.g., 2026.02) — monthly version
+
+- latest — points to the most recent build
+
+**Why two tags?**
+
+- latest is convenient for development and testing
+
+- Date-based tag provides a stable reference for production rollbacks
+
+### Workflow Triggers
+Why push + PR?
+
+- Push triggers ensure every commit is tested immediately
+
+- PR triggers prevent merging broken code into master
+
+Lab03 branch is included to test the workflow itself during development
+
+### Test Coverage
+What is tested?
+
+- All happy paths (200 OK responses)
+
+- Response structure and data types
+
+- Error handlers (404)
+
+- Helper functions
+
+What is NOT tested?
+
+- 500 error handler (requires mocking internal errors)
+
+- Actual hostname value (changes per environment)
+
+- IP address format (varies in CI)
+
diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png
new file mode 100644
index 0000000000..ac11fa5644
Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ
diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png
new file mode 100644
index 0000000000..cf67c921ae
Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ
diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png
new file mode 100644
index 0000000000..8f72401155
Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ
diff --git a/app_python/docs/screenshots/04 -terminal-build-output.png b/app_python/docs/screenshots/04 -terminal-build-output.png
new file mode 100644
index 0000000000..93764e5f0b
Binary files /dev/null and b/app_python/docs/screenshots/04 -terminal-build-output.png differ
diff --git a/app_python/docs/screenshots/05-run-app-output.png b/app_python/docs/screenshots/05-run-app-output.png
new file mode 100644
index 0000000000..baf13ebf1a
Binary files /dev/null and b/app_python/docs/screenshots/05-run-app-output.png differ
diff --git a/app_python/docs/screenshots/06-endpoint-test-with-docker.png b/app_python/docs/screenshots/06-endpoint-test-with-docker.png
new file mode 100644
index 0000000000..cb7be8e994
Binary files /dev/null and b/app_python/docs/screenshots/06-endpoint-test-with-docker.png differ
diff --git a/app_python/docs/screenshots/07-test-output.png b/app_python/docs/screenshots/07-test-output.png
new file mode 100644
index 0000000000..b2289cb27b
Binary files /dev/null and b/app_python/docs/screenshots/07-test-output.png differ
diff --git a/app_python/docs/screenshots/08-test-coverage-output.png b/app_python/docs/screenshots/08-test-coverage-output.png
new file mode 100644
index 0000000000..2f14bea072
Binary files /dev/null and b/app_python/docs/screenshots/08-test-coverage-output.png differ
diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt
new file mode 100644
index 0000000000..d3443530ee
--- /dev/null
+++ b/app_python/requirements-dev.txt
@@ -0,0 +1,10 @@
+Flask==2.3.3
+
+# Testing
+pytest==8.0.0
+pytest-cov==5.0.0
+
+# Linting
+pylint==3.0.3
+flake8==7.0.0
+black==24.2.0
\ No newline at end of file
diff --git a/app_python/requirements.txt b/app_python/requirements.txt
new file mode 100644
index 0000000000..0a03d45171
--- /dev/null
+++ b/app_python/requirements.txt
@@ -0,0 +1,5 @@
+Flask==3.1.0
+
+
+#metrics
+prometheus-client==0.23.1
\ No newline at end of file
diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py
new file mode 100644
index 0000000000..044e6f9d17
--- /dev/null
+++ b/app_python/tests/test_app.py
@@ -0,0 +1,192 @@
+"""
+Unit tests for DevOps Info Service Flask application.
+"""
+
+import pytest
+import sys
+import os
+from datetime import datetime
+
+# Добавляем путь к app.py чтобы импортировать
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from app import app, get_system_info, get_uptime, get_service_info
+
+@pytest.fixture
+def client():
+ """Фикстура: создаём тестовый клиент Flask"""
+ app.config['TESTING'] = True
+ with app.test_client() as client:
+ yield client
+
+# ========== ТЕСТЫ ДЛЯ ЭНДПОИНТА / ==========
+
+def test_index_status_code(client):
+ """Тест 1: Проверяем, что главная страница возвращает 200 OK"""
+ response = client.get('/')
+ assert response.status_code == 200
+
+def test_index_is_json(client):
+ """Тест 2: Проверяем, что ответ — это JSON"""
+ response = client.get('/')
+ assert response.is_json
+
+def test_index_has_required_sections(client):
+ """Тест 3: Проверяем, что есть все основные секции"""
+ response = client.get('/')
+ data = response.get_json()
+
+ assert 'service' in data
+ assert 'system' in data
+ assert 'runtime' in data
+ assert 'request' in data
+ assert 'endpoints' in data
+
+def test_index_service_info(client):
+ """Тест 4: Проверяем информацию о сервисе"""
+ response = client.get('/')
+ service = response.get_json()['service']
+
+ assert service['name'] == 'devops-info-service'
+ assert service['version'] == '1.0.0'
+ assert service['framework'] == 'Flask'
+ assert isinstance(service['description'], str)
+
+def test_index_system_info_fields(client):
+ """Тест 5: Проверяем, что системная информация содержит все поля"""
+ response = client.get('/')
+ system = response.get_json()['system']
+
+ expected_fields = ['hostname', 'platform', 'platform_version',
+ 'architecture', 'cpu_count', 'python_version']
+
+ for field in expected_fields:
+ assert field in system
+
+ # Проверяем типы данных
+ assert isinstance(system['hostname'], str)
+ assert isinstance(system['cpu_count'], int)
+ assert isinstance(system['python_version'], str)
+
+def test_index_runtime_info(client):
+ """Тест 6: Проверяем информацию о времени работы"""
+ response = client.get('/')
+ runtime = response.get_json()['runtime']
+
+ assert 'uptime_seconds' in runtime
+ assert 'uptime_human' in runtime
+ assert 'current_time' in runtime
+ assert 'timezone' in runtime
+
+ # uptime_seconds должен быть положительным числом
+ assert runtime['uptime_seconds'] >= 0
+ assert isinstance(runtime['uptime_seconds'], int)
+
+def test_index_request_info(client):
+ """Тест 7: Проверяем информацию о запросе"""
+ response = client.get('/')
+ request_info = response.get_json()['request']
+
+ assert 'client_ip' in request_info
+ assert 'user_agent' in request_info
+ assert 'method' in request_info
+ assert 'path' in request_info
+
+ assert request_info['method'] == 'GET'
+ assert request_info['path'] == '/'
+
+def test_index_endpoints_list(client):
+ """Тест 8: Проверяем список эндпоинтов"""
+ response = client.get('/')
+ endpoints = response.get_json()['endpoints']
+
+ assert isinstance(endpoints, list)
+ assert len(endpoints) >= 2
+
+ # Проверяем, что есть / и /health
+ paths = [e['path'] for e in endpoints]
+ assert '/' in paths
+ assert '/health' in paths
+
+# ========== ТЕСТЫ ДЛЯ ЭНДПОИНТА /HEALTH ==========
+
+def test_health_status_code(client):
+ """Тест 9: Проверяем, что health endpoint доступен"""
+ response = client.get('/health')
+ assert response.status_code == 200
+
+def test_health_is_json(client):
+ """Тест 10: Проверяем, что health возвращает JSON"""
+ response = client.get('/health')
+ assert response.is_json
+
+def test_health_response_structure(client):
+ """Тест 11: Проверяем структуру ответа health"""
+ response = client.get('/health')
+ data = response.get_json()
+
+ assert 'status' in data
+ assert 'timestamp' in data
+ assert 'uptime_seconds' in data
+
+ assert data['status'] == 'healthy'
+ assert data['uptime_seconds'] >= 0
+
+def test_health_timestamp_format(client):
+ """Тест 12: Проверяем, что timestamp в правильном формате"""
+ response = client.get('/health')
+ timestamp = response.get_json()['timestamp']
+
+ # Пробуем распарсить ISO формат даты
+ try:
+ datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
+ is_valid = True
+ except ValueError:
+ is_valid = False
+
+ assert is_valid
+
+# ========== ТЕСТЫ ДЛЯ ОБРАБОТЧИКОВ ОШИБОК ==========
+
+def test_404_not_found(client):
+ """Тест 13: Проверяем, что несуществующий путь возвращает 404"""
+ response = client.get('/non-existent-path')
+ assert response.status_code == 404
+
+ data = response.get_json()
+ assert 'error' in data
+ assert 'message' in data
+ assert data['error'] == 'Not Found'
+
+def test_method_not_allowed(client):
+ """Тест 14: Проверяем POST на GET endpoint"""
+ response = client.post('/')
+ assert response.status_code in [405, 404] # 405 Method Not Allowed
+
+# ========== ТЕСТЫ ДЛЯ ХЕЛПЕР-ФУНКЦИЙ ==========
+
+def test_get_system_info_function():
+ """Тест 15: Проверяем функцию get_system_info"""
+ info = get_system_info()
+
+ assert isinstance(info, dict)
+ assert 'hostname' in info
+ assert 'platform' in info
+ assert 'cpu_count' in info
+ assert info['cpu_count'] > 0
+
+def test_get_uptime_function():
+ """Тест 16: Проверяем функцию get_uptime"""
+ uptime = get_uptime()
+
+ assert 'seconds' in uptime
+ assert 'human' in uptime
+ assert uptime['seconds'] >= 0
+ assert isinstance(uptime['human'], str)
+
+def test_get_service_info_function():
+ """Тест 17: Проверяем функцию get_service_info"""
+ info = get_service_info()
+
+ assert info['name'] == 'devops-info-service'
+ assert info['version'] == '1.0.0'
\ No newline at end of file
diff --git a/data/visits b/data/visits
new file mode 100644
index 0000000000..f11c82a4cb
--- /dev/null
+++ b/data/visits
@@ -0,0 +1 @@
+9
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000..eac08dc170
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,18 @@
+version: "3.9"
+
+services:
+ devops-info:
+ build:
+ context: ./app_python
+ dockerfile: Dockerfile
+ container_name: devops-info
+ ports:
+ - "5000:5000"
+ environment:
+ - HOST=0.0.0.0
+ - PORT=5000
+ - DEBUG=False
+ - VISITS_FILE=/data/visits
+ volumes:
+ - ./data:/data
+ restart: unless-stopped
\ No newline at end of file
diff --git a/edge-api/.editorconfig b/edge-api/.editorconfig
new file mode 100644
index 0000000000..a727df347a
--- /dev/null
+++ b/edge-api/.editorconfig
@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
diff --git a/edge-api/.gitignore b/edge-api/.gitignore
new file mode 100644
index 0000000000..4138168d75
--- /dev/null
+++ b/edge-api/.gitignore
@@ -0,0 +1,167 @@
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+\*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+\*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+\*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+\*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.cache
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+.cache/
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+.cache
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.\*
+
+# wrangler project
+
+.dev.vars*
+!.dev.vars.example
+.env*
+!.env.example
+.wrangler/
diff --git a/edge-api/.prettierrc b/edge-api/.prettierrc
new file mode 100644
index 0000000000..5c7b5d3c7a
--- /dev/null
+++ b/edge-api/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "printWidth": 140,
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": true
+}
diff --git a/edge-api/.vscode/settings.json b/edge-api/.vscode/settings.json
new file mode 100644
index 0000000000..0126e59b82
--- /dev/null
+++ b/edge-api/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "wrangler.json": "jsonc"
+ }
+}
\ No newline at end of file
diff --git a/edge-api/AGENTS.md b/edge-api/AGENTS.md
new file mode 100644
index 0000000000..340506a599
--- /dev/null
+++ b/edge-api/AGENTS.md
@@ -0,0 +1,41 @@
+# Cloudflare Workers
+
+STOP. Your knowledge of Cloudflare Workers APIs and limits may be outdated. Always retrieve current documentation before any Workers, KV, R2, D1, Durable Objects, Queues, Vectorize, AI, or Agents SDK task.
+
+## Docs
+
+- https://developers.cloudflare.com/workers/
+- MCP: `https://docs.mcp.cloudflare.com/mcp`
+
+For all limits and quotas, retrieve from the product's `/platform/limits/` page. eg. `/workers/platform/limits`
+
+## Commands
+
+| Command | Purpose |
+|---------|---------|
+| `npx wrangler dev` | Local development |
+| `npx wrangler deploy` | Deploy to Cloudflare |
+| `npx wrangler types` | Generate TypeScript types |
+
+Run `wrangler types` after changing bindings in wrangler.jsonc.
+
+## Node.js Compatibility
+
+https://developers.cloudflare.com/workers/runtime-apis/nodejs/
+
+## Errors
+
+- **Error 1102** (CPU/Memory exceeded): Retrieve limits from `/workers/platform/limits/`
+- **All errors**: https://developers.cloudflare.com/workers/observability/errors/
+
+## Product Docs
+
+Retrieve API references and limits from:
+`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/`
+
+## Best Practices (conditional)
+
+If the application uses Durable Objects or Workflows, refer to the relevant best practices:
+
+- Durable Objects: https://developers.cloudflare.com/durable-objects/best-practices/rules-of-durable-objects/
+- Workflows: https://developers.cloudflare.com/workflows/build/rules-of-workflows/
diff --git a/edge-api/Screenshot 2026-05-10 210424-1.png b/edge-api/Screenshot 2026-05-10 210424-1.png
new file mode 100644
index 0000000000..ef97951ba4
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 210424-1.png differ
diff --git a/edge-api/Screenshot 2026-05-10 210424.png b/edge-api/Screenshot 2026-05-10 210424.png
new file mode 100644
index 0000000000..ef97951ba4
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 210424.png differ
diff --git a/edge-api/Screenshot 2026-05-10 213242.png b/edge-api/Screenshot 2026-05-10 213242.png
new file mode 100644
index 0000000000..caacac249c
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 213242.png differ
diff --git a/edge-api/Screenshot 2026-05-10 213254.png b/edge-api/Screenshot 2026-05-10 213254.png
new file mode 100644
index 0000000000..09be3dd860
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 213254.png differ
diff --git a/edge-api/Screenshot 2026-05-10 213526-1.png b/edge-api/Screenshot 2026-05-10 213526-1.png
new file mode 100644
index 0000000000..28584051c7
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 213526-1.png differ
diff --git a/edge-api/Screenshot 2026-05-10 213526-2.png b/edge-api/Screenshot 2026-05-10 213526-2.png
new file mode 100644
index 0000000000..28584051c7
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 213526-2.png differ
diff --git a/edge-api/Screenshot 2026-05-10 213526.png b/edge-api/Screenshot 2026-05-10 213526.png
new file mode 100644
index 0000000000..28584051c7
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 213526.png differ
diff --git a/edge-api/Screenshot 2026-05-10 222021.png b/edge-api/Screenshot 2026-05-10 222021.png
new file mode 100644
index 0000000000..992af70597
Binary files /dev/null and b/edge-api/Screenshot 2026-05-10 222021.png differ
diff --git a/edge-api/WORKERS.md b/edge-api/WORKERS.md
new file mode 100644
index 0000000000..f27343e05f
--- /dev/null
+++ b/edge-api/WORKERS.md
@@ -0,0 +1,80 @@
+# Lab 17 — Cloudflare Workers (WORKERS.md)
+
+## Deployment Summary
+- **Worker URL:** https://edge-api.s-palkina.workers.dev
+- **Main routes:**
+ - `/` – basic app info
+ - `/health` – health check
+ - `/edge` – edge metadata from `request.cf`
+ - `/counter` – KV-backed visit counter
+- **Configuration used:**
+ - `APP_NAME` and `COURSE_NAME` via `wrangler.jsonc` vars
+ - `API_TOKEN`, `ADMIN_EMAIL` as Wrangler secrets
+ - `SETTINGS` KV namespace binding
+
+## Evidence
+- Cloudflare dashboard (metrics + bindings):
+ ![alt text]()
+
+- Deployment + deployments list (terminal):
+![alt text]()
+
+- Example `/edge` JSON response
+![alt text]()
+
+- Logs: viewed with `npx wrangler tail` after adding `console.log`.
+![alt text]()
+
+- Persistant
+![alt text]()
+![alt text]()
+
+
+## Edge Execution Notes
+Workers execute at Cloudflare’s edge network automatically, close to the client.
+There is no “deploy to N regions” step because Workers are globally distributed by default.
+
+## Routing Concepts
+- **workers.dev**: default public URL for quick deployment.
+- **Routes**: attach a Worker to traffic of an existing Cloudflare zone.
+- **Custom Domains**: make the Worker serve as the origin for a domain/subdomain.
+
+## Configuration & Persistence
+- **Vars** (plaintext): stored in `wrangler.jsonc` — safe for non-secrets only.
+- **Secrets**: set via Wrangler, never stored in Git.
+- **KV Namespace**: persistent key-value storage; `/counter` proves values survive redeploy.
+
+## Observability & Operations
+- **Logs**: `npx wrangler tail` shows `console.log` entries.
+- **Metrics**: viewed in dashboard (requests, errors, CPU time).
+- **Deployments**: `npx wrangler deployments list` shows version history.
+
+### Rollback (described)
+If needed, rollback to a previous version can be done with:
+```bash
+npx wrangler rollback
+```
+This reverts the Worker to an earlier deployment in the history.
+
+## Kubernetes vs Cloudflare Workers Comparison
+
+| Aspect | Kubernetes | Cloudflare Workers |
+|--------|------------|--------------------|
+| Setup complexity | High (clusters, nodes, networking) | Low (CLI + deploy) |
+| Deployment speed | Slower (build + registry + rollout) | Fast (seconds) |
+| Global distribution | Manual regions | Built-in global edge |
+| Cost (for small apps) | Higher baseline | Very low / free tier |
+| State/persistence model | Pods + DB/cache | KV / Durable Objects |
+| Control/flexibility | Full control | Limited runtime |
+| Best use case | Complex services, long-running workloads | Lightweight APIs, edge logic |
+
+## When to Use Each
+- **Kubernetes**: complex systems, custom runtimes, long-running services, advanced networking.
+- **Workers**: fast global APIs, lightweight HTTP logic, edge caching, low-latency needs.
+
+**Recommendation:** Use Workers for small, globally distributed APIs. Use Kubernetes when full infrastructure control is required.
+
+## Reflection
+- **Easier than Kubernetes:** setup, deploy speed, global distribution.
+- **More constrained:** limited runtime, no Docker.
+- **What changed:** Workers run in a serverless edge runtime, so you design for short-lived requests and platform bindings instead of containers.
\ No newline at end of file
diff --git a/edge-api/package-lock.json b/edge-api/package-lock.json
new file mode 100644
index 0000000000..7adee25cf0
--- /dev/null
+++ b/edge-api/package-lock.json
@@ -0,0 +1,2913 @@
+{
+ "name": "edge-api",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "edge-api",
+ "version": "0.0.0",
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "^0.12.4",
+ "@types/node": "^25.6.2",
+ "typescript": "^5.5.2",
+ "vitest": "~3.2.0",
+ "wrangler": "^4.90.0"
+ }
+ },
+ "node_modules/@cloudflare/kv-asset-handler": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@cloudflare/unenv-preset": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "peerDependencies": {
+ "unenv": "2.0.0-rc.24",
+ "workerd": ">1.20260305.0 <2.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "workerd": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/vitest-pool-workers": {
+ "version": "0.12.21",
+ "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.12.21.tgz",
+ "integrity": "sha512-xqvqVR+qAhekXWaTNY36UtFFmHrz13yGUoWVGOu6LDC2ABiQqI1E1lQ3eUZY8KVB+1FXY/mP5dB6oD07XUGnPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cjs-module-lexer": "^1.2.3",
+ "esbuild": "0.27.3",
+ "miniflare": "4.20260310.0",
+ "wrangler": "4.72.0"
+ },
+ "peerDependencies": {
+ "@vitest/runner": "2.0.x - 3.2.x",
+ "@vitest/snapshot": "2.0.x - 3.2.x",
+ "vitest": "2.0.x - 3.2.x"
+ }
+ },
+ "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/kv-asset-handler": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz",
+ "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/unenv-preset": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz",
+ "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "peerDependencies": {
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "workerd": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": {
+ "version": "4.72.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.72.0.tgz",
+ "integrity": "sha512-bKkb8150JGzJZJWiNB2nu/33smVfawmfYiecA6rW4XH7xS23/jqMbgpdelM34W/7a1IhR66qeQGVqTRXROtAZg==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.4.2",
+ "@cloudflare/unenv-preset": "2.15.0",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.27.3",
+ "miniflare": "4.20260310.0",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260310.1"
+ },
+ "bin": {
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^4.20260310.1"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260310.1.tgz",
+ "integrity": "sha512-hF2VpoWaMb1fiGCQJqCY6M8I+2QQqjkyY4LiDYdTL5D/w6C1l5v1zhc0/jrjdD1DXfpJtpcSMSmEPjHse4p9Ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260310.1.tgz",
+ "integrity": "sha512-h/Vl3XrYYPI6yFDE27XO1QPq/1G1lKIM8tzZGIWYpntK3IN5XtH3Ee/sLaegpJ49aIJoqhF2mVAZ6Yw+Vk2gJw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260310.1.tgz",
+ "integrity": "sha512-XzQ0GZ8G5P4d74bQYOIP2Su4CLdNPpYidrInaSOuSxMw+HamsHaFrjVsrV2mPy/yk2hi6SY2yMbgKFK9YjA7vw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260310.1.tgz",
+ "integrity": "sha512-sxv4CxnN4ZR0uQGTFVGa0V4KTqwdej/czpIc5tYS86G8FQQoGIBiAIs2VvU7b8EROPcandxYHDBPTb+D9HIMPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260310.1.tgz",
+ "integrity": "sha512-+1ZTViWKJypLfgH/luAHCqkent0DEBjAjvO40iAhOMHRLYP/SPphLvr4Jpi6lb+sIocS8Q1QZL4uM5Etg1Wskg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@poppinss/colors": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^4.1.5"
+ }
+ },
+ "node_modules/@poppinss/dumper": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@sindresorhus/is": "^7.0.2",
+ "supports-color": "^10.0.0"
+ }
+ },
+ "node_modules/@poppinss/exception": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
+ "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
+ "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
+ "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
+ "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
+ "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
+ "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
+ "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
+ "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
+ "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
+ "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
+ "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
+ "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
+ "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
+ "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
+ "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
+ "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
+ "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
+ "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
+ "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
+ "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
+ "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
+ "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
+ "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
+ "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@speed-highlight/core": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz",
+ "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
+ "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/blake3-wasm": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/error-stack-parser-es": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/miniflare": {
+ "version": "4.20260310.0",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260310.0.tgz",
+ "integrity": "sha512-uC5vNPenFpDSj5aUU3wGSABG6UUqMr+Xs1m4AkCrTHo37F4Z6xcQw5BXqViTfPDVT/zcYH1UgTVoXhr1l6ZMXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "sharp": "^0.34.5",
+ "undici": "7.18.2",
+ "workerd": "1.20260310.1",
+ "ws": "8.18.0",
+ "youch": "4.1.0-beta.10"
+ },
+ "bin": {
+ "miniflare": "bootstrap.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
+ "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.3",
+ "@rollup/rollup-android-arm64": "4.60.3",
+ "@rollup/rollup-darwin-arm64": "4.60.3",
+ "@rollup/rollup-darwin-x64": "4.60.3",
+ "@rollup/rollup-freebsd-arm64": "4.60.3",
+ "@rollup/rollup-freebsd-x64": "4.60.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.3",
+ "@rollup/rollup-linux-arm64-musl": "4.60.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.3",
+ "@rollup/rollup-linux-loong64-musl": "4.60.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.3",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-gnu": "4.60.3",
+ "@rollup/rollup-linux-x64-musl": "4.60.3",
+ "@rollup/rollup-openbsd-x64": "4.60.3",
+ "@rollup/rollup-openharmony-arm64": "4.60.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.3",
+ "@rollup/rollup-win32-x64-gnu": "4.60.3",
+ "@rollup/rollup-win32-x64-msvc": "4.60.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
+ "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unenv": {
+ "version": "2.0.0-rc.24",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
+ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
+ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/workerd": {
+ "version": "1.20260310.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260310.1.tgz",
+ "integrity": "sha512-yawXhypXXHtArikJj15HOMknNGikpBbSg2ZDe6lddUbqZnJXuCVSkgc/0ArUeVMG1jbbGvpst+REFtKwILvRTQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20260310.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260310.1",
+ "@cloudflare/workerd-linux-64": "1.20260310.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260310.1",
+ "@cloudflare/workerd-windows-64": "1.20260310.1"
+ }
+ },
+ "node_modules/wrangler": {
+ "version": "4.90.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.90.0.tgz",
+ "integrity": "sha512-bmNIykl59TfCUn5xQgU7IWylSsPx3LQaPLMSAq2VQHt89CBrcj9qXQ0eYfjBCWA5XTBVgten391evt7xxtXwcA==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.5.0",
+ "@cloudflare/unenv-preset": "2.16.1",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.27.3",
+ "miniflare": "4.20260507.1",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260507.1"
+ },
+ "bin": {
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^4.20260507.1"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260507.1.tgz",
+ "integrity": "sha512-S85aMwcaPJUjKWDiG6iMMnioKWtPLACa6m0j/EhHR1GYfVpnxb974cBc6d25L+sf7jHWHJI2u5hGp0UTJ7MtXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260507.1.tgz",
+ "integrity": "sha512-GMEBu8Zp9Q97HLnf7bWJN4KjWpN5MxpeqdvHjBGWNl8UYprJI0k+Jkp89+Wh5S8vIon+HoVbDfOzPa7VwgL6Eg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260507.1.tgz",
+ "integrity": "sha512-QlrKEBdgA3uVc0Ok0Q3+0/CW0CTjgj5ySir1i1YY5FXVv0X6GpwtnB5umjunjF2MFprss+L+iFGZzxcSvMC1nA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260507.1.tgz",
+ "integrity": "sha512-eGbbupEtK2nh9V9Dhcx3vv3GTKeXqSVNgAEYVCCN0NGS9tl9HbMoHRX/4JL181FKXROMigWBCQVL//qPhsAzBQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260507.1.tgz",
+ "integrity": "sha512-dmClJ/E0BAcuDetQIZFqbeAXejWrG5pysGRMQ6T83Y0IW/7IAamY2zFEkAJ10I5xwZsdHuYsZtzlOxpEXpJs7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/wrangler/node_modules/miniflare": {
+ "version": "4.20260507.1",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260507.1.tgz",
+ "integrity": "sha512-PSXBiLExTdZ4UGO/raKCHQauUpYL7F880ZRB7j0+78Rv8h7TsdN2E/iEDK9sK2Y+SPQ5wJSeAa+rDeVKoZZoEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "sharp": "^0.34.5",
+ "undici": "7.24.8",
+ "workerd": "1.20260507.1",
+ "ws": "8.18.0",
+ "youch": "4.1.0-beta.10"
+ },
+ "bin": {
+ "miniflare": "bootstrap.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/wrangler/node_modules/undici": {
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz",
+ "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/wrangler/node_modules/workerd": {
+ "version": "1.20260507.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260507.1.tgz",
+ "integrity": "sha512-z7JhsFSe6+X1b5fUHaVpo15VM1IRMJiLofEkq8iKdCo+Veqc+FUg5lIsuz8NwePxuSKrXtO4ZQpGkQLbPVXFhg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20260507.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260507.1",
+ "@cloudflare/workerd-linux-64": "1.20260507.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260507.1",
+ "@cloudflare/workerd-windows-64": "1.20260507.1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "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
+ }
+ }
+ },
+ "node_modules/youch": {
+ "version": "4.1.0-beta.10",
+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@poppinss/dumper": "^0.6.4",
+ "@speed-highlight/core": "^1.2.7",
+ "cookie": "^1.0.2",
+ "youch-core": "^0.3.3"
+ }
+ },
+ "node_modules/youch-core": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/exception": "^1.2.2",
+ "error-stack-parser-es": "^1.0.5"
+ }
+ }
+ }
+}
diff --git a/edge-api/package.json b/edge-api/package.json
new file mode 100644
index 0000000000..4452b7a7f3
--- /dev/null
+++ b/edge-api/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "edge-api",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "deploy": "wrangler deploy",
+ "dev": "wrangler dev",
+ "start": "wrangler dev",
+ "test": "vitest",
+ "cf-typegen": "wrangler types"
+ },
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "^0.12.4",
+ "@types/node": "^25.6.2",
+ "typescript": "^5.5.2",
+ "vitest": "~3.2.0",
+ "wrangler": "^4.90.0"
+ }
+}
\ No newline at end of file
diff --git a/edge-api/src/index.ts b/edge-api/src/index.ts
new file mode 100644
index 0000000000..9cda88e0b4
--- /dev/null
+++ b/edge-api/src/index.ts
@@ -0,0 +1,45 @@
+export interface Env {
+ APP_NAME: string;
+ API_TOKEN: string;
+ ADMIN_EMAIL: string;
+ SETTINGS: KVNamespace;
+}
+
+export default {
+ async fetch(request: Request, env: Env): Promise {
+ const url = new URL(request.url);
+ console.log("path", url.pathname, "colo", request.cf?.colo);
+
+ if (url.pathname === "/health") {
+ return Response.json({ status: "ok" });
+ }
+
+ if (url.pathname === "/") {
+ return Response.json({
+ app: env.APP_NAME,
+ message: "Hello from Cloudflare Workers",
+ timestamp: new Date().toISOString(),
+ });
+ }
+
+ if (url.pathname === "/edge") {
+ return Response.json({
+ colo: request.cf?.colo,
+ country: request.cf?.country,
+ city: request.cf?.city,
+ asn: request.cf?.asn,
+ httpProtocol: request.cf?.httpProtocol,
+ tlsVersion: request.cf?.tlsVersion,
+ });
+ }
+
+ if (url.pathname === "/counter") {
+ const raw = await env.SETTINGS.get("visits");
+ const visits = Number(raw ?? "0") + 1;
+ await env.SETTINGS.put("visits", String(visits));
+ return Response.json({ visits });
+ }
+
+ return new Response("Not Found", { status: 404 });
+ },
+};
\ No newline at end of file
diff --git a/edge-api/tsconfig.json b/edge-api/tsconfig.json
new file mode 100644
index 0000000000..8c98cdbece
--- /dev/null
+++ b/edge-api/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "target": "es2024",
+ /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ "lib": ["es2024"],
+ /* Specify what JSX code is generated. */
+ "jsx": "react-jsx",
+
+ /* Specify what module code is generated. */
+ "module": "es2022",
+ /* Specify how TypeScript looks up a file from a given module specifier. */
+ "moduleResolution": "Bundler",
+ /* Enable importing .json files */
+ "resolveJsonModule": true,
+
+ /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+ "allowJs": true,
+ /* Enable error reporting in type-checked JavaScript files. */
+ "checkJs": false,
+
+ /* Disable emitting files from a compilation. */
+ "noEmit": true,
+
+ /* Ensure that each file can be safely transpiled without relying on other imports. */
+ "isolatedModules": true,
+ /* Allow 'import x from y' when a module doesn't have a default export. */
+ "allowSyntheticDefaultImports": true,
+ /* Ensure that casing is correct in imports. */
+ "forceConsistentCasingInFileNames": true,
+
+ /* Enable all strict type-checking options. */
+ "strict": true,
+
+ /* Skip type checking all .d.ts files. */
+ "skipLibCheck": true,
+ "types": [
+ "./worker-configuration.d.ts",
+ "node"
+ ]
+ },
+ "exclude": ["test"],
+ "include": ["worker-configuration.d.ts", "src/**/*.ts"]
+}
diff --git a/edge-api/vitest.config.mts b/edge-api/vitest.config.mts
new file mode 100644
index 0000000000..7ccad75efa
--- /dev/null
+++ b/edge-api/vitest.config.mts
@@ -0,0 +1,11 @@
+import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
+
+export default defineWorkersConfig({
+ test: {
+ poolOptions: {
+ workers: {
+ wrangler: { configPath: "./wrangler.jsonc" },
+ },
+ },
+ },
+});
diff --git a/edge-api/worker-configuration.d.ts b/edge-api/worker-configuration.d.ts
new file mode 100644
index 0000000000..1c8133e210
--- /dev/null
+++ b/edge-api/worker-configuration.d.ts
@@ -0,0 +1,13550 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: b739a9c19cff1463949c4db47674ed86)
+// Runtime types generated with workerd@1.20260507.1 2026-05-10 nodejs_compat
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./src/index");
+ }
+ interface Env {
+ }
+}
+interface Env extends Cloudflare.Env {}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. 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
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never;
+/**
+ * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string);
+ /**
+ * The **`message`** read-only property of the a message or description associated with the given error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
+ */
+ readonly message: string;
+ /**
+ * The **`name`** read-only property of the one of the strings associated with an error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
+ */
+ readonly name: string;
+ /**
+ * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number;
+ static readonly INDEX_SIZE_ERR: number;
+ static readonly DOMSTRING_SIZE_ERR: number;
+ static readonly HIERARCHY_REQUEST_ERR: number;
+ static readonly WRONG_DOCUMENT_ERR: number;
+ static readonly INVALID_CHARACTER_ERR: number;
+ static readonly NO_DATA_ALLOWED_ERR: number;
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ static readonly NOT_FOUND_ERR: number;
+ static readonly NOT_SUPPORTED_ERR: number;
+ static readonly INUSE_ATTRIBUTE_ERR: number;
+ static readonly INVALID_STATE_ERR: number;
+ static readonly SYNTAX_ERR: number;
+ static readonly INVALID_MODIFICATION_ERR: number;
+ static readonly NAMESPACE_ERR: number;
+ static readonly INVALID_ACCESS_ERR: number;
+ static readonly VALIDATION_ERR: number;
+ static readonly TYPE_MISMATCH_ERR: number;
+ static readonly SECURITY_ERR: number;
+ static readonly NETWORK_ERR: number;
+ static readonly ABORT_ERR: number;
+ static readonly URL_MISMATCH_ERR: number;
+ static readonly QUOTA_EXCEEDED_ERR: number;
+ static readonly TIMEOUT_ERR: number;
+ static readonly INVALID_NODE_TYPE_ERR: number;
+ static readonly DATA_CLONE_ERR: number;
+ get stack(): any;
+ set stack(value: any);
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent;
+ scheduled: ScheduledEvent;
+ queue: QueueEvent;
+ unhandledrejection: PromiseRejectionEvent;
+ rejectionhandled: PromiseRejectionEvent;
+};
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget;
+}
+/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
+ * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
+ */
+interface Console {
+ "assert"(condition?: boolean, ...data: any[]): void;
+ /**
+ * The **`console.clear()`** static method clears the console if possible.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
+ */
+ clear(): void;
+ /**
+ * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
+ */
+ count(label?: string): void;
+ /**
+ * The **`console.countReset()`** static method resets counter used with console/count_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
+ */
+ countReset(label?: string): void;
+ /**
+ * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
+ */
+ debug(...data: any[]): void;
+ /**
+ * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)
+ */
+ dir(item?: any, options?: any): void;
+ /**
+ * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)
+ */
+ dirxml(...data: any[]): void;
+ /**
+ * The **`console.error()`** static method outputs a message to the console at the 'error' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
+ */
+ error(...data: any[]): void;
+ /**
+ * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)
+ */
+ group(...data: any[]): void;
+ /**
+ * The **`console.groupCollapsed()`** static method creates a new inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)
+ */
+ groupCollapsed(...data: any[]): void;
+ /**
+ * The **`console.groupEnd()`** static method exits the current inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)
+ */
+ groupEnd(): void;
+ /**
+ * The **`console.info()`** static method outputs a message to the console at the 'info' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)
+ */
+ info(...data: any[]): void;
+ /**
+ * The **`console.log()`** static method outputs a message to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
+ */
+ log(...data: any[]): void;
+ /**
+ * The **`console.table()`** static method displays tabular data as a table.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)
+ */
+ table(tabularData?: any, properties?: string[]): void;
+ /**
+ * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)
+ */
+ time(label?: string): void;
+ /**
+ * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)
+ */
+ timeEnd(label?: string): void;
+ /**
+ * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
+ */
+ timeLog(label?: string, ...data: any[]): void;
+ timeStamp(label?: string): void;
+ /**
+ * The **`console.trace()`** static method outputs a stack trace to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)
+ */
+ trace(...data: any[]): void;
+ /**
+ * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)
+ */
+ warn(...data: any[]): void;
+}
+declare const console: Console;
+type BufferSource = ArrayBufferView | ArrayBuffer;
+type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string);
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string);
+ }
+ type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";
+ interface GlobalDescriptor {
+ value: ValueType;
+ mutable?: boolean;
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+ value: any;
+ valueOf(): any;
+ }
+ type ImportValue = ExportValue | number;
+ type ModuleImports = Record;
+ type Imports = Record;
+ type ExportValue = Function | Global | Memory | Table;
+ type Exports = Record;
+ class Instance {
+ constructor(module: Module, imports?: Imports);
+ readonly exports: Exports;
+ }
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ shared?: boolean;
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+ readonly buffer: ArrayBuffer;
+ grow(delta: number): number;
+ }
+ type ImportExportKind = "function" | "global" | "memory" | "table";
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind;
+ name: string;
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind;
+ module: string;
+ name: string;
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[];
+ static exports(module: Module): ModuleExportDescriptor[];
+ static imports(module: Module): ModuleImportDescriptor[];
+ }
+ type TableKind = "anyfunc" | "externref";
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any);
+ readonly length: number;
+ get(index: number): any;
+ grow(delta: number, value?: any): number;
+ set(index: number, value?: any): void;
+ }
+ function instantiate(module: Module, imports?: Imports): Promise;
+ function validate(bytes: BufferSource): boolean;
+}
+/**
+ * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException;
+ WorkerGlobalScope: typeof WorkerGlobalScope;
+ btoa(data: string): string;
+ atob(data: string): string;
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+ setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearTimeout(timeoutId: number | null): void;
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+ setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearInterval(timeoutId: number | null): void;
+ queueMicrotask(task: Function): void;
+ structuredClone(value: T, options?: StructuredSerializeOptions): T;
+ reportError(error: any): void;
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+ self: ServiceWorkerGlobalScope;
+ crypto: Crypto;
+ caches: CacheStorage;
+ scheduler: Scheduler;
+ performance: Performance;
+ Cloudflare: Cloudflare;
+ readonly origin: string;
+ Event: typeof Event;
+ ExtendableEvent: typeof ExtendableEvent;
+ CustomEvent: typeof CustomEvent;
+ PromiseRejectionEvent: typeof PromiseRejectionEvent;
+ FetchEvent: typeof FetchEvent;
+ TailEvent: typeof TailEvent;
+ TraceEvent: typeof TailEvent;
+ ScheduledEvent: typeof ScheduledEvent;
+ MessageEvent: typeof MessageEvent;
+ CloseEvent: typeof CloseEvent;
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;
+ ReadableStream: typeof ReadableStream;
+ WritableStream: typeof WritableStream;
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;
+ TransformStream: typeof TransformStream;
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
+ CountQueuingStrategy: typeof CountQueuingStrategy;
+ ErrorEvent: typeof ErrorEvent;
+ MessageChannel: typeof MessageChannel;
+ MessagePort: typeof MessagePort;
+ EventSource: typeof EventSource;
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
+ ReadableByteStreamController: typeof ReadableByteStreamController;
+ WritableStreamDefaultController: typeof WritableStreamDefaultController;
+ TransformStreamDefaultController: typeof TransformStreamDefaultController;
+ CompressionStream: typeof CompressionStream;
+ DecompressionStream: typeof DecompressionStream;
+ TextEncoderStream: typeof TextEncoderStream;
+ TextDecoderStream: typeof TextDecoderStream;
+ Headers: typeof Headers;
+ Body: typeof Body;
+ Request: typeof Request;
+ Response: typeof Response;
+ WebSocket: typeof WebSocket;
+ WebSocketPair: typeof WebSocketPair;
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;
+ AbortController: typeof AbortController;
+ AbortSignal: typeof AbortSignal;
+ TextDecoder: typeof TextDecoder;
+ TextEncoder: typeof TextEncoder;
+ navigator: Navigator;
+ Navigator: typeof Navigator;
+ URL: typeof URL;
+ URLSearchParams: typeof URLSearchParams;
+ URLPattern: typeof URLPattern;
+ Blob: typeof Blob;
+ File: typeof File;
+ FormData: typeof FormData;
+ Crypto: typeof Crypto;
+ SubtleCrypto: typeof SubtleCrypto;
+ CryptoKey: typeof CryptoKey;
+ CacheStorage: typeof CacheStorage;
+ Cache: typeof Cache;
+ FixedLengthStream: typeof FixedLengthStream;
+ IdentityTransformStream: typeof IdentityTransformStream;
+ HTMLRewriter: typeof HTMLRewriter;
+}
+declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void;
+declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void;
+/**
+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(value: T, options?: StructuredSerializeOptions): T;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+declare const self: ServiceWorkerGlobalScope;
+/**
+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+* The Workers runtime implements the full surface of this API, but with some differences in
+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+* compared to those implemented in most browsers.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+*/
+declare const crypto: Crypto;
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare const caches: CacheStorage;
+declare const scheduler: Scheduler;
+/**
+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+* as well as timing of subrequests and other operations.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+*/
+declare const performance: Performance;
+declare const Cloudflare: Cloudflare;
+declare const origin: string;
+declare const navigator: Navigator;
+interface TestController {
+}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void;
+ passThroughOnException(): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ cache?: CacheContext;
+ tracing?: Tracing;
+}
+type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise;
+type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise;
+type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise;
+interface ExportedHandler {
+ fetch?: ExportedHandlerFetchHandler;
+ connect?: ExportedHandlerConnectHandler;
+ tail?: ExportedHandlerTailHandler;
+ trace?: ExportedHandlerTraceHandler;
+ tailStream?: ExportedHandlerTailStreamHandler;
+ scheduled?: ExportedHandlerScheduledHandler;
+ test?: ExportedHandlerTestHandler;
+ email?: EmailExportedHandler;
+ queue?: ExportedHandlerQueueHandler;
+}
+interface StructuredSerializeOptions {
+ transfer?: any[];
+}
+declare abstract class Navigator {
+ sendBeacon(url: string, body?: BodyInit): boolean;
+ readonly userAgent: string;
+ readonly hardwareConcurrency: number;
+ readonly platform: string;
+ readonly language: string;
+ readonly languages: string[];
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean;
+ readonly retryCount: number;
+ readonly scheduledTime: number;
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record;
+}
+interface CachePurgeError {
+ code: number;
+ message: string;
+}
+interface CachePurgeResult {
+ success: boolean;
+ errors: CachePurgeError[];
+}
+interface CachePurgeOptions {
+ tags?: string[];
+ pathPrefixes?: string[];
+ purgeEverything?: boolean;
+}
+interface CacheContext {
+ purge(options: CachePurgeOptions): Promise;
+}
+declare abstract class ColoLocalActorNamespace {
+ get(actorId: string): Fetcher;
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise;
+ connect?(socket: Socket): void | Promise;
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise;
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise;
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise;
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise;
+}
+type DurableObjectStub = Fetcher & {
+ readonly id: DurableObjectId;
+ readonly name?: string;
+};
+interface DurableObjectId {
+ toString(): string;
+ equals(other: DurableObjectId): boolean;
+ readonly name?: string;
+ readonly jurisdiction?: string;
+}
+declare abstract class DurableObjectNamespace {
+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;
+ idFromName(name: string): DurableObjectId;
+ idFromString(id: string): DurableObjectId;
+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace;
+}
+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high";
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction;
+}
+type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me";
+type DurableObjectRoutingMode = "primary-only";
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint;
+ routingMode?: DurableObjectRoutingMode;
+}
+interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {
+}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ readonly id: DurableObjectId;
+ readonly storage: DurableObjectStorage;
+ container?: Container;
+ facets: DurableObjectFacets;
+ blockConcurrencyWhile(callback: () => Promise): Promise;
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
+ getWebSockets(tag?: string): WebSocket[];
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;
+ getHibernatableWebSocketEventTimeout(): number | null;
+ getTags(ws: WebSocket): string[];
+ abort(reason?: string): void;
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise