9090/TCP 2m34s
+```
+
+## Dashboard Answers
+
+### Pod Resources: CPU/memory usage of your StatefulSet
+
+Due to the pods and the app itself being very lightweight, CPU and memory usage never went higher than initially allocated resources (100m CPU and 128Mi memory). Even under high load (I used multiple loops with curl), the initial resources were enough. You will see more detailed usages info in the next question.
+
+Example for pod 2:
+
+
+
+
+
+### Namespace Analysis: Which pods use most/least CPU in default namespace?
+
+I decided to use Prometheus for evidence, since the resource usage was really low, and didn't show up properly in Grafana.
+
+curl I used (the first count is much bigger since I previously tested only with pod 2)
+
+
+
+usage
+
+
+
+As we can see, all statefulset pods used roughly the same amount of CPU and memory resources. This is anticipated, because load balancing is used for routing traffic to different pods.
+
+### Node Metrics: Memory usage (% and MB), CPU cores
+
+CPU and Memory usage for the whole minikube node was:
+
+
+
+It is much higher than resources used in statefulset since the node contains all different namespaces in my minikube cluster.
+
+### Kubelet: How many pods/containers managed?
+
+16 pods and 41 containers
+
+
+
+### Network: Traffic for pods in default namespace
+
+
+
+### Alerts: How many active alerts? Check Alertmanager UI
+
+
+
+## Init Containers: Implementation and proof of success
+
+I implemented two init container patterns. First one downloads a file with wget into a shared emptyDir volume and the main container successfully accessed it from /data/index.html. Second one uses a wait-for-service init container that continuously checks the nginx service with wget and only starts the main container after the service becomes reachable.
+
+### Init container
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 0/1 Init:0/1 0 2s
+myapp-app-python-0 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-1 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-2 1/1 Running 3 (6h15m ago) 7d20h
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 0/1 PodInitializing 0 4s
+myapp-app-python-0 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-1 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-2 1/1 Running 3 (6h15m ago) 7d20h
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 1/1 Running 0 5s
+myapp-app-python-0 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-1 1/1 Running 3 (6h15m ago) 7d20h
+myapp-app-python-2 1/1 Running 3 (6h15m ago) 7d20h
+```
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl logs init-download-pod -c init-download
+Connecting to example.com (172.66.147.243:443)
+wget: note: TLS certificate validation not implemented
+saving to '/work-dir/index.html'
+index.html 100% |********************************| 528 0:00:00 ETA
+'/work-dir/index.html' saved
+```
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl exec init-download-pod -- cat /data/index.html
+Defaulted container "main-app" out of: main-app, init-download (init)
+Example DomainExample Domain
This domain is for use in documentation examples without needing permission. Avoid use in operations.
Learn more
+```
+
+### Waiting for service container
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl apply -f waiting.yaml
+pod/wait-service-pod created
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 1/1 Running 0 51m
+myapp-app-python-0 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-1 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-2 1/1 Running 3 (7h6m ago) 7d21h
+wait-service-pod 0/1 Init:0/1 0 22s
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl logs wait-service-pod -c wait-for-service
+wget: bad address 'myservice.default.svc.cluster.local'
+waiting for service
+wget: bad address 'myservice.default.svc.cluster.local'
+waiting for service
+wget: bad address 'myservice.default.svc.cluster.local'
+waiting for service
+wget: bad address 'myservice.default.svc.cluster.local'
+waiting for service
+wget: bad address 'myservice.default.svc.cluster.local'
+waiting for service
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl apply -f nginx.yaml
+deployment.apps/myservice created
+service/myservice created
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 1/1 Running 0 51m
+myapp-app-python-0 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-1 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-2 1/1 Running 3 (7h6m ago) 7d21h
+myservice-ffc6675d7-pmjfr 1/1 Running 0 3s
+wait-service-pod 0/1 Init:0/1 0 55s
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 1/1 Running 0 51m
+myapp-app-python-0 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-1 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-2 1/1 Running 3 (7h6m ago) 7d21h
+myservice-ffc6675d7-pmjfr 1/1 Running 0 5s
+wait-service-pod 0/1 PodInitializing 0 57s
+(devops) fountainer@Veronicas-MacBook-Air templates % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+init-download-pod 1/1 Running 0 51m
+myapp-app-python-0 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-1 1/1 Running 3 (7h6m ago) 7d21h
+myapp-app-python-2 1/1 Running 3 (7h6m ago) 7d21h
+myservice-ffc6675d7-pmjfr 1/1 Running 0 7s
+wait-service-pod 1/1 Running 0 59s
+```
+
+After service was discovered:
+
+```bash
+waiting for service
+
+
+
+Welcome to nginx!
+
+
+
+Welcome to nginx!
+If you see this page, nginx is successfully installed and working.
+Further configuration is required for the web server, reverse proxy,
+API gateway, load balancer, content cache, or other features.
+
+For online documentation and support please refer to
+nginx.org.
+To engage with the community please visit
+community.nginx.org.
+For enterprise grade support, professional services, additional
+security features and capabilities please refer to
+f5.com/nginx.
+
+Thank you for using nginx.
+
+
+```
\ No newline at end of file
diff --git a/app_python/k8s/README.md b/app_python/k8s/README.md
new file mode 100644
index 0000000000..20ea3e883d
--- /dev/null
+++ b/app_python/k8s/README.md
@@ -0,0 +1,301 @@
+# Documentation
+
+## Architecture Overview
+
+### Diagram or description of your deployment architecture
+
+```mermaid
+flowchart LR
+
+subgraph Kubernetes_Cluster["Minikube Kubernetes Cluster"]
+
+ S[Service node port
port 80 -> nodePort 30007]
+
+ subgraph Deployment["Deployment: my-app (5 replicas)"]
+ P1[Pod 1
app-python]
+ P2[Pod 2
app-python]
+ P3[Pod 3
app-python]
+ P4[Pod 4
app-python]
+ P5[Pod 5
app-python]
+ end
+
+end
+
+User[User / curl / browser]
+
+User -->|HTTP request
http://nodeIP:30007| S
+
+S -->|routes traffic via selector
app: my-app| P1
+S --> P2
+S --> P3
+S --> P4
+S --> P5
+
+P1 -->|/health /ready /metrics| AppLogic[(Flask App)]
+P2 --> AppLogic
+P3 --> AppLogic
+P4 --> AppLogic
+P5 --> AppLogic
+
+```
+
+### How many Pods, which Services, networking flow
+
+- I used 5 pods managed by a deployment and one NodePort service, where traffic goes from the service (port 80 / nodePort 30007) to the pods using the app: my-app label selector.
+
+### Resource allocation strategy
+
+- I defined small cpu and memory requests/limits (100m–500m cpu, 128Mi–256Mi memory) to keep the app stable and prevent it from using too many cluster resources.
+
+### Brief explanation of your chosen tool (minikube/kind) and why
+
+I used minikube because it’s easy to set up locally and lets me run a full kubernetes cluster on my machine, which is enough for testing deployments without needing a real cloud setup.
+
+## Manifest Files
+
+### Brief description of each manifest
+
+- Deployment: deployment.yml defines how my app runs in Kubernetes, including how many pods, which image to use, and how they are configured.
+
+- Service: service.yml exposes the app inside and outside the cluster by routing traffic to the pods created by the deployment.
+
+### Key configuration choices
+
+- Deployment: I set 3 replicas, added resource limits/requests, configured liveness and readiness probes, used labels for selection, and set a rolling update strategy
+
+- Service: I used NodePort type, matched the selector with app: my-app, set port 80 as the service port, and mapped it to container port 12345 with a fixed nodePort.
+
+### Why you chose specific values (replicas, resources, etc.)
+
+- Deployment: I used 3 replicas for basic availability, small cpu/memory values since the app is lightweight, and probes to make sure kubernetes can detect when the app is healthy and ready to serve traffic.
+
+- Service: I used NodePort so i can access the app locally with minikube, port 80 for convenience, targetPort 12345 to match the app, and a fixed nodePort (30007) to make testing easier.
+
+## Deployment Evidence
+
+### Successful cluster setup
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % minikube start
+
+😄 minikube v1.38.1 on Darwin 26.3 (arm64)
+✨ Using the docker driver based on existing profile
+👍 Starting "minikube" primary control-plane node in "minikube" cluster
+🚜 Pulling base image v0.0.50 ...
+🏃 Updating the running docker "minikube" container ...
+🐳 Preparing Kubernetes v1.35.1 on Docker 29.2.1 ...
+🔎 Verifying Kubernetes components...
+ ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
+🌟 Enabled addons: default-storageclass, storage-provisioner
+
+❗ /Applications/Docker.app/Contents/Resources/bin/kubectl is version 1.32.2, which may have incompatibilities with Kubernetes 1.35.1.
+ ▪ Want kubectl v1.35.1? Try 'minikube kubectl -- get pods -A'
+🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
+```
+### Output of kubectl cluster-info and kubectl get nodes
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl cluster-info
+Kubernetes control plane is running at https://127.0.0.1:51390
+CoreDNS is running at https://127.0.0.1:51390/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
+
+To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get nodes
+NAME STATUS ROLES AGE VERSION
+minikube Ready control-plane 6h45m v1.35.1
+```
+
+### kubectl get all output
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get all
+NAME READY STATUS RESTARTS AGE
+pod/my-app-deployment-6f67848dfb-kxbtv 1/1 Running 0 3m11s
+pod/my-app-deployment-6f67848dfb-mjq8x 1/1 Running 0 3m11s
+pod/my-app-deployment-6f67848dfb-vx95p 1/1 Running 0 3m11s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/kubernetes ClusterIP 10.96.0.1 443/TCP 6h58m
+
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/my-app-deployment 3/3 3 3 3m11s
+
+NAME DESIRED CURRENT READY AGE
+replicaset.apps/my-app-deployment-6f67848dfb 3 3 3 3m11s
+```
+### kubectl get pods,svc with detailed view
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get pods,svc
+NAME READY STATUS RESTARTS AGE
+pod/my-app-deployment-6f67848dfb-kxbtv 1/1 Running 0 3m35s
+pod/my-app-deployment-6f67848dfb-mjq8x 1/1 Running 0 3m35s
+pod/my-app-deployment-6f67848dfb-vx95p 1/1 Running 0 3m35s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/kubernetes ClusterIP 10.96.0.1 443/TCP 6h59m
+```
+
+### kubectl describe deployment showing replicas and strategy
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get pods
+NAME READY STATUS RESTARTS AGE
+my-app-deployment-6f67848dfb-kxbtv 1/1 Running 0 29s
+my-app-deployment-6f67848dfb-mjq8x 1/1 Running 0 29s
+my-app-deployment-6f67848dfb-vx95p 1/1 Running 0 29s
+```
+### Screenshot or curl output showing app working
+
+
+
+### Service deployment
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get services
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+kubernetes ClusterIP 10.96.0.1 443/TCP 38m
+my-app-service NodePort 10.98.179.244 80:30007/TCP 41s
+(devops) fountainer@Veronicas-MacBook-Air k8s % minikube service my-app-service
+┌───────────┬────────────────┬─────────────┬───────────────────────────┐
+│ NAMESPACE │ NAME │ TARGET PORT │ URL │
+├───────────┼────────────────┼─────────────┼───────────────────────────┤
+│ default │ my-app-service │ 80 │ http://192.168.49.2:30007 │
+└───────────┴────────────────┴─────────────┴───────────────────────────┘
+🔗 Starting tunnel for service my-app-service.
+┌───────────┬────────────────┬─────────────┬────────────────────────┐
+│ NAMESPACE │ NAME │ TARGET PORT │ URL │
+├───────────┼────────────────┼─────────────┼────────────────────────┤
+│ default │ my-app-service │ │ http://127.0.0.1:57348 │
+└───────────┴────────────────┴─────────────┴────────────────────────┘
+🎉 Opening service default/my-app-service in default browser...
+❗ Because you are using a Docker driver on darwin, the terminal needs to be open to run it.
+```
+```bash
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get services
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+kubernetes ClusterIP 10.96.0.1 443/TCP 42m
+my-app-service NodePort 10.98.179.244 80:30007/TCP 4m16s
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl describe service my-app-service
+Name: my-app-service
+Namespace: default
+Labels:
+Annotations:
+Selector: app=my-app
+Type: NodePort
+IP Family Policy: SingleStack
+IP Families: IPv4
+IP: 10.98.179.244
+IPs: 10.98.179.244
+Port: 80/TCP
+TargetPort: 12345/TCP
+NodePort: 30007/TCP
+Endpoints: 10.244.0.16:12345,10.244.0.14:12345,10.244.0.15:12345
+Session Affinity: None
+External Traffic Policy: Cluster
+Internal Traffic Policy: Cluster
+Events:
+(devops) fountainer@Veronicas-MacBook-Air k8s % kubectl get endpoints
+Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
+NAME ENDPOINTS AGE
+kubernetes 192.168.49.2:8443 42m
+my-app-service 10.244.0.14:12345,10.244.0.15:12345,10.244.0.16:12345 4m36s
+(devops) fountainer@Veronicas-MacBook-Air k8s %
+```
+
+## Operations Performed
+
+### Commands used to deploy
+
+- ```bash kubectl apply -f k8s/deployment.yml```
+- ```bash kubectl apply -f k8s/service.yml```
+- ```bash kubectl get pods```
+- ```bash kubectl get services ```
+
+### Scaling demonstration output
+
+
+
+### Rolling update demonstration output
+
+I changed ```bash image: fountainer/my-app:latest``` to ```bash image: fountainer/my-app:2026.03.26```.
+
+
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl rollout history deployment/my-app-deployment
+deployment.apps/my-app-deployment
+REVISION CHANGE-CAUSE
+1
+2
+3
+
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl rollout undo deployment/my-app-deployment
+deployment.apps/my-app-deployment rolled back
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl rollout status deployment/my-app-deployment
+Waiting for deployment "my-app-deployment" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "my-app-deployment" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "my-app-deployment" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "my-app-deployment" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "my-app-deployment" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 of 5 updated replicas are available...
+Waiting for deployment "my-app-deployment" rollout to finish: 4 of 5 updated replicas are available...
+deployment "my-app-deployment" successfully rolled out
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pods
+NAME READY STATUS RESTARTS AGE
+my-app-deployment-7b5479788b-bj4pt 1/1 Running 0 37s
+my-app-deployment-7b5479788b-n2pnb 1/1 Running 0 12s
+my-app-deployment-7b5479788b-nvzdr 1/1 Running 0 22s
+my-app-deployment-7b5479788b-q9g66 1/1 Running 0 36s
+my-app-deployment-7b5479788b-w2nc6 1/1 Running 0 22s
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course %
+```
+
+### Service access method and verification
+
+I accessed the app using ```bash minikube service my-app-service ``` and verified it by sending requests with curl to endpoints like /health and /ready.
+
+## Production Considerations
+
+### What health checks did you implement and why?
+
+I implemented a liveness probe on /health to restart unhealthy containers and a readiness probe on /ready to ensure pods start receiving traffic only when they are ready to work.
+
+### Resource limits rationale
+
+- I set limits to prevent resource overuse, and requests to guarantee the pod gets enough cpu and memory to run reliably.
+
+### How would you improve this for production?
+
+- I would add proper logging/monitoring like we did in the previous labs, add autoscaling, consider other update strategies (like canary update).
+
+### Monitoring and observability strategy
+
+- In previous labs we used Prometheus for metrics and loki & promtail for logs, also Grafana for dashboard representation
+
+## Challenges & Solutions
+
+### Issues encountered
+
+- I didn't work with NodePort before so I has to stydy it a little bit. Also I didn't know about minikube.
+
+### How you debugged (logs, describe, events)
+
+- I researched StackOverflow and other sources, such as documentation for kubernetes and minikube
+
+### What you learned about Kubernetes
+
+- I studied kubernetes in the SRE course last semester so I was already pretty familiar with it. We didn't use NodePort service though, and also didn't set up our own cluster since the course team provided us with it.
+
diff --git a/app_python/k8s/ROLLOUTS.md b/app_python/k8s/ROLLOUTS.md
new file mode 100644
index 0000000000..7a37244b4c
--- /dev/null
+++ b/app_python/k8s/ROLLOUTS.md
@@ -0,0 +1,252 @@
+# Documentation
+
+## Argo Rollouts Setup
+
+### Installation verification
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
+customresourcedefinition.apiextensions.k8s.io/analysisruns.argoproj.io created
+customresourcedefinition.apiextensions.k8s.io/analysistemplates.argoproj.io created
+customresourcedefinition.apiextensions.k8s.io/clusteranalysistemplates.argoproj.io created
+customresourcedefinition.apiextensions.k8s.io/experiments.argoproj.io created
+customresourcedefinition.apiextensions.k8s.io/rollouts.argoproj.io created
+serviceaccount/argo-rollouts created
+clusterrole.rbac.authorization.k8s.io/argo-rollouts created
+clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-admin created
+clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-edit created
+clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-view created
+clusterrolebinding.rbac.authorization.k8s.io/argo-rollouts created
+configmap/argo-rollouts-config created
+secret/argo-rollouts-notification-secret created
+service/argo-rollouts-metrics created
+deployment.apps/argo-rollouts created
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pods -n argo-rollouts
+NAME READY STATUS RESTARTS AGE
+argo-rollouts-5f64f8d68-zxx5z 1/1 Running 0 54s
+```
+```bash
+==> Fetching downloads for: kubectl-argo-rollouts
+✔︎ Formula kubectl-argo-rollouts (v1.8.3) Verified 130.1MB/130.1MB
+==> Installing kubectl-argo-rollouts from argoproj/tap
+```
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl argo rollouts version
+kubectl-argo-rollouts: v1.8.3+49fa151
+ BuildDate: 2025-06-04T22:19:21Z
+ GitCommit: 49fa1516cf71672b69e265267da4e1d16e1fe114
+ GitTreeState: clean
+ GoVersion: go1.23.9
+ Compiler: gc
+ Platform: darwin/amd64
+```
+
+### Dashboard access
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pods -n argo-rollouts
+NAME READY STATUS RESTARTS AGE
+argo-rollouts-5f64f8d68-zxx5z 1/1 Running 0 12m
+argo-rollouts-dashboard-755bbc64c-pnkl6 1/1 Running 0 28s
+```
+
+
+
+### Understand Rollout vs Deployment
+
+Rollout CRD vs Deployment
+
+- Rollout and Deployment are kinda similar and both have replicas, selector, template, strategy fields, they manage pod creation. But rollout has additional fields for strategy that allow to perform more controllable rollouts with specific configurations, like rolling an update for a group of users, not for all.
+
+Additional fields for progressive delivery
+
+- canary: allows gradual traffic shifting to a new version using steps (e.g., setWeight, pause)
+- blueGreen: supports switching between old and new versions using separate services
+- steps: defines staged rollout progression
+- analysis: integrates automated checks (metrics, tests) during rollout
+- pause: enables manual or timed pauses between steps
+- trafficRouting: controls how traffic is split between versions (with ingress/service mesh)
+
+
+## Canary Deployment
+
+### Strategy configuration explained
+
+The rollout uses a canary strategy to gradually shift traffic from the old version to the new one. It is configured in steps (20%, 40%, 60%, 80%, 100%) with pauses to allow validation and manual control. This approach reduces risk by exposing the new version to a small part of users before full deployment.
+
+### Step-by-step rollout progression (screenshots from dashboard)
+
+
+
+
+
+### Promotion and abort demonstration
+
+Promotion (screenshots can be seen in the prev step)
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl argo rollouts get rollout myapp-app-python -n argo-rollouts
+Name: myapp-app-python
+Namespace: argo-rollouts
+Status: ॥ Paused
+Message: CanaryPauseStep
+Strategy: Canary
+ Step: 1/9
+ SetWeight: 20
+ ActualWeight: 25
+Images: fountainer/my-app:16-04 (canary, stable)
+Replicas:
+ Desired: 3
+ Current: 4
+ Updated: 1
+ Ready: 4
+ Available: 4
+
+NAME KIND STATUS AGE INFO
+⟳ myapp-app-python Rollout ॥ Paused 17m
+├──# revision:2
+│ └──⧉ myapp-app-python-76b59b6c66 ReplicaSet ✔ Healthy 69s canary
+│ └──□ myapp-app-python-76b59b6c66-pgtgq Pod ✔ Running 68s ready:1/1
+└──# revision:1
+ └──⧉ myapp-app-python-5bc87cfdf6 ReplicaSet ✔ Healthy 17m stable
+ ├──□ myapp-app-python-5bc87cfdf6-2tzkc Pod ✔ Running 17m ready:1/1
+ ├──□ myapp-app-python-5bc87cfdf6-bnpd6 Pod ✔ Running 17m ready:1/1
+ └──□ myapp-app-python-5bc87cfdf6-qfg9s Pod ✔ Running 17m ready:1/1
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl argo rollouts promote myapp-app-python -n argo-rollouts
+rollout 'myapp-app-python' promoted
+```
+
+Abort
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get rollouts -n argo-rollouts
+NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
+myapp-app-python 3 4 1 4 31m
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl argo rollouts abort myapp-app-python -n argo-rollouts
+rollout 'myapp-app-python' aborted
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl argo rollouts get rollout myapp-app-python -n argo-rollouts
+Name: myapp-app-python
+Namespace: argo-rollouts
+Status: ✖ Degraded
+Message: RolloutAborted: Rollout aborted update to revision 3
+Strategy: Canary
+ Step: 0/9
+ SetWeight: 0
+ ActualWeight: 0
+Images: fountainer/my-app:16-04 (stable)
+Replicas:
+ Desired: 3
+ Current: 3
+ Updated: 0
+ Ready: 3
+ Available: 3
+
+NAME KIND STATUS AGE INFO
+⟳ myapp-app-python Rollout ✖ Degraded 32m
+├──# revision:3
+│ └──⧉ myapp-app-python-5bc87cfdf6 ReplicaSet • ScaledDown 32m canary
+└──# revision:2
+ └──⧉ myapp-app-python-76b59b6c66 ReplicaSet ✔ Healthy 16m stable
+ ├──□ myapp-app-python-76b59b6c66-pgtgq Pod ✔ Running 16m ready:1/1
+ ├──□ myapp-app-python-76b59b6c66-7cwr4 Pod ✔ Running 10m ready:1/1
+ └──□ myapp-app-python-76b59b6c66-skfdd Pod ✔ Running 10m ready:1/1
+```
+
+
+## Blue-Green Deployment
+
+### Strategy configuration explained
+
+The blue-green strategy uses two environments: active and preview. The preview service runs the new version while the active service continues serving production traffic. After testing, the active service is switched to the new version instantly when promoted. This allows safe testing before release and quick rollback if needed.
+
+### Preview vs active service
+
+The active service is used by users in production and always points to the stable version. The preview service is used to test the new version before it is promoted. This separation ensures the new version can be verified without affecting real users.
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl port-forward svc/myapp-app-python-preview 8081:80 -n argo-rollouts
+Forwarding from 127.0.0.1:8081 -> 12345
+Forwarding from [::1]:8081 -> 12345
+Handling connection for 8081
+Handling connection for 8081
+```
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl port-forward svc/myapp-app-python-service 8080:80 -n argo-rollouts
+Forwarding from 127.0.0.1:8080 -> 12345
+Forwarding from [::1]:8080 -> 12345
+Handling connection for 8080
+Handling connection for 8080
+```
+
+### Promotion process
+
+Promotion
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % helm upgrade --install myapp . -n argo-rollouts
+Release "myapp" has been upgraded. Happy Helming!
+NAME: myapp
+LAST DEPLOYED: Thu Apr 30 23:12:57 2026
+NAMESPACE: argo-rollouts
+STATUS: deployed
+REVISION: 9
+DESCRIPTION: Upgrade complete
+TEST SUITE: None
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get pods -n argo-rollouts
+kubectl get svc -n argo-rollouts
+NAME READY STATUS RESTARTS AGE
+argo-rollouts-5f64f8d68-zxx5z 1/1 Running 0 6h24m
+argo-rollouts-dashboard-755bbc64c-pnkl6 1/1 Running 0 6h12m
+myapp-app-python-76b59b6c66-7cwr4 1/1 Running 0 37m
+myapp-app-python-76b59b6c66-pgtgq 1/1 Running 0 43m
+myapp-app-python-76b59b6c66-skfdd 1/1 Running 0 37m
+myapp-app-python-f7cddd7c7-5nvtx 1/1 Running 0 12m
+myapp-app-python-f7cddd7c7-xng4z 1/1 Running 0 12m
+myapp-app-python-f7cddd7c7-zjfpv 1/1 Running 0 12m
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+argo-rollouts-dashboard ClusterIP 10.106.240.192 3100/TCP 6h12m
+argo-rollouts-metrics ClusterIP 10.109.176.51 8090/TCP 6h24m
+myapp-app-python-preview ClusterIP 10.97.144.248 80/TCP 16m
+myapp-app-python-service NodePort 10.101.217.107 80:30009/TCP 59m
+```
+
+
+
+
+
+Rollback
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl argo rollouts undo myapp-app-python -n argo-rollouts
+rollout 'myapp-app-python' undo
+```
+
+
+
+## Strategy Comparison
+
+### When to use canary vs blue-green
+
+canary is used when you want to slowly roll out changes to users and reduce risk step by step. blue-green is used when you want an instant switch between versions after testing
+
+### Pros and cons of each
+
+- canary is safer for production because it exposes changes gradually, but it takes longer and is more complex to monitor
+
+- blue-green is faster and simpler at switch time, but requires double resources and has less gradual control.
+
+### Your recommendation for different scenarios
+
+use canary for production systems where stability is critical. use blue-green for fast releases or when you want quick testing and instant rollback.
+
+## CLI Commands Reference
+
+### Commands you used
+
+```kubectl argo rollouts get rollout -w``` is used to watch rollout progress. ```kubectl argo rollouts promote``` is used to move to the next step in canary or switch in blue-green. ```kubectl argo rollouts undo``` is used to rollback to the previous version.
+
+### Monitoring and troubleshooting
+
+```kubectl get pods```, ```kubectl get svc```, and ```kubectl describe rollout``` are used to check cluster state and debug issues. dashboard is used to visually monitor rollout progress and traffic changes.
\ No newline at end of file
diff --git a/app_python/k8s/SECRETS.md b/app_python/k8s/SECRETS.md
new file mode 100644
index 0000000000..1c62eae2de
--- /dev/null
+++ b/app_python/k8s/SECRETS.md
@@ -0,0 +1,197 @@
+# Documentation
+
+## Kubernetes Secrets
+
+### Output of creating and viewing your secret
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl create secret generic app-credentials --from-literal=username=fountainer --from-literal=password=‘mypass293i20@@nekf’
+secret/app-credentials created
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get secret app-credentials -o yaml
+apiVersion: v1
+data:
+ password: 4oCYbXlwYXNzMjkzaTIwQEBuZWtm4oCZ
+ username: Zm91bnRhaW5lcg==
+kind: Secret
+metadata:
+ creationTimestamp: "2026-04-07T14:46:16Z"
+ name: app-credentials
+ namespace: default
+ resourceVersion: "24859"
+ uid: 6997ca85-68fa-4278-9d51-a6531df977e9
+type: Opaque
+```
+### Decoded secret values demonstration
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % echo "4oCYbXlwYXNzMjkzaTIwQEBuZWtm4oCZ" | base64 -d
+‘mypass293i20@@nekf’%
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % echo "Zm91bnRhaW5lcg==" | base64 -d
+fountainer%
+```
+### Explanation of base64 encoding vs encryption
+
+- Encoding is when we use some publicly accesible algorithm to encode our data. The goal is keeping integrity and usability of the data, it is not really about security.
+
+- In turn, Encryption is about securuty. It envolves encrypting with an algorithm that can be only resolved by a user who has an encryption key.
+
+## Helm Secret Integration
+
+### Chart structure showing secrets.yaml
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % tree app_python/k8s/app_python
+app_python/k8s/app_python
+├── Chart.yaml
+├── charts
+├── templates
+│ ├── _helpers.tpl
+│ ├── deployment.yaml
+│ ├── hooks
+│ │ ├── post-install-job.yaml
+│ │ └── pre-install-job.yaml
+│ ├── secrets.yaml
+│ └── service.yaml
+├── values-dev.yaml
+├── values-prod.yaml
+└── values.yaml
+```
+
+### How secrets are consumed in deployment
+
+- I have $secretName variable that is dynamically set to the name from the values.yaml and values I provide in the helm install command OR defaults to the value from helper.
+
+### Verification output (env vars in pod, excluding actual values)
+
+- in pod I have correct env vars:
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl exec -it mysecretrelease-app-python-7975557578-6zc9m -- sh
+$ echo $PASSWORD
+mypass293i20@@nekf
+$ echo $USERNAME
+fountainer
+```
+
+- and outside the secrets are hidden
+
+from ```bash kubectl describe pod mysecretrelease-app-python-7975557578-6zc9m```
+
+```bash
+Environment:
+ PASSWORD: Optional: false
+ USERNAME: Optional: false
+```
+
+
+## Resource Management
+
+### Resource limits configuration
+
+```bash
+resources:
+ requests:
+ cpu: {{ .Values.resources.requests.cpu }}
+ memory: {{ .Values.resources.requests.memory }}
+ limits:
+ cpu: {{ .Values.resources.limits.cpu }}
+ memory: {{ .Values.resources.limits.memory }}
+```
+- in values.yaml I have
+
+```bash
+resources:
+ requests:
+ cpu: "100m"
+ memory: "128Mi"
+ limits:
+ cpu: "500m"
+ memory: "256Mi"
+```
+
+### Explanation of requests vs limits
+
+- requests is a setting that shows kubernates how much resources are needed for a container to run
+- limits show how much resources a container is allowed to use (max)
+
+### How to choose appropriate values
+
+- you should analyze what processes does your container run and how many cpu/memory it may need
+- values can be adjusted by observing the running container
+- if you have multiple containers/pods you should constraint them in such a way that they all can work without throttling
+- if the memory limit is too low the container can be killed right away
+
+## Vault Integration
+
+### Vault installation verification (kubectl get pods)
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl get pod
+NAME READY STATUS RESTARTS AGE
+mysecretrelease-app-python-7975557578-6zc9m 1/1 Running 0 63m
+mysecretrelease-app-python-7975557578-7l4tv 1/1 Running 0 63m
+mysecretrelease-app-python-7975557578-bqnpd 1/1 Running 0 63m
+mysecretrelease-app-python-7975557578-cjjcb 1/1 Running 0 63m
+mysecretrelease-app-python-7975557578-st2jd 1/1 Running 0 63m
+vault-0 1/1 Running 0 8m23s
+vault-agent-injector-848dd747d7-qvgl2 1/1 Running 0 8m23s
+```
+
+### Policy and role configuration (sanitized)
+
+- policy
+
+```bash
+/ $ vault policy write myapp-policy /tmp/myapp-policy.hcl
+Success! Uploaded policy: myapp-policy
+/ $ vault policy read myapp-policy
+path "secret/data/myapp/config" {
+ capabilities = ["read"]
+}
+```
+
+- role config
+
+```bash
+vault write auth/kubernetes/role/myapp-role \
+ bound_service_account_names=default \
+ bound_service_account_namespaces=default \
+ policies=myapp-policy \
+ ttl=48h
+```
+
+
+### Proof of secret injection (show file exists, path structure)
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl exec -it mysecretrelease-app-python-558b98bb9d-8299m -- /bin/sh
+Defaulted container "app-python" out of: app-python, vault-agent, vault-agent-init (init)
+$ ls -l /vault/secrets
+total 4
+-rw-r--r-- 1 100 newuser 180 Apr 7 23:55 config
+$ cat /vault/secrets/config
+data: map[password:mypass293i20@@nekf username:fountainer]
+metadata: map[created_time:2026-04-07T23:32:33.85543147Z custom_metadata: deletion_time: destroyed:false version:1]
+$
+```
+
+### Explanation of the sidecar injection pattern
+
+- now every pod contains not only my app container but also vault sidecar container
+- vault is able to authenticate in kubernates and inject secrets into the pod
+
+## Security Analysis
+
+### Comparison: K8s Secrets vs Vault
+
+- kubernates secrets are just encoded into base 64 and everyone who gets access to the cluster can decode them and get sensitive data, on the other hand, vault provides data encryption that is much more safer since you need an encryption key to encrypt it
+
+### When to use each approach
+
+- encoding is good for keeping data usability and integrity, so different machines can use it (like for seeing special symbols on a web page), it is like... more secure than nothing, but not reeally secure
+
+- vault encryption is needed for keeping sensitive data secure, like for storing passwords for the services on the virtual machines, etc
+
+### Production recommendations
+
+- in production you should always try to use strong encryption algorithms to keep your data secure
diff --git a/app_python/k8s/STATEFULSET.md b/app_python/k8s/STATEFULSET.md
new file mode 100644
index 0000000000..b5a5b60b72
--- /dev/null
+++ b/app_python/k8s/STATEFULSET.md
@@ -0,0 +1,168 @@
+# Documentation
+
+## StatefulSet Overview
+
+### Why StatefulSet
+
+It is used when pods need stable identity and storage, like each pod keeping its own data and name even after restart.
+
+### Differences from Deployment
+
+Key differences:
+- deployment pods are interchangeable and can change names/storage after restarts, while statefulset pods have fixed names (pod-0, pod-1) and their own persistent storage.
+
+When to use Deployment vs StatefulSet:
+- deployment is used for stateless apps (like web servers), and statefulset for apps that need stable data and identity (like databases).
+
+Examples of stateful workloads:
+- databases like mysql/postgresql, message queues, systems like elasticsearch
+
+### Headless Services
+
+What is a headless service (clusterIP: None)?
+- a service without a cluster ip that lets you directly access individual pods instead of load balancing
+
+How DNS works with StatefulSets?
+- each pod gets its own dns name like pod-0.service-name.namespace.svc.cluster.local, and they can be addressed individually
+
+## Resource Verification
+
+### Output of kubectl get pod,sts,svc,pvc
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get statefulset
+NAME READY AGE
+myapp-app-python 3/3 38s
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get pods
+NAME READY STATUS RESTARTS AGE
+my-app-app-python-5f57899757-4phmz 1/1 Running 1 (7d4h ago) 7d6h
+my-app-app-python-5f57899757-6sj7k 1/1 Running 1 (7d4h ago) 7d5h
+my-app-app-python-5f57899757-75mlj 1/1 Running 1 (7d4h ago) 7d5h
+myapp-app-python-0 1/1 Running 0 42s
+myapp-app-python-1 1/1 Running 0 25s
+myapp-app-python-2 1/1 Running 0 16s
+myapp-app-python-5bc87cfdf6-dhkt6 1/1 Running 0 42s
+myapp-app-python-5bc87cfdf6-mxpkd 1/1 Running 0 42s
+myapp-app-python-5bc87cfdf6-wpc68 1/1 Running 0 42s
+myapp-app-python-7dc6cbf89f-46pbw 1/1 Running 0 42s
+myapp-app-python-7dc6cbf89f-9krh8 1/1 Running 0 42s
+myapp-app-python-7dc6cbf89f-lp4rh 1/1 Running 0 42s
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get pvc
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
+data-volume-myapp-app-python-0 Bound pvc-2a043668-bbae-4c8d-86dc-ae89242a4b28 100Mi RWO standard 53s
+data-volume-myapp-app-python-1 Bound pvc-f9152007-9fff-4292-bc28-d1bc16b0214e 100Mi RWO standard 36s
+data-volume-myapp-app-python-2 Bound pvc-cec60c23-bdb5-44df-bf9f-9e2938726dc6 100Mi RWO standard 27s
+my-app-app-python-data Bound pvc-a5009930-2af6-4223-8fad-16257b59e9aa 100Mi RWO standard 7d6h
+myapp-app-python-data Bound pvc-22a66b4f-e1f6-486f-a528-76f27f090535 100Mi RWO standard 53s
+(devops) fountainer@Veronicas-MacBook-Air app_python %
+```
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl get pod,sts,svc,pvc
+NAME READY STATUS RESTARTS AGE
+pod/my-app-app-python-5f57899757-4phmz 1/1 Running 1 (7d4h ago) 7d6h
+pod/my-app-app-python-5f57899757-6sj7k 1/1 Running 1 (7d4h ago) 7d5h
+pod/my-app-app-python-5f57899757-75mlj 1/1 Running 1 (7d4h ago) 7d5h
+pod/myapp-app-python-0 1/1 Running 0 99s
+pod/myapp-app-python-1 1/1 Running 0 82s
+pod/myapp-app-python-2 1/1 Running 0 73s
+pod/myapp-app-python-5bc87cfdf6-dhkt6 1/1 Running 0 99s
+pod/myapp-app-python-5bc87cfdf6-mxpkd 1/1 Running 0 99s
+pod/myapp-app-python-5bc87cfdf6-wpc68 1/1 Running 0 99s
+pod/myapp-app-python-7dc6cbf89f-46pbw 1/1 Running 0 99s
+pod/myapp-app-python-7dc6cbf89f-9krh8 1/1 Running 0 99s
+pod/myapp-app-python-7dc6cbf89f-lp4rh 1/1 Running 0 99s
+
+NAME READY AGE
+statefulset.apps/myapp-app-python 3/3 99s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/kubernetes ClusterIP 10.96.0.1 443/TCP 7d8h
+service/myapp-app-python-preview ClusterIP 10.98.51.97 80/TCP 99s
+service/myapp-app-python-service NodePort 10.110.182.139 80:30009/TCP 99s
+
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
+persistentvolumeclaim/data-volume-myapp-app-python-0 Bound pvc-2a043668-bbae-4c8d-86dc-ae89242a4b28 100Mi RWO standard 99s
+persistentvolumeclaim/data-volume-myapp-app-python-1 Bound pvc-f9152007-9fff-4292-bc28-d1bc16b0214e 100Mi RWO standard 82s
+persistentvolumeclaim/data-volume-myapp-app-python-2 Bound pvc-cec60c23-bdb5-44df-bf9f-9e2938726dc6 100Mi RWO standard 73s
+persistentvolumeclaim/my-app-app-python-data Bound pvc-a5009930-2af6-4223-8fad-16257b59e9aa 100Mi RWO standard 7d6h
+persistentvolumeclaim/myapp-app-python-data Bound pvc-22a66b4f-e1f6-486f-a528-76f27f090535 100Mi RWO standard 99s
+(devops) fountainer@Veronicas-MacBook-Air app_python %
+```
+
+## Network Identity
+
+### DNS resolution outputs
+
+the naming pattern is ```.```
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % kubectl exec -it myapp-app-python-0 -- /bin/sh
+$ getent hosts myapp-app-python-0.myapp-app-python-headless
+10.244.0.177 myapp-app-python-0.myapp-app-python-headless.default.svc.cluster.local
+$ getent hosts myapp-app-python-1.myapp-app-python-headless
+10.244.0.179 myapp-app-python-1.myapp-app-python-headless.default.svc.cluster.local
+$ getent hosts myapp-app-python-2.myapp-app-python-headless
+10.244.0.180 myapp-app-python-2.myapp-app-python-headless.default.svc.cluster.local
+```
+
+## Per-Pod Storage Evidence
+
+### Different visit counts per pod
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl port-forward pod/myapp-app-python-0 8080:12345
+Forwarding from 127.0.0.1:8080 -> 12345
+Forwarding from [::1]:8080 -> 12345
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+```
+
+```bash
+fountainer@Veronicas-MacBook-Air DevOps-Core-Course % pyenv shell devops
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl port-forward pod/myapp-app-python-1 8081:12345
+Forwarding from 127.0.0.1:8081 -> 12345
+Forwarding from [::1]:8081 -> 12345
+Handling connection for 8081
+Handling connection for 8081
+Handling connection for 8081
+Handling connection for 8081
+Handling connection for 8081
+```
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air DevOps-Core-Course % kubectl port-forward pod/myapp-app-python-2 8082:12345
+Forwarding from 127.0.0.1:8082 -> 12345
+Forwarding from [::1]:8082 -> 12345
+Handling connection for 8082
+Handling connection for 8082
+Handling connection for 8082
+Handling connection for 8082
+```
+```bash
+(devops) fountainer@Veronicas-MacBook-Air app_python % curl localhost:8080/visits
+{
+ "visits": 13
+}
+(devops) fountainer@Veronicas-MacBook-Air app_python % curl localhost:8081/visits
+{
+ "visits": 7
+}
+(devops) fountainer@Veronicas-MacBook-Air app_python % curl localhost:8082/visits
+{
+ "visits": 2
+}
+```
+
+
+
+## Persistence Test
+
+### data survives pod deletion
+
+
\ No newline at end of file
diff --git a/app_python/k8s/app_python/.helmignore b/app_python/k8s/app_python/.helmignore
new file mode 100644
index 0000000000..0e8a0eb36f
--- /dev/null
+++ b/app_python/k8s/app_python/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/app_python/k8s/app_python/Chart.yaml b/app_python/k8s/app_python/Chart.yaml
new file mode 100644
index 0000000000..3e280aeaaf
--- /dev/null
+++ b/app_python/k8s/app_python/Chart.yaml
@@ -0,0 +1,16 @@
+apiVersion: v2
+name: app_python
+description: My Python application Helm chart
+
+type: application
+version: 0.1.0
+appVersion: "1.0"
+
+keywords:
+ - python
+ - web
+maintainers:
+ - name: Veronika Levasheva
+ email: veronikalev2005@gmail.com
+sources:
+ - https://github.com/ffountainer/DevOps-Core-Course
diff --git a/app_python/k8s/app_python/files/config.json b/app_python/k8s/app_python/files/config.json
new file mode 100644
index 0000000000..ba8808215e
--- /dev/null
+++ b/app_python/k8s/app_python/files/config.json
@@ -0,0 +1,11 @@
+{
+ "app_name": "my-app",
+ "environment": "dev",
+ "feature_flags": {
+ "debug": true,
+ "metrics": true
+ },
+ "settings": {
+ "log_level": "info"
+ }
+}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/service-headless.yaml b/app_python/k8s/app_python/service-headless.yaml
new file mode 100644
index 0000000000..04d26061e8
--- /dev/null
+++ b/app_python/k8s/app_python/service-headless.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "mychart.fullname" . }}-headless
+spec:
+ clusterIP: None
+ selector:
+ {{- include "mychart.selectorLabels" . | nindent 4 }}
+ ports:
+ - port: {{ .Values.service.port }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/_helpers.tpl b/app_python/k8s/app_python/templates/_helpers.tpl
new file mode 100644
index 0000000000..2697f80e58
--- /dev/null
+++ b/app_python/k8s/app_python/templates/_helpers.tpl
@@ -0,0 +1,50 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "mychart.name" -}}
+{{- default .Chart.Name .Values.nameOverride | replace "_" "-" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "mychart.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | replace "_" "-" | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := include "mychart.name" . }}
+{{- printf "%s-%s" .Release.Name $name | replace "_" "-" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+
+{{/*
+Chart name and version.
+*/}}
+{{- define "mychart.chart" -}}
+{{ .Chart.Name }}-{{ .Chart.Version }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "mychart.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "mychart.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Common labels.
+*/}}
+{{- define "mychart.labels" -}}
+helm.sh/chart: {{ include "mychart.chart" . }}
+{{ include "mychart.selectorLabels" . }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Service name helper.
+*/}}
+{{- define "mychart.serviceName" -}}
+{{ include "mychart.fullname" . }}-service
+{{- end }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/configmap-env.yaml b/app_python/k8s/app_python/templates/configmap-env.yaml
new file mode 100644
index 0000000000..f41c2df7d3
--- /dev/null
+++ b/app_python/k8s/app_python/templates/configmap-env.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "mychart.fullname" . }}-env
+ labels:
+ app: {{ include "mychart.name" . }}
+data:
+ APP_NAME: {{ .Values.appName | quote }}
+ APP_ENV: {{ .Values.environment | quote }}
+ LOG_LEVEL: {{ .Values.logLevel | quote }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/configmap.yaml b/app_python/k8s/app_python/templates/configmap.yaml
new file mode 100644
index 0000000000..a037d8bd60
--- /dev/null
+++ b/app_python/k8s/app_python/templates/configmap.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "mychart.fullname" . }}-config
+ labels:
+ app: {{ include "mychart.name" . }}
+data:
+ config.json: |-
+{{ .Files.Get "files/config.json" | indent 4 }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/deployment.yaml b/app_python/k8s/app_python/templates/deployment.yaml
new file mode 100644
index 0000000000..c3cada620d
--- /dev/null
+++ b/app_python/k8s/app_python/templates/deployment.yaml
@@ -0,0 +1,82 @@
+{{- $secretName := .Values.secret.name | default (include "mychart.fullname" .) }}
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "mychart.fullname" . }}
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxUnavailable: {{ .Values.strategy.maxUnavailable }}
+ maxSurge: {{ .Values.strategy.maxSurge }}
+ selector:
+ matchLabels:
+ {{- include "mychart.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ labels:
+ {{- include "mychart.selectorLabels" . | nindent 8 }}
+ annotations:
+ vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/role: "myapp-role"
+ vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"
+ spec:
+ containers:
+ - name: {{ include "mychart.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ env:
+ - name: PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: password
+ - name: USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: username
+ - name: DATA_DIR
+ value: /app/data
+ volumeMounts:
+ - name: config-volume
+ mountPath: /config
+ - name: data-volume
+ mountPath: /app/data
+ envFrom:
+ - configMapRef:
+ name: {{ include "mychart.fullname" . }}-env
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.container.port }}
+ resources:
+ requests:
+ cpu: {{ .Values.resources.requests.cpu }}
+ memory: {{ .Values.resources.requests.memory }}
+ limits:
+ cpu: {{ .Values.resources.limits.cpu }}
+ memory: {{ .Values.resources.limits.memory }}
+ livenessProbe:
+ httpGet:
+ path: {{ .Values.livenessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
+ timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
+ failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
+ readinessProbe:
+ httpGet:
+ path: {{ .Values.readinessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
+ volumes:
+ - name: config-volume
+ configMap:
+ name: {{ include "mychart.fullname" . }}-config
+ - name: data-volume
+ persistentVolumeClaim:
+ claimName: {{ include "mychart.fullname" . }}-data
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/headless-service.yaml b/app_python/k8s/app_python/templates/headless-service.yaml
new file mode 100644
index 0000000000..04d26061e8
--- /dev/null
+++ b/app_python/k8s/app_python/templates/headless-service.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "mychart.fullname" . }}-headless
+spec:
+ clusterIP: None
+ selector:
+ {{- include "mychart.selectorLabels" . | nindent 4 }}
+ ports:
+ - port: {{ .Values.service.port }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/hooks/post-install-job.yaml b/app_python/k8s/app_python/templates/hooks/post-install-job.yaml
new file mode 100644
index 0000000000..578cdf58a7
--- /dev/null
+++ b/app_python/k8s/app_python/templates/hooks/post-install-job.yaml
@@ -0,0 +1,26 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: "{{ include "mychart.fullname" . }}-post-install"
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+ annotations:
+ "helm.sh/hook": post-install
+ "helm.sh/hook-weight": "5"
+ "helm.sh/hook-delete-policy": hook-succeeded
+spec:
+ template:
+ metadata:
+ name: "{{ include "mychart.fullname" . }}-post-install"
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: post-install-job
+ image: busybox
+ command:
+ - sh
+ - -c
+ - |
+ echo "Post-install validation running"
+ sleep 10
+ echo "Validation passed"
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/hooks/pre-install-job.yaml b/app_python/k8s/app_python/templates/hooks/pre-install-job.yaml
new file mode 100644
index 0000000000..90018bbb06
--- /dev/null
+++ b/app_python/k8s/app_python/templates/hooks/pre-install-job.yaml
@@ -0,0 +1,26 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: "{{ include "mychart.fullname" . }}-pre-install"
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+ annotations:
+ "helm.sh/hook": pre-install
+ "helm.sh/hook-weight": "-5"
+ "helm.sh/hook-delete-policy": hook-succeeded
+spec:
+ template:
+ metadata:
+ name: "{{ include "mychart.fullname" . }}-pre-install"
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: pre-install-job
+ image: busybox
+ command:
+ - sh
+ - -c
+ - |
+ echo "Pre-install task running"
+ sleep 10
+ echo "Pre-install completed"
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/init.yaml b/app_python/k8s/app_python/templates/init.yaml
new file mode 100644
index 0000000000..e7b959f9de
--- /dev/null
+++ b/app_python/k8s/app_python/templates/init.yaml
@@ -0,0 +1,31 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: init-download-pod
+ namespace: default
+spec:
+ initContainers:
+ - name: init-download
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - wget -O /work-dir/index.html https://example.com
+ volumeMounts:
+ - name: workdir
+ mountPath: /work-dir
+
+ containers:
+ - name: main-app
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - sleep 3600
+ volumeMounts:
+ - name: workdir
+ mountPath: /data
+
+ volumes:
+ - name: workdir
+ emptyDir: {}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/nginx.yaml b/app_python/k8s/app_python/templates/nginx.yaml
new file mode 100644
index 0000000000..cc07ba1e93
--- /dev/null
+++ b/app_python/k8s/app_python/templates/nginx.yaml
@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: myservice
+ namespace: default
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: myservice
+ template:
+ metadata:
+ labels:
+ app: myservice
+ spec:
+ containers:
+ - name: nginx
+ image: nginx
+ ports:
+ - containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: myservice
+ namespace: default
+spec:
+ selector:
+ app: myservice
+ ports:
+ - port: 80
+ targetPort: 80
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/preview-service.yaml b/app_python/k8s/app_python/templates/preview-service.yaml
new file mode 100644
index 0000000000..e59503c982
--- /dev/null
+++ b/app_python/k8s/app_python/templates/preview-service.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "mychart.fullname" . }}-preview
+spec:
+ selector:
+ {{- include "mychart.selectorLabels" . | nindent 4 }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: {{ .Values.service.targetPort }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/pvc.yaml b/app_python/k8s/app_python/templates/pvc.yaml
new file mode 100644
index 0000000000..a930a3cb18
--- /dev/null
+++ b/app_python/k8s/app_python/templates/pvc.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.persistence.enabled }}
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: {{ include "mychart.fullname" . }}-data
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: {{ .Values.persistence.size }}
+ {{- if .Values.persistence.storageClass }}
+ storageClassName: {{ .Values.persistence.storageClass }}
+ {{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/rollout.yaml b/app_python/k8s/app_python/templates/rollout.yaml
new file mode 100644
index 0000000000..6d4e687d75
--- /dev/null
+++ b/app_python/k8s/app_python/templates/rollout.yaml
@@ -0,0 +1,86 @@
+{{- $secretName := .Values.secret.name | default (include "mychart.fullname" .) }}
+
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+ name: {{ include "mychart.fullname" . }}
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+
+ selector:
+ matchLabels:
+ {{- include "mychart.selectorLabels" . | nindent 6 }}
+
+ template:
+ metadata:
+ labels:
+ {{- include "mychart.selectorLabels" . | nindent 8 }}
+ annotations:
+ vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/role: "myapp-role"
+ vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"
+ spec:
+ containers:
+ - name: {{ include "mychart.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ env:
+ - name: PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: password
+ - name: USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: username
+ - name: DATA_DIR
+ value: /app/data
+ volumeMounts:
+ - name: config-volume
+ mountPath: /config
+ - name: data-volume
+ mountPath: /app/data
+ envFrom:
+ - configMapRef:
+ name: {{ include "mychart.fullname" . }}-env
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.container.port }}
+ resources:
+ requests:
+ cpu: {{ .Values.resources.requests.cpu }}
+ memory: {{ .Values.resources.requests.memory }}
+ limits:
+ cpu: {{ .Values.resources.limits.cpu }}
+ memory: {{ .Values.resources.limits.memory }}
+ livenessProbe:
+ httpGet:
+ path: {{ .Values.livenessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
+ timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
+ failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
+ readinessProbe:
+ httpGet:
+ path: {{ .Values.readinessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
+
+ volumes:
+ - name: config-volume
+ configMap:
+ name: {{ include "mychart.fullname" . }}-config
+ - name: data-volume
+ persistentVolumeClaim:
+ claimName: {{ include "mychart.fullname" . }}-data
+
+ strategy:
+ blueGreen:
+ activeService: {{ include "mychart.fullname" . }}-service
+ previewService: {{ include "mychart.fullname" . }}-preview
+ autoPromotionEnabled: false
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/secrets.yaml b/app_python/k8s/app_python/templates/secrets.yaml
new file mode 100644
index 0000000000..2460f91d72
--- /dev/null
+++ b/app_python/k8s/app_python/templates/secrets.yaml
@@ -0,0 +1,12 @@
+{{- $secretName := .Values.secret.name | default (include "mychart.fullname" .) }}
+
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ $secretName }}
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+type: Opaque
+stringData:
+ username: {{ .Values.secret.data.username | quote }}
+ password: {{ .Values.secret.data.password | quote }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/service.yaml b/app_python/k8s/app_python/templates/service.yaml
new file mode 100644
index 0000000000..558f0675e3
--- /dev/null
+++ b/app_python/k8s/app_python/templates/service.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "mychart.fullname" . }}-service
+spec:
+ type: {{ .Values.service.type }}
+ selector:
+ {{- include "mychart.selectorLabels" . | nindent 6 }}
+ ports:
+ - protocol: {{ .Values.service.protocol }}
+ port: {{ .Values.service.port }}
+ targetPort: {{ .Values.service.targetPort }}
+ nodePort: {{ .Values.service.nodePort }}
\ No newline at end of file
diff --git a/app_python/k8s/app_python/templates/statefulset.yaml b/app_python/k8s/app_python/templates/statefulset.yaml
new file mode 100644
index 0000000000..333b200ca0
--- /dev/null
+++ b/app_python/k8s/app_python/templates/statefulset.yaml
@@ -0,0 +1,84 @@
+{{- $secretName := .Values.secret.name | default (include "mychart.fullname" .) }}
+
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: {{ include "mychart.fullname" . }}
+ labels:
+ {{- include "mychart.labels" . | nindent 4 }}
+spec:
+ serviceName: {{ include "mychart.fullname" . }}-headless
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ {{- include "mychart.selectorLabels" . | nindent 6 }}
+
+ template:
+ metadata:
+ labels:
+ {{- include "mychart.selectorLabels" . | nindent 8 }}
+ annotations:
+ vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/role: "myapp-role"
+ vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"
+ spec:
+ containers:
+ - name: {{ include "mychart.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ env:
+ - name: PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: password
+ - name: USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: {{ $secretName }}
+ key: username
+ - name: DATA_DIR
+ value: /app/data
+ volumeMounts:
+ - name: config-volume
+ mountPath: /config
+ - name: data-volume
+ mountPath: /app/data
+ envFrom:
+ - configMapRef:
+ name: {{ include "mychart.fullname" . }}-env
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.container.port }}
+ resources:
+ requests:
+ cpu: {{ .Values.resources.requests.cpu }}
+ memory: {{ .Values.resources.requests.memory }}
+ limits:
+ cpu: {{ .Values.resources.limits.cpu }}
+ memory: {{ .Values.resources.limits.memory }}
+ livenessProbe:
+ httpGet:
+ path: {{ .Values.livenessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
+ timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
+ failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
+ readinessProbe:
+ httpGet:
+ path: {{ .Values.readinessProbe.path }}
+ port: {{ .Values.container.port }}
+ initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
+ volumes:
+ - name: config-volume
+ configMap:
+ name: {{ include "mychart.fullname" . }}-config
+ volumeClaimTemplates:
+ - metadata:
+ name: data-volume
+ spec:
+ accessModes: [ "ReadWriteOnce" ]
+ resources:
+ requests:
+ storage: {{ .Values.persistence.size }}
diff --git a/app_python/k8s/app_python/templates/waiting.yaml b/app_python/k8s/app_python/templates/waiting.yaml
new file mode 100644
index 0000000000..12a9e5575b
--- /dev/null
+++ b/app_python/k8s/app_python/templates/waiting.yaml
@@ -0,0 +1,20 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: wait-service-pod
+ namespace: default
+spec:
+ initContainers:
+ - name: wait-for-service
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - until wget -qO- http://myservice.default.svc.cluster.local; do echo waiting for service; sleep 2; done
+ containers:
+ - name: main-app
+ image: busybox:1.36
+ command:
+ - sh
+ - -c
+ - echo Service found! && sleep 3600
\ No newline at end of file
diff --git a/app_python/k8s/app_python/values-dev.yaml b/app_python/k8s/app_python/values-dev.yaml
new file mode 100644
index 0000000000..81c63488ec
--- /dev/null
+++ b/app_python/k8s/app_python/values-dev.yaml
@@ -0,0 +1,40 @@
+replicaCount: 1
+
+image:
+ repository: fountainer/my-app
+ tag: "16-04"
+ pullPolicy: Always
+
+container:
+ port: 12345
+
+resources:
+ requests:
+ cpu: "50m"
+ memory: "64Mi"
+ limits:
+ cpu: "100m"
+ memory: "128Mi"
+
+strategy:
+ maxUnavailable: 1
+ maxSurge: 1
+
+livenessProbe:
+ path: /health
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+
+readinessProbe:
+ path: /ready
+ initialDelaySeconds: 2
+ periodSeconds: 5
+
+service:
+ type: NodePort
+ port: 80
+ targetPort: 12345
+ protocol: TCP
+ nodePort: 30007
\ No newline at end of file
diff --git a/app_python/k8s/app_python/values-prod.yaml b/app_python/k8s/app_python/values-prod.yaml
new file mode 100644
index 0000000000..0c3548c5d2
--- /dev/null
+++ b/app_python/k8s/app_python/values-prod.yaml
@@ -0,0 +1,40 @@
+replicaCount: 3
+
+image:
+ repository: fountainer/my-app
+ tag: "16-04"
+ pullPolicy: Always
+
+container:
+ port: 12345
+
+resources:
+ requests:
+ cpu: "200m"
+ memory: "256Mi"
+ limits:
+ cpu: "500m"
+ memory: "512Mi"
+
+strategy:
+ maxUnavailable: 1
+ maxSurge: 1
+
+livenessProbe:
+ path: /health
+ initialDelaySeconds: 30
+ periodSeconds: 5
+ timeoutSeconds: 5
+ failureThreshold: 5
+
+readinessProbe:
+ path: /ready
+ initialDelaySeconds: 10
+ periodSeconds: 3
+
+service:
+ type: LoadBalancer
+ port: 80
+ targetPort: 12345
+ protocol: TCP
+ nodePort: 30008
\ No newline at end of file
diff --git a/app_python/k8s/app_python/values.yaml b/app_python/k8s/app_python/values.yaml
new file mode 100644
index 0000000000..b0ce838464
--- /dev/null
+++ b/app_python/k8s/app_python/values.yaml
@@ -0,0 +1,60 @@
+replicaCount: 3
+
+image:
+ repository: fountainer/my-app
+ tag: "2026.05.01"
+ pullPolicy: Always
+
+container:
+ port: 12345
+
+persistence:
+ size: 1Gi
+
+storageClass: ""
+
+resources:
+ requests:
+ cpu: "100m"
+ memory: "128Mi"
+ limits:
+ cpu: "500m"
+ memory: "256Mi"
+
+strategy:
+ maxUnavailable: 1
+ maxSurge: 1
+
+livenessProbe:
+ path: /health
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 5
+
+readinessProbe:
+ path: /ready
+ initialDelaySeconds: 5
+ periodSeconds: 5
+
+service:
+ type: NodePort
+ port: 80
+ targetPort: 12345
+ protocol: TCP
+ nodePort: 30009
+
+secret:
+ name: app-credentials
+ data:
+ username: "placeholder-user"
+ password: "placeholder-password"
+
+appName: my-app
+environment: dev
+logLevel: info
+
+persistence:
+ enabled: true
+ size: 100Mi
+ storageClass: ""
\ No newline at end of file
diff --git a/app_python/k8s/app_python/vault/myapp-policy.hcl b/app_python/k8s/app_python/vault/myapp-policy.hcl
new file mode 100644
index 0000000000..230c2125cb
--- /dev/null
+++ b/app_python/k8s/app_python/vault/myapp-policy.hcl
@@ -0,0 +1,3 @@
+path "secret/data/myapp/config" {
+ capabilities = ["read"]
+}
\ No newline at end of file
diff --git a/app_python/k8s/argocd/application-dev.yaml b/app_python/k8s/argocd/application-dev.yaml
new file mode 100644
index 0000000000..587c67c97d
--- /dev/null
+++ b/app_python/k8s/argocd/application-dev.yaml
@@ -0,0 +1,26 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: python-app-dev
+ namespace: argocd
+spec:
+ project: default
+
+ source:
+ repoURL: https://github.com/ffountainer/DevOps-Core-Course
+ targetRevision: lab13
+ path: app_python/k8s/app_python
+ helm:
+ valueFiles:
+ - values-dev.yaml
+
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: dev
+
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
\ No newline at end of file
diff --git a/app_python/k8s/argocd/application-prod.yaml b/app_python/k8s/argocd/application-prod.yaml
new file mode 100644
index 0000000000..9ada9f09eb
--- /dev/null
+++ b/app_python/k8s/argocd/application-prod.yaml
@@ -0,0 +1,23 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: python-app-prod
+ namespace: argocd
+spec:
+ project: default
+
+ source:
+ repoURL: https://github.com/ffountainer/DevOps-Core-Course
+ targetRevision: lab13
+ path: app_python/k8s/app_python
+ helm:
+ valueFiles:
+ - values-prod.yaml
+
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: prod
+
+ syncPolicy:
+ syncOptions:
+ - CreateNamespace=true
\ No newline at end of file
diff --git a/app_python/k8s/argocd/application.yaml b/app_python/k8s/argocd/application.yaml
new file mode 100644
index 0000000000..72e9fd692c
--- /dev/null
+++ b/app_python/k8s/argocd/application.yaml
@@ -0,0 +1,21 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: my-app
+ namespace: argocd
+spec:
+ project: default
+
+ source:
+ repoURL: https://github.com/ffountainer/DevOps-Core-Course
+ targetRevision: lab13
+ path: app_python/k8s/app_python
+ helm:
+ valueFiles:
+ - values.yaml
+
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: default
+
+ syncPolicy: {}
\ No newline at end of file
diff --git a/app_python/k8s/deployment.yml b/app_python/k8s/deployment.yml
new file mode 100644
index 0000000000..8be175e432
--- /dev/null
+++ b/app_python/k8s/deployment.yml
@@ -0,0 +1,47 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: my-app-deployment
+ labels:
+ app: my-app
+spec:
+ replicas: 5
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxUnavailable: 1
+ maxSurge: 1
+ selector:
+ matchLabels:
+ app: my-app
+ template:
+ metadata:
+ labels:
+ app: my-app
+ spec:
+ containers:
+ - name: app-python
+ image: fountainer/my-app:2026.03.26
+ ports:
+ - containerPort: 12345
+ resources:
+ requests:
+ cpu: "100m"
+ memory: "128Mi"
+ limits:
+ cpu: "500m"
+ memory: "256Mi"
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 12345
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 5
+ readinessProbe:
+ httpGet:
+ path: /ready
+ port: 12345
+ initialDelaySeconds: 5
+ periodSeconds: 5
\ No newline at end of file
diff --git a/app_python/monitoring/docker-compose.yml b/app_python/monitoring/docker-compose.yml
new file mode 100644
index 0000000000..84f8d57b49
--- /dev/null
+++ b/app_python/monitoring/docker-compose.yml
@@ -0,0 +1,138 @@
+services:
+ loki:
+ image: grafana/loki:3.0.0
+ container_name: loki
+ ports:
+ - "3100:3100"
+ command: -config.file=/etc/loki/config.yml
+ healthcheck:
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+ volumes:
+ - ./loki/config.yml:/etc/loki/config.yml
+ - loki-data:/loki
+ networks:
+ - logging
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 1G
+ reservations:
+ cpus: "0.50"
+ memory: 512M
+ promtail:
+ image: grafana/promtail:3.0.0
+ container_name: promtail
+ ports:
+ - "9080:9080"
+ command: -config.file=/etc/promtail/config.yml
+ volumes:
+ - ./promtail/config.yml:/etc/promtail/config.yml
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ depends_on:
+ - loki
+ networks:
+ - logging
+ deploy:
+ resources:
+ limits:
+ cpus: "0.25"
+ memory: 256M
+ reservations:
+ cpus: "0.10"
+ memory: 128M
+ grafana:
+ image: grafana/grafana:12.3.1
+ container_name: grafana
+ ports:
+ - "3000:3000"
+ healthcheck:
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+ environment:
+ - GF_AUTH_ANONYMOUS_ENABLED=false
+ - GF_SECURITY_ADMIN_PASSWORD=${GF_PASSWORD}
+ - GF_SECURITY_ADMIN_EMAIL=${GF_EMAIL}
+ volumes:
+ - grafana-data:/var/lib/grafana
+ depends_on:
+ - loki
+ networks:
+ - logging
+ deploy:
+ resources:
+ limits:
+ cpus: "0.5"
+ memory: 512M
+ reservations:
+ cpus: "0.25"
+ memory: 256M
+ app-python:
+ image: fountainer/my-app:latest
+ container_name: app-python
+ ports:
+ - "1999:12345"
+ healthcheck:
+ test: ["CMD-SHELL", "curl -f http://localhost:12345/health || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ networks:
+ - logging
+ labels:
+ logging: "promtail"
+ app: "my-app"
+ deploy:
+ resources:
+ limits:
+ cpus: "0.5"
+ memory: 256M
+ reservations:
+ cpus: "0.10"
+ memory: 128M
+ prometheus:
+ image: prom/prometheus:v3.9.0
+ container_name: prometheus
+ ports:
+ - "9090:9090"
+ healthcheck:
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.retention.time=15d
+ - --storage.tsdb.retention.size=10GB
+ volumes:
+ - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+ - prometheus-data:/prometheus
+ networks:
+ - logging
+ depends_on:
+ - app-python
+ - loki
+ - grafana
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 1G
+volumes:
+ loki-data:
+ grafana-data:
+ prometheus-data:
+networks:
+ logging:
+ driver: bridge
+
+
+
diff --git a/app_python/monitoring/docs/LAB07.md b/app_python/monitoring/docs/LAB07.md
new file mode 100644
index 0000000000..02946794ee
--- /dev/null
+++ b/app_python/monitoring/docs/LAB07.md
@@ -0,0 +1 @@
+!!!!!! real LAB07.md documentation lies in the app_python/docs/LAB07.md, since all prev labs documentation files were there, and I wanted to remain consistent. Please check this file, it has everything required by the task
\ No newline at end of file
diff --git a/app_python/monitoring/docs/LAB08.md b/app_python/monitoring/docs/LAB08.md
new file mode 100644
index 0000000000..8da919fdae
--- /dev/null
+++ b/app_python/monitoring/docs/LAB08.md
@@ -0,0 +1 @@
+!!!!!! real LAB08.md documentation lies in the app_python/docs/LAB08.md, since all prev labs documentation files were there, and I wanted to remain consistent. Please check this file, it has everything required by the task
\ No newline at end of file
diff --git a/app_python/monitoring/loki/config.yml b/app_python/monitoring/loki/config.yml
new file mode 100644
index 0000000000..9a27a4c15d
--- /dev/null
+++ b/app_python/monitoring/loki/config.yml
@@ -0,0 +1,31 @@
+auth_enabled: false
+server:
+ http_listen_port: 3100
+common:
+ path_prefix: /loki
+ storage:
+ filesystem:
+ chunks_directory: /loki/chunks
+ rules_directory: /loki/rules
+ replication_factor: 1
+ ring:
+ kvstore:
+ store: inmemory
+schema_config:
+ configs:
+ - from: 2026-03-01
+ store: tsdb
+ object_store: filesystem
+ schema: v13
+ index:
+ prefix: index_
+ period: 24h
+storage_config:
+ filesystem:
+ directory: /loki/chunks
+limits_config:
+ retention_period: 168h
+compactor:
+ working_directory: /loki/compactor
+ retention_enabled: true
+ delete_request_store: filesystem
\ No newline at end of file
diff --git a/app_python/monitoring/prometheus/dashboard.json b/app_python/monitoring/prometheus/dashboard.json
new file mode 100644
index 0000000000..d63d022ade
--- /dev/null
+++ b/app_python/monitoring/prometheus/dashboard.json
@@ -0,0 +1,630 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(http_requests_total[5m])) by (endpoint)",
+ "legendFormat": "",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "request rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "fixedColor": "red",
+ "mode": "fixed"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 0
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "__systemRef": "hideSeriesFrom",
+ "matcher": {
+ "id": "byNames",
+ "options": {
+ "mode": "exclude",
+ "names": [
+ "sum(rate(http_requests_total{status_code=~\"[45]..\"}[5m]))"
+ ],
+ "prefix": "All except:",
+ "readOnly": true
+ }
+ },
+ "properties": [
+ {
+ "id": "custom.hideFrom",
+ "value": {
+ "legend": false,
+ "tooltip": true,
+ "viz": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "sum(rate(http_requests_total{status_code=~\"[45]..\"}[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "error rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ }
+ },
+ "mappings": []
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "404"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "light-red",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 5,
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "pieType": "pie",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "sort": "desc",
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "sum by (status_code) (rate(http_requests_total[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "status code distribution",
+ "type": "piechart"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "scaleDistribution": {
+ "type": "linear"
+ }
+ },
+ "unit": "arcsec"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "calculate": false,
+ "cellGap": 1,
+ "color": {
+ "exponent": 0.5,
+ "fill": "dark-orange",
+ "mode": "scheme",
+ "reverse": false,
+ "scale": "exponential",
+ "scheme": "Oranges",
+ "steps": 64
+ },
+ "exemplars": {
+ "color": "rgba(255,0,255,0.7)"
+ },
+ "filterValues": {
+ "le": 1e-9
+ },
+ "legend": {
+ "show": true
+ },
+ "rowsFrame": {
+ "layout": "auto"
+ },
+ "tooltip": {
+ "mode": "single",
+ "showColorScale": false,
+ "yHistogram": false
+ },
+ "yAxis": {
+ "axisPlacement": "left",
+ "reverse": false
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "rate(http_request_duration_seconds_bucket[5m])",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "latency heatmap",
+ "type": "heatmap"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "showValues": false,
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 16
+ },
+ "id": 7,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "hideZeros": false,
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "p95 Latency",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 16
+ },
+ "id": 4,
+ "options": {
+ "minVizHeight": 75,
+ "minVizWidth": 75,
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "sizing": "auto"
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "http_requests_in_progress",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "active requests",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "ffgik5dl3u2o0c"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": 0
+ },
+ {
+ "color": "red",
+ "value": 0
+ },
+ {
+ "color": "semi-dark-green",
+ "value": 1
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 24
+ },
+ "id": 6,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "pluginVersion": "12.3.1",
+ "targets": [
+ {
+ "editorMode": "code",
+ "expr": "up{job=\"app\"}",
+ "legendFormat": "__auto",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "service health",
+ "type": "stat"
+ }
+ ],
+ "preload": false,
+ "schemaVersion": 42,
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-5m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "browser",
+ "title": "devops lab8",
+ "uid": "adt5g7d",
+ "version": 11
+}
\ No newline at end of file
diff --git a/app_python/monitoring/prometheus/prometheus.yml b/app_python/monitoring/prometheus/prometheus.yml
new file mode 100644
index 0000000000..442e8ae576
--- /dev/null
+++ b/app_python/monitoring/prometheus/prometheus.yml
@@ -0,0 +1,23 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: 'prometheus'
+ static_configs:
+ - targets: ['localhost:9090']
+
+ - job_name: 'app'
+ static_configs:
+ - targets: ['app-python:12345']
+ metrics_path: /metrics
+
+ - job_name: 'loki'
+ static_configs:
+ - targets: ['loki:3100']
+ metrics_path: /metrics
+
+ - job_name: 'grafana'
+ static_configs:
+ - targets: ['grafana:3000']
+ metrics_path: /metrics
\ No newline at end of file
diff --git a/app_python/monitoring/promtail/config.yml b/app_python/monitoring/promtail/config.yml
new file mode 100644
index 0000000000..02c6e0501c
--- /dev/null
+++ b/app_python/monitoring/promtail/config.yml
@@ -0,0 +1,22 @@
+server:
+ http_listen_port: 9080
+positions:
+ filename: "/tmp/positions.yaml"
+ sync_period: 10s
+ ignore_invalid_yaml: false
+clients:
+ - url: http://loki:3100/loki/api/v1/push
+scrape_configs:
+ - job_name: docker
+ docker_sd_configs:
+ - host: unix:///var/run/docker.sock
+ refresh_interval: 5s
+ relabel_configs:
+ - source_labels: ['__meta_docker_container_name']
+ regex: '/(.*)'
+ target_label: 'container'
+ - target_label: job
+ replacement: docker
+ - source_labels: ['__meta_docker_container_label_logging']
+ regex: promtail
+ action: keep
\ No newline at end of file
diff --git a/app_python/pulumi/.gitignore b/app_python/pulumi/.gitignore
new file mode 100644
index 0000000000..a3807e5bdb
--- /dev/null
+++ b/app_python/pulumi/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+venv/
diff --git a/app_python/pulumi/Pulumi.dev.yaml b/app_python/pulumi/Pulumi.dev.yaml
new file mode 100644
index 0000000000..87ad76aa1d
--- /dev/null
+++ b/app_python/pulumi/Pulumi.dev.yaml
@@ -0,0 +1,9 @@
+encryptionsalt: v1:ibiWcVbfzPE=:v1:JGYI/AyYOtO6LyQX:wm2sULkxKAuOHYQjzutkotLh1kDMWQ==
+config:
+ pulumi-vm:zone: "ru-central1-a"
+ pulumi-vm:appPort: "1999"
+ pulumi-vm:sshUser: "ubuntu"
+ pulumi-vm:sshPublicKeyPath: "/Users/fountainer/.ssh/terraform-vm-key.pub"
+ yandex:cloudId: "b1g4lhssapi17vlk0t0m"
+ yandex:folderId: "b1gcu87lbfoq3i0quvi9"
+ yandex:serviceAccountKeyFile: "/Users/fountainer/.yc/pulumi-key.json"
\ No newline at end of file
diff --git a/app_python/pulumi/Pulumi.yaml b/app_python/pulumi/Pulumi.yaml
new file mode 100644
index 0000000000..f1628db528
--- /dev/null
+++ b/app_python/pulumi/Pulumi.yaml
@@ -0,0 +1,11 @@
+name: pulumi-vm
+description: A minimal Python Pulumi program
+runtime:
+ name: python
+ options:
+ toolchain: pip
+ virtualenv: venv
+config:
+ pulumi:tags:
+ value:
+ pulumi:template: python
diff --git a/app_python/pulumi/__main__.py b/app_python/pulumi/__main__.py
new file mode 100644
index 0000000000..b519111013
--- /dev/null
+++ b/app_python/pulumi/__main__.py
@@ -0,0 +1,64 @@
+"""A Python Pulumi program"""
+
+import pulumi
+import pulumi_yandex as yandex
+import os
+
+config = pulumi.Config()
+zone = config.require("zone")
+app_port = config.require_int("appPort")
+ssh_user = config.require("sshUser")
+ssh_key_path = config.require("sshPublicKeyPath")
+
+with open(os.path.expanduser(ssh_key_path)) as f:
+ public_key = f.read().strip()
+
+network = yandex.VpcNetwork("network", name="network")
+
+subnet = yandex.VpcSubnet(
+ "subnet",
+ name="subnet1",
+ zone=zone,
+ network_id=network.id,
+ v4_cidr_blocks=["192.168.10.0/24"]
+)
+
+vm_sg = yandex.VpcSecurityGroup(
+ "vm-sg",
+ description="VM security group",
+ network_id=network.id,
+ ingresses=[
+ yandex.VpcSecurityGroupIngressArgs(port=22, protocol="TCP", v4_cidr_blocks=["0.0.0.0/0"]),
+ yandex.VpcSecurityGroupIngressArgs(port=80, protocol="TCP", v4_cidr_blocks=["0.0.0.0/0"]),
+ yandex.VpcSecurityGroupIngressArgs(port=app_port, protocol="TCP", v4_cidr_blocks=["0.0.0.0/0"]),
+ ],
+ egresses=[
+ yandex.VpcSecurityGroupEgressArgs(protocol="ANY", from_port=0, to_port=0, v4_cidr_blocks=["0.0.0.0/0"])
+ ]
+)
+
+image = yandex.get_compute_image(family="ubuntu-2204-lts")
+
+vm = yandex.ComputeInstance(
+ "vm",
+ name="pulumi",
+ zone=zone,
+ resources={"cores": 2, "memory": 2},
+ boot_disk={
+ "initialize_params": {
+ "name": "boot-disk",
+ "size": 20,
+ "type": "network-hdd",
+ "image_id": image.id,
+ }
+ },
+ network_interfaces=[{
+ "subnet_id": subnet.id,
+ "nat": True,
+ "security_group_ids": [vm_sg.id],
+ }],
+ metadata={"ssh-keys": f"{ssh_user}:{public_key}"}
+)
+
+pulumi.export("internal_ip", vm.network_interfaces[0]["ip_address"])
+pulumi.export("external_ip", vm.network_interfaces[0]["nat_ip_address"])
diff --git a/app_python/pulumi/requirements.txt b/app_python/pulumi/requirements.txt
new file mode 100644
index 0000000000..938e7bfb54
--- /dev/null
+++ b/app_python/pulumi/requirements.txt
@@ -0,0 +1,4 @@
+pulumi>=3.223.0
+pulumi_yandex==0.13.0
+PyYAML
+setuptools
\ No newline at end of file
diff --git a/app_python/requirements.txt b/app_python/requirements.txt
new file mode 100644
index 0000000000..47296a42b8
--- /dev/null
+++ b/app_python/requirements.txt
@@ -0,0 +1,6 @@
+# Web Framework
+flask==3.1.2
+pytest==9.0.2
+python-json-logger==4.0.0
+python-dotenv==1.2.2
+prometheus-client==0.23.1
\ No newline at end of file
diff --git a/app_python/terraform/.gitignore b/app_python/terraform/.gitignore
new file mode 100644
index 0000000000..b7cf2bdd62
--- /dev/null
+++ b/app_python/terraform/.gitignore
@@ -0,0 +1,4 @@
+.terraform/
+terraform.tfstate
+terraform.tfstate.backup
+terraform.tfvars
\ No newline at end of file
diff --git a/app_python/terraform/.terraform.lock.hcl b/app_python/terraform/.terraform.lock.hcl
new file mode 100644
index 0000000000..280b0e4395
--- /dev/null
+++ b/app_python/terraform/.terraform.lock.hcl
@@ -0,0 +1,9 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/yandex-cloud/yandex" {
+ version = "0.191.0"
+ hashes = [
+ "h1:MGHtJSlDigrSSCHWdl28B+XXnH87C82Qu0978/lbJtM=",
+ ]
+}
diff --git a/app_python/terraform/README.md b/app_python/terraform/README.md
new file mode 100644
index 0000000000..22dafe1be3
--- /dev/null
+++ b/app_python/terraform/README.md
@@ -0,0 +1,33 @@
+## SETUP INSTRUCTIONS: TERRAFORM
+
+### Set environment variables
+```bash
+export YC_TOKEN=$(yc iam create-token --impersonate-service-account-id )
+export YC_CLOUD_ID=$(yc config get cloud-id)
+export YC_FOLDER_ID=$(yc config get folder-id)
+```
+
+### Initialize Terraform
+```bash
+terraform init
+```
+
+### Plan infrastructure
+```bash
+terraform plan
+```
+
+### Apply infrastructure
+```bash
+terraform apply
+```
+
+### Access VM via SSH
+```bash
+ssh -i /path/to/terraform-vm-key ubuntu@
+```
+
+### Destroy resources
+```bash
+terraform destroy
+```
diff --git a/app_python/terraform/main.tf b/app_python/terraform/main.tf
new file mode 100644
index 0000000000..6d122555f9
--- /dev/null
+++ b/app_python/terraform/main.tf
@@ -0,0 +1,95 @@
+terraform {
+ required_providers {
+ yandex = {
+ source = "yandex-cloud/yandex"
+ }
+ }
+ required_version = ">= 0.13"
+}
+
+provider "yandex" {
+ service_account_key_file = "/Users/fountainer/.yandexcloud/my-sa-key.json"
+ zone = var.zone
+ cloud_id = "b1g4lhssapi17vlk0t0m"
+ folder_id = "b1gcu87lbfoq3i0quvi9"
+}
+
+data "yandex_compute_image" "ubuntu_2204" {
+ family = "ubuntu-2204-lts"
+}
+
+resource "yandex_compute_disk" "boot-disk" {
+ name = "boot-disk-terraform"
+ type = "network-hdd"
+ zone = var.zone
+ size = "20"
+ image_id = data.yandex_compute_image.ubuntu_2204.id
+}
+
+resource "yandex_compute_instance" "vm" {
+ name = "terraform"
+
+ resources {
+ cores = 2
+ memory = 2
+ }
+
+ boot_disk {
+ disk_id = yandex_compute_disk.boot-disk.id
+ }
+
+ network_interface {
+ subnet_id = yandex_vpc_subnet.subnet.id
+ nat = true
+ security_group_ids = [yandex_vpc_security_group.vm_sg.id]
+
+ }
+
+ metadata = {
+ ssh-keys = "ubuntu:${file(var.ssh_key_path)}"
+ }
+}
+
+resource "yandex_vpc_network" "network" {
+ name = "network"
+}
+
+resource "yandex_vpc_subnet" "subnet" {
+ name = "subnet1"
+ zone = var.zone
+ network_id = yandex_vpc_network.network.id
+ v4_cidr_blocks = ["192.168.10.0/24"]
+}
+
+resource "yandex_vpc_security_group" "vm_sg" {
+ name = "vm-security-group"
+ network_id = yandex_vpc_network.network.id
+
+ ingress {
+ protocol = "TCP"
+ description = "SSH"
+ port = 22
+ v4_cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ ingress {
+ protocol = "TCP"
+ description = "HTTP"
+ port = 80
+ v4_cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ ingress {
+ protocol = "TCP"
+ description = "App port"
+ port = var.app_port
+ v4_cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ protocol = "ANY"
+ description = "Allow all outgoing traffic"
+ v4_cidr_blocks = ["0.0.0.0/0"]
+ }
+}
+
diff --git a/app_python/terraform/outputs.tf b/app_python/terraform/outputs.tf
new file mode 100644
index 0000000000..17df178a5f
--- /dev/null
+++ b/app_python/terraform/outputs.tf
@@ -0,0 +1,10 @@
+output "internal_ip_address_vm" {
+ description = "Internal IP address of the VM"
+ value = yandex_compute_instance.vm.network_interface.0.ip_address
+}
+
+output "external_ip_address_vm" {
+ description = "External (public) IP address of the VM"
+ value = yandex_compute_instance.vm.network_interface.0.nat_ip_address
+}
+
diff --git a/app_python/terraform/variables.tf b/app_python/terraform/variables.tf
new file mode 100644
index 0000000000..c30b81c0b3
--- /dev/null
+++ b/app_python/terraform/variables.tf
@@ -0,0 +1,15 @@
+variable "zone" {
+ description = "Yandex Cloud availability zone"
+}
+
+variable "ssh_key_path" {
+ description = "Path to the SSH key for VM access"
+}
+
+variable "ssh_source_ip" {
+ description = "Your IP address to allow SSH access"
+}
+
+variable "app_port" {
+ description = "Custom app port to open in the firewall"
+}
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_health_endpoint.py b/app_python/tests/test_health_endpoint.py
new file mode 100644
index 0000000000..74f99cefe8
--- /dev/null
+++ b/app_python/tests/test_health_endpoint.py
@@ -0,0 +1,40 @@
+from app import app
+import pytest
+
+
+def test_health_endpoint():
+ client = app.test_client()
+ response = client.get("/health")
+
+ assert (
+ response.status_code == 200
+ or response.status_code == 404
+ or response.status_code == 500
+ )
+
+ resp = response.get_json()
+
+ if response.status_code == 200:
+
+ status = resp.get("status")
+ assert status
+ assert isinstance(status, str)
+
+ timestamp = resp.get("timestamp")
+ assert timestamp
+ assert isinstance(timestamp, str)
+
+ uptime_seconds = resp.get("uptime_seconds")
+ assert uptime_seconds >= 0
+ assert isinstance(uptime_seconds, int)
+
+ elif response.status_code == 404 or response.status_code == 500:
+ assert isinstance(resp, dict)
+
+ error = resp.get("error")
+ assert error
+ assert isinstance(error, str)
+
+ message = resp.get("message")
+ assert message
+ assert isinstance(message, str)
diff --git a/app_python/tests/test_home_endpoint.py b/app_python/tests/test_home_endpoint.py
new file mode 100644
index 0000000000..2bd12651a9
--- /dev/null
+++ b/app_python/tests/test_home_endpoint.py
@@ -0,0 +1,74 @@
+from app import app
+import pytest
+
+
+def test_home_endpoint():
+ client = app.test_client()
+ response = client.get("/")
+
+ assert (
+ response.status_code == 200
+ or response.status_code == 404
+ or response.status_code == 500
+ )
+ resp = response.get_json()
+
+ if response.status_code == 200:
+ message = resp.get("message")
+ assert message
+ assert isinstance(message, dict)
+
+ service = message.get("service")
+ assert service
+ assert isinstance(service, dict)
+ assert service.get("name")
+ assert service.get("version")
+ assert service.get("description")
+ assert service.get("framework")
+ assert service.get("debug status")
+
+ system = message.get("system")
+ assert system
+ assert isinstance(system, dict)
+ assert system.get("hostname")
+ assert system.get("platform")
+ assert system.get("platform_version")
+ assert system.get("architecture")
+ assert system.get("cpu_count")
+ assert system.get("python_version")
+
+ runtime = message.get("runtime")
+ assert runtime
+ assert isinstance(runtime, dict)
+ assert runtime.get("uptime_seconds")
+ assert runtime.get("uptime_human")
+ assert runtime.get("current_time")
+ assert runtime.get("timezone")
+
+ request = message.get("request")
+ assert request
+ assert isinstance(request, dict)
+ assert request.get("client_ip")
+ assert request.get("port")
+ assert request.get("user_agent")
+ assert request.get("method")
+ assert request.get("path")
+
+ endpoints = message.get("endpoints")
+ assert endpoints
+ assert isinstance(endpoints, list)
+ assert endpoints[0]
+ assert endpoints[0].get("path")
+ assert endpoints[1]
+ assert endpoints[1].get("path")
+
+ elif response.status_code == 404 or response.status_code == 500:
+ assert isinstance(resp, dict)
+
+ error = resp.get("error")
+ assert error
+ assert isinstance(error, str)
+
+ message = resp.get("message")
+ assert message
+ assert isinstance(message, str)
diff --git a/cloudflare/WORKERS.md b/cloudflare/WORKERS.md
new file mode 100644
index 0000000000..bb77eb3bd8
--- /dev/null
+++ b/cloudflare/WORKERS.md
@@ -0,0 +1,215 @@
+# Documentation
+
+## Deployment summary
+
+### Worker URL
+
+[https://edge-api.v-levasheva.workers.dev](https://edge-api.v-levasheva.workers.dev)
+
+### Main routes
+
+/ for general app info
+
+/health for health status
+
+/edge for cloudflare edge metadata
+
+/counter for a persistent KV-based request counter, which shows the number of visits for / route
+
+/admin-check is for checking if the requester is an admin, it uses secrets API_TOKEN and ADMIN_EMAIL, and the requester should provide valid token and email in the request headers
+
+and also there is "Not Found" response for non-existent route
+
+### Configuration used
+
+the worker was configured using wrangler.jsonc, which defines the worker name, entrypoint (src/index.ts), compatibility date, environment variables, and resource bindings. the project uses a TypeScript Worker template and includes an APP_NAME variable binding together with a COUNTER_KV KV namespace binding used for persistent storage in the /counter endpoint.
+
+## Evidence
+
+### Screenshot of Cloudflare dashboard
+
+
+
+### Example /edge JSON response
+
+Firstly, the local testing of all endpoints (for the deployed /edge some info such as asn and httpProtocol were added):
+
+
+
+And the /edge endpoint in the deployed worker:
+
+
+
+### How Workers distributes execution globally?
+
+cloudflare workers are automatically executed on cloudflare edge locations around the world, so requests are handled close to the user without manually selecting regions. unlike traditional VM or PaaS platforms where you often deploy separately to regions, workers use cloudflare’s global network automatically, so there is no separate “deploy to 3 regions” step.
+
+### The difference between workers.dev, Routes, and Custom Domains
+
+- workers.dev is the default public cloudflare subdomain automatically provided for testing and accessing workers
+
+- routes are specific url endpoints that worker will provide access to
+
+- custom domains allow exposing the worker directly through an owned custom domain, not through a provided workers.dev
+
+## Configuration, Secrets & Persistence
+
+### Explain why plaintext vars are not suitable for secrets
+
+variables I added: "APP_NAME" and "COURSE_NAME"
+
+plaintext vars are not safe for secrets because they are stored in config and visible in repo and dashboard, so anyone can read them
+
+### Secrets
+
+I added 2 secrets: API_TOKEN and ADMIN_EMAIL. They are used in the /admin-check endpoint, where the requester can pass their access token and email and see if they can be authenticated as an admin.
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air edge-api % npx wrangler secret list
+[
+ {
+ "name": "ADMIN_EMAIL",
+ "type": "secret_text"
+ },
+ {
+ "name": "API_TOKEN",
+ "type": "secret_text"
+ }
+]
+```
+
+
+
+### Workers KV persistence
+
+### Document what you stored and how you verified it
+
+I stored the number of visits of the / endpoint. Each time / is visited, the counter increases by one, and visits value is updated. The value then can be accessed through the /visits endpoint.
+
+The persistance verification:
+
+
+
+## Observability & Operations
+
+### Example log or metrics screenshot
+
+Logs
+
+
+
+Metrics
+
+
+
+I looked at errors, requests, and request duration metrics for all deployed versions for the last 24 hours.
+
+requests: total of 192. this metrics shows how many http requests hit the Worker through all endpoints.
+
+errors: 0 errors. it can be confusing since I intentially hit invalid endpoint such as https://edge-api.v-levasheva.workers.dev/smth a lot of times to get error: "Not Found", but the case was that the Worker does not count 404 responses as errors, the real errors that are considered are runtime failures, which I did not have.
+
+request duration: 3.15 ms on average. this metric shows the latency - how much it takes to process a request and send back a response.
+
+### Multiple Deployments
+
+* I created about 10 deployments but put here last 2 for readability
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air edge-api % npx wrangler deployments list
+
+ ⛅️ wrangler 4.90.0
+Created: 2026-05-10T16:57:52.287Z
+Author: v.levasheva@innopolis.university
+Source: Unknown (deployment)
+Message: -
+Version(s): (100%) 4844c942-5e05-4199-999f-b54af219ed99
+ Created: 2026-05-10T16:48:11.805Z
+ Tag: -
+ Message: -
+
+Created: 2026-05-10T17:01:05.875Z
+Author: v.levasheva@innopolis.university
+Source: Unknown (deployment)
+Message: -
+Version(s): (100%) 9f0c5465-b3c2-462d-b199-bb1a6dc23d9f
+ Created: 2026-05-10T17:01:03.267Z
+ Tag: -
+ Message: -
+```
+
+```bash
+(devops) fountainer@Veronicas-MacBook-Air edge-api % npx wrangler rollback
+
+ ⛅️ wrangler 4.90.0
+───────────────────
+├ Your current deployment has 1 version(s):
+│
+│ (100%) 9f0c5465-b3c2-462d-b199-bb1a6dc23d9f
+│ Created: 2026-05-10T17:01:03.267267Z
+│ Tag: -
+│ Message: -
+│
+✔ Please provide an optional message for this rollback (120 characters max) … test rollback
+│
+├ WARNING You are about to rollback to Worker Version 4844c942-5e05-4199-999f-b54af219ed99.
+│ This will immediately replace the current deployment and become the active deployment across all your deployed triggers.
+│ However, your local development environment will not be affected by this rollback.
+│ Rolling back to a previous deployment will not rollback any of the bound resources (Durable Object, D1, R2, KV, etc).
+│
+│ (100%) 4844c942-5e05-4199-999f-b54af219ed99
+│ Created: 2026-05-10T16:48:11.805042Z
+│ Tag: -
+│ Message: -
+│
+✔ Are you sure you want to deploy this Worker Version to 100% of traffic? … yes
+Performing rollback...
+│
+╰ SUCCESS Worker Version 4844c942-5e05-4199-999f-b54af219ed99 has been deployed to 100% of traffic.
+
+Current Version ID: 4844c942-5e05-4199-999f-b54af219ed99
+```
+
+## Kubernetes vs Cloudflare Workers Comparison
+
+| Aspect | Kubernetes | Cloudflare Workers |
+|--------|------------|--------------------|
+| Setup complexity | really high, you should create cluster setup, manifests, networking, monitoring, and scaling configuration yourself | much simpler, just write code and deploy with minimal config |
+| Deployment speed | slower, containers must build and start | very fast, seconds are enough |
+| Global distribution | usually requires manual configuration | automatic global edge distribution |
+| Cost (for small apps) | can become expensive due to always-running release | cheaper for lightweight APIs and low traffic apps |
+| State/persistence model | persistent volumes, databases, StatefulSets | stateless by default, persistence through KV |
+| Control/flexibility | very high control over runtime, networking, and storage | less low-level control, but much simpler to use |
+| Best use case | complex and heavy programs, projects that require customisation | edge APIs, lightweight services, and globally distributed low-latency apps |
+
+## When to Use Each
+
+### Scenarios favoring Kubernetes
+
+- complex applications with multiple services
+- custom configuration
+- long-running work processes
+
+### Scenarios favoring Workers
+
+- lightweight APIs
+- edge services
+- globally distributed applications with low latency requirements
+
+### Your recommendation
+
+I would use k8s and VMs for big projects that require heavy customisation, and Cloudflare Workers for lightweight apps that will benefit immensely from locating close to the users.
+
+## Reflection
+
+### What felt easier than Kubernetes?
+
+- to be honest, everything felt easier
+
+### What felt more constrained?
+
+- no control over networking, no customisation, no manual resource distribution and controllable scaling.
+
+### What changed because Workers is not a Docker host?
+
+- as opposed to docker, Workers provide serverless architecture, and Workers runtime model is forced. moreover, I didn't have to keep process running locally, like with docker containers, it was nice.
+
diff --git a/cloudflare/edge-api/.editorconfig b/cloudflare/edge-api/.editorconfig
new file mode 100644
index 0000000000..a727df347a
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/.gitignore b/cloudflare/edge-api/.gitignore
new file mode 100644
index 0000000000..4138168d75
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/.prettierrc b/cloudflare/edge-api/.prettierrc
new file mode 100644
index 0000000000..5c7b5d3c7a
--- /dev/null
+++ b/cloudflare/edge-api/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "printWidth": 140,
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": true
+}
diff --git a/cloudflare/edge-api/.vscode/settings.json b/cloudflare/edge-api/.vscode/settings.json
new file mode 100644
index 0000000000..0126e59b82
--- /dev/null
+++ b/cloudflare/edge-api/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "wrangler.json": "jsonc"
+ }
+}
\ No newline at end of file
diff --git a/cloudflare/edge-api/package-lock.json b/cloudflare/edge-api/package-lock.json
new file mode 100644
index 0000000000..7adee25cf0
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/package.json b/cloudflare/edge-api/package.json
new file mode 100644
index 0000000000..4452b7a7f3
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/src/index.ts b/cloudflare/edge-api/src/index.ts
new file mode 100644
index 0000000000..6b8ad0d63c
--- /dev/null
+++ b/cloudflare/edge-api/src/index.ts
@@ -0,0 +1,129 @@
+/**
+ * Welcome to Cloudflare Workers! This is your first worker.
+ *
+ * - Run `npm run dev` in your terminal to start a development server
+ * - Open a browser tab at http://localhost:8787/ to see your worker in action
+ * - Run `npm run deploy` to publish your worker
+ *
+ * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the
+ * `Env` object can be regenerated with `npm run cf-typegen`.
+ *
+ * Learn more at https://developers.cloudflare.com/workers/
+ */
+
+export interface Env {
+ COURSE_NAME: string;
+ APP_NAME: string;
+ COUNTER_KV: KVNamespace;
+ API_TOKEN: string;
+ ADMIN_EMAIL: string;
+}
+
+function jsonResponse(data: unknown, status = 200): Response {
+ return new Response(JSON.stringify(data, null, 2), {
+ status,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+}
+
+export default {
+ async fetch(request: Request, env: Env): Promise {
+ const url = new URL(request.url);
+
+ if (url.pathname === "/") {
+ const current = await env.COUNTER_KV.get("visits");
+ let count = current ? parseInt(current) : 0;
+ count += 1;
+ await env.COUNTER_KV.put("visits", count.toString());
+
+ console.log("request", {
+ path: url.pathname,
+ method: request.method,
+ colo: request.cf?.colo,
+ });
+
+ return jsonResponse({
+ app: env.APP_NAME,
+ course: env.COURSE_NAME,
+ message: "Hello friend!",
+ framework: "Cloudflare Workers",
+ timestamp: new Date().toISOString(),
+ visits: count
+ });
+ }
+
+ if (url.pathname === "/health") {
+ console.log("request", {
+ path: url.pathname,
+ method: request.method,
+ colo: request.cf?.colo,
+ });
+ return jsonResponse({
+ status: "healthy",
+ timestamp: new Date().toISOString(),
+ });
+ }
+
+ if (url.pathname === "/admin-check") {
+ console.log("request", {
+ path: url.pathname,
+ method: request.method,
+ colo: request.cf?.colo,
+ });
+ const token = request.headers.get("token");
+ const email = request.headers.get("email");
+
+ const valid =
+ token === env.API_TOKEN &&
+ email === env.ADMIN_EMAIL;
+
+ return jsonResponse({
+ valid: valid,
+ });
+ }
+
+
+ if (url.pathname === "/edge") {
+ console.log("request", {
+ path: url.pathname,
+ method: request.method,
+ colo: request.cf?.colo,
+ });
+ return jsonResponse({
+ colo: request.cf?.colo,
+ country: request.cf?.country,
+ city: request.cf?.city,
+ asn: request.cf?.asn,
+ httpProtocol: request.cf?.httpProtocol,
+ tlsVersion: request.cf?.tlsVersion,
+ userAgent: request.headers.get("User-Agent"),
+ });
+ }
+
+ if (url.pathname === "/counter") {
+ console.log("request", {
+ path: url.pathname,
+ method: request.method,
+ colo: request.cf?.colo,
+ });
+ const current = await env.COUNTER_KV.get("visits");
+ let count = current ? parseInt(current) : 0;
+ count += 1;
+
+ return jsonResponse({
+ visits: count,
+ });
+ }
+
+ return jsonResponse(
+ {
+ error: "Not Found",
+ },
+ 404
+ );
+
+
+ },
+};
\ No newline at end of file
diff --git a/cloudflare/edge-api/test/env.d.ts b/cloudflare/edge-api/test/env.d.ts
new file mode 100644
index 0000000000..67b3610dbc
--- /dev/null
+++ b/cloudflare/edge-api/test/env.d.ts
@@ -0,0 +1,3 @@
+declare module "cloudflare:test" {
+ interface ProvidedEnv extends Env {}
+}
diff --git a/cloudflare/edge-api/test/index.spec.ts b/cloudflare/edge-api/test/index.spec.ts
new file mode 100644
index 0000000000..017dbd0c3e
--- /dev/null
+++ b/cloudflare/edge-api/test/index.spec.ts
@@ -0,0 +1,33 @@
+import {
+ env,
+ SELF,
+} from "cloudflare:test";
+
+import { describe, it, expect } from "vitest";
+import worker from "../src/index";
+
+const IncomingRequest = Request;
+
+describe("Edge API worker", () => {
+ it("responds from root endpoint", async () => {
+ const request = new IncomingRequest("http://example.com");
+
+ const response = await worker.fetch(request, env);
+
+ expect(response.status).toBe(200);
+
+ const text = await response.text();
+
+ expect(text).toContain("Hello friend!");
+ });
+
+ it("responds from integration test", async () => {
+ const response = await SELF.fetch("https://example.com");
+
+ expect(response.status).toBe(200);
+
+ const text = await response.text();
+
+ expect(text).toContain("Hello friend!");
+ });
+});
\ No newline at end of file
diff --git a/cloudflare/edge-api/test/tsconfig.json b/cloudflare/edge-api/test/tsconfig.json
new file mode 100644
index 0000000000..978ecd87b7
--- /dev/null
+++ b/cloudflare/edge-api/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "types": ["@cloudflare/vitest-pool-workers"]
+ },
+ "include": ["./**/*.ts", "../worker-configuration.d.ts"],
+ "exclude": []
+}
diff --git a/cloudflare/edge-api/tsconfig.json b/cloudflare/edge-api/tsconfig.json
new file mode 100644
index 0000000000..8c98cdbece
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/vitest.config.mts b/cloudflare/edge-api/vitest.config.mts
new file mode 100644
index 0000000000..7ccad75efa
--- /dev/null
+++ b/cloudflare/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/cloudflare/edge-api/worker-configuration.d.ts b/cloudflare/edge-api/worker-configuration.d.ts
new file mode 100644
index 0000000000..2e82a8c549
--- /dev/null
+++ b/cloudflare/edge-api/worker-configuration.d.ts
@@ -0,0 +1,13559 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: dde33b4d5fc92e4633f964a9bd2a5f64)
+// Runtime types generated with workerd@1.20260507.1 2026-05-09 nodejs_compat
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./src/index");
+ }
+ interface Env {
+ COUNTER_KV: KVNamespace;
+ APP_NAME: "my-edge-api-app";
+ COURSE_NAME: "devops-core";
+ }
+}
+interface Env extends Cloudflare.Env {}
+type StringifyValues> = {
+ [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
+};
+declare namespace NodeJS {
+ interface ProcessEnv extends StringifyValues> {}
+}
+
+// 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