Compare commits

..

32 Commits

Author SHA1 Message Date
Morten Olsen
4d46998668 more-charts 2025-09-03 21:41:58 +02:00
Morten Olsen
00d90bfa21 more-stuff 2025-09-03 17:24:27 +02:00
Morten Olsen
03e406322f more stuff 2025-09-03 15:16:50 +02:00
Morten Olsen
5ee7a76443 more stuff 2025-09-03 14:33:48 +02:00
mortenolsenzn
683de402ff Merge pull request #1 from morten-olsen/rewrite2
Rewrite2
2025-09-03 12:24:40 +02:00
Morten Olsen
e8e939ad19 fixes 2025-08-22 11:44:53 +02:00
Morten Olsen
1b5b5145b0 stuff 2025-08-22 07:35:50 +02:00
Morten Olsen
cfd2d76873 more 2025-08-20 22:45:30 +02:00
Morten Olsen
9e5081ed9b updates 2025-08-20 14:58:34 +02:00
Morten Olsen
3ab2b1969a stuff 2025-08-19 22:05:41 +02:00
Morten Olsen
a27b563113 rewrite2 2025-08-18 08:02:48 +02:00
Morten Olsen
295472a028 update 2025-08-15 22:01:18 +02:00
Morten Olsen
91298b3cf7 update 2025-08-15 21:20:23 +02:00
Morten Olsen
638c288a5c update 2025-08-15 20:52:17 +02:00
Morten Olsen
2be6bdca84 update 2025-08-15 20:45:28 +02:00
Morten Olsen
f362f4afc4 fix: missing permissions 2025-08-13 09:01:30 +02:00
Morten Olsen
9fadbf75fb publish operator yaml 2025-08-13 08:50:17 +02:00
Morten Olsen
2add15d283 fix: authentik port 2025-08-12 23:25:03 +02:00
Morten Olsen
5426495be5 updates 2025-08-12 23:22:47 +02:00
Morten Olsen
b8bb16ccbb updates 2025-08-12 22:32:09 +02:00
Morten Olsen
d4b56007f1 add authentik connection crd 2025-08-12 08:36:29 +02:00
Morten Olsen
130bfec468 fix reconciliation of db 2025-08-11 20:00:01 +02:00
Morten Olsen
ddb3c79657 fix pg db 2025-08-11 15:00:06 +02:00
Morten Olsen
47cf43b44e Added storage provisioner 2025-08-11 12:07:36 +02:00
Morten Olsen
aa6d14738a simplify 2025-08-07 23:26:33 +02:00
Morten Olsen
9cdbaf7929 stuff 2025-08-07 22:21:33 +02:00
Morten Olsen
cfb90f7c9f more 2025-08-06 21:18:02 +02:00
Morten Olsen
757b2fcfac lot more stuff 2025-08-04 23:44:14 +02:00
Morten Olsen
daf0ea21bb update 2025-08-01 14:47:53 +02:00
Morten Olsen
26b58a59c0 lot of updates 2025-08-01 14:40:16 +02:00
Morten Olsen
a25e0b9ffb updates 2025-08-01 07:52:09 +02:00
Morten Olsen
5782d59f71 add dotenv 2025-07-31 13:23:01 +02:00
306 changed files with 132075 additions and 60267 deletions

View File

@@ -71,9 +71,23 @@ jobs:
environment: release
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
- id: create-release
uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter-config.yml
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create-release.outputs.upload_url }}
asset_path: ./operator.yaml
asset_name: operator.yaml
asset_content_type: application/yaml

4
.gitignore vendored
View File

@@ -32,3 +32,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
/data/
/cloudflare.yaml

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -1,6 +1,6 @@
FROM node:23-alpine
FROM node:23-slim
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
COPY . .
CMD ["node", "src/index.ts"]
CMD ["node", "src/index.ts"]

14
Makefile Normal file
View File

@@ -0,0 +1,14 @@
.PHONY: dev-recreate dev-destroy server-install
dev-destroy:
colima delete -f
dev-recreate: dev-destroy
colima start --network-address --kubernetes -m 8 --k3s-arg="--disable helm-controller,local-storage,traefik --docker" # --mount ${PWD}/data:/data:w
flux install --components="source-controller,helm-controller"
setup-flux:
flux install --components="source-controller,helm-controller"
server-install:
curl -sfL https://get.k3s.io | sh -s - --disable traefik,local-storage,helm-controller

282
README.md
View File

@@ -1,282 +0,0 @@
# homelab-operator
A Kubernetes operator designed for homelab environments that simplifies the
management of PostgreSQL databases and Kubernetes secrets. Built with TypeScript
and designed to run efficiently in resource-constrained environments.
## Features
- **PostgreSQL Database Management**: Automatically create and manage PostgreSQL
databases and roles
- **Secret Management**: Generate and manage Kubernetes secrets with
configurable data
- **Owner References**: Automatic cleanup when resources are deleted
- **Status Tracking**: Comprehensive status conditions and error reporting
- **Lightweight**: Minimal resource footprint suitable for homelab environments
## Architecture
The operator manages two main Custom Resource Definitions (CRDs):
### PostgresDatabase
Manages PostgreSQL databases and their associated roles:
- Creates a PostgreSQL role with a secure random password
- Creates a database owned by that role
- Generates a Kubernetes secret containing database credentials
- Ensures proper cleanup through owner references
### SecretRequest
Generates Kubernetes secrets with configurable data:
- Supports custom secret names
- Configurable data fields with various encodings
- Automatic secret lifecycle management
## Installation
### Prerequisites
- Kubernetes cluster (1.20+)
- PostgreSQL instance accessible from the cluster
- Helm 3.x (for chart-based installation)
### Using Helm Chart
1. Clone the repository:
```bash
git clone <repository-url>
cd homelab-operator
```
2. Install using Helm:
```bash
helm install homelab-operator ./chart \
--set-string env.POSTGRES_HOST=<your-postgres-host> \
--set-string env.POSTGRES_USER=<admin-user> \
--set-string env.POSTGRES_PASSWORD=<admin-password>
```
### Using kubectl
1. Build and push the Docker image:
```bash
docker build -t your-registry/homelab-operator:latest .
docker push your-registry/homelab-operator:latest
```
2. Apply the Kubernetes manifests:
```bash
kubectl apply -f chart/templates/
```
## Configuration
The operator is configured through environment variables:
| Variable | Description | Required | Default |
| ------------------- | ---------------------------------------- | -------- | ------- |
| `POSTGRES_HOST` | PostgreSQL server hostname | Yes | - |
| `POSTGRES_USER` | PostgreSQL admin username | Yes | - |
| `POSTGRES_PASSWORD` | PostgreSQL admin password | Yes | - |
| `POSTGRES_PORT` | PostgreSQL server port | No | 5432 |
| `LOG_LEVEL` | Logging level (debug, info, warn, error) | No | info |
## Usage
### PostgreSQL Database
Create a PostgreSQL database with an associated role:
```yaml
apiVersion: homelab.mortenolsen.pro/v1
kind: PostgresDatabase
metadata:
name: my-app-db
namespace: my-namespace
spec: {}
```
This will create:
- A PostgreSQL role named `my-app-db`
- A PostgreSQL database named `my-namespace_my-app-db` owned by the role
- A Kubernetes secret `postgres-database-my-app-db` containing:
- `name`: Base64-encoded database name
- `user`: Base64-encoded username
- `password`: Base64-encoded password
### Secret Request
Generate a Kubernetes secret with custom data:
```yaml
apiVersion: homelab.mortenolsen.pro/v1
kind: SecretRequest
metadata:
name: my-secret
namespace: my-namespace
spec:
secretName: app-config
data:
- key: api-key
value: "my-api-key"
encoding: base64
- key: database-url
value: "postgresql://user:pass@host:5432/db"
- key: random-token
length: 32
chars: "abcdefghijklmnopqrstuvwxyz0123456789"
```
### Accessing Created Resources
To retrieve database credentials:
```bash
# Get the secret
kubectl get secret postgres-database-my-app-db -o jsonpath='{.data.user}' | base64 -d
kubectl get secret postgres-database-my-app-db -o jsonpath='{.data.password}' | base64 -d
kubectl get secret postgres-database-my-app-db -o jsonpath='{.data.name}' | base64 -d
```
## Development
### Prerequisites
- [Bun](https://bun.sh/) runtime
- [pnpm](https://pnpm.io/) package manager
- Docker (for building images)
- Access to a Kubernetes cluster for testing
### Setup
1. Clone the repository:
```bash
git clone <repository-url>
cd homelab-operator
```
2. Install dependencies:
```bash
pnpm install
```
3. Set up development environment:
```bash
cp .env.example .env
# Edit .env with your PostgreSQL connection details
```
### Running Locally
For development, you can run the operator locally against a remote cluster:
```bash
# Ensure kubectl is configured for your development cluster
export KUBECONFIG=~/.kube/config
# Set PostgreSQL connection environment variables
export POSTGRES_HOST=localhost
export POSTGRES_USER=postgres
export POSTGRES_PASSWORD=yourpassword
# Run the operator
bun run src/index.ts
```
### Development with Docker Compose
A development environment with PostgreSQL is provided:
```bash
docker-compose -f docker-compose.dev.yaml up -d
```
### Building
Build the Docker image:
```bash
docker build -t homelab-operator:latest .
```
### Testing
```bash
# Run linting
pnpm run test:lint
# Apply test resources
kubectl apply -f test.yaml
```
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/new-feature`
3. Make your changes and add tests
4. Run linting: `pnpm run test:lint`
5. Commit your changes: `git commit -am 'Add new feature'`
6. Push to the branch: `git push origin feature/new-feature`
7. Submit a pull request
## Project Structure
```
├── chart/ # Helm chart for deployment
├── src/
│ ├── crds/ # Custom Resource Definitions
│ │ ├── postgres/ # PostgreSQL database management
│ │ └── secrets/ # Secret generation
│ ├── custom-resource/ # Base CRD framework
│ ├── database/ # Database migrations
│ ├── services/ # Core services
│ │ ├── config/ # Configuration management
│ │ ├── k8s.ts # Kubernetes API client
│ │ ├── log/ # Logging service
│ │ ├── postgres/ # PostgreSQL service
│ │ └── secrets/ # Secret management
│ └── utils/ # Utilities and constants
├── Dockerfile # Container build configuration
└── docker-compose.dev.yaml # Development environment
```
## License
This project is licensed under the MIT License - see the LICENSE file for
details.
## Support
For support and questions:
- Create an issue in the GitHub repository
- Check existing issues for similar problems
- Review the logs using `kubectl logs -l app=homelab-operator`
## Status Monitoring
Monitor the operator status:
```bash
# Check operator logs
kubectl logs -l app=homelab-operator -f
# Check CRD status
kubectl get postgresdatabases
kubectl get secretrequests
# Describe resources for detailed status
kubectl describe postgresdatabase my-app-db
kubectl describe secretrequest my-secret
```

View File

@@ -1,14 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "homelab-operator.fullname" . }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "watch", "list"]
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "watch", "list", "patch"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "create", "replace"]

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: audiobookshelf

View File

@@ -0,0 +1,13 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
redirectUris:
- path: /audiobookshelf/auth/openid/callback
subdomain: '{{ .Values.subdomain }}'
matchingMode: strict
- path: /audiobookshelf/auth/openid/mobile-redirect
subdomain: '{{ .Values.subdomain }}'
matchingMode: strict

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /config
name: config
- mountPath: /metadata
name: metadata
- mountPath: /audiobooks
name: audiobooks
- mountPath: /podcasts
name: podcasts
volumes:
- name: config
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-config'
- name: metadata
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-metadata'
- name: audiobooks
persistentVolumeClaim:
claimName: books
- name: podcasts
persistentVolumeClaim:
claimName: podcasts

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,24 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-config'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-metadata'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,7 @@
globals:
environment: prod
image:
repository: ghcr.io/advplyr/audiobookshelf
tag: 2.26.1
pullPolicy: IfNotPresent
subdomain: audiobookshelf

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: ByteStash

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}-headless'
labels:
app: '{{ .Release.Name }}'
spec:
clusterIP: None
ports:
- port: 5000
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: HttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}'
port:
number: 80

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
redirectUris:
- path: /api/auth/oidc/callback
subdomain: bytestash
matchingMode: strict

View File

@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
serviceName: '{{ .Release.Name }}-headless'
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: ghcr.io/jordan-dalby/bytestash:latest
ports:
- containerPort: 5000
name: http
env:
- name: ALLOW_NEW_ACCOUNTS
value: 'true'
- name: DISABLE_INTERNAL_ACCOUNTS
value: 'true'
- name: OIDC_ENABLED
value: 'true'
- name: OIDC_DISPLAY_NAME
value: OIDC
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientId
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientSecret
- name: OIDC_ISSUER_URL
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: configuration
volumeMounts:
- mountPath: /data/snippets
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-data'

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-data'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 5000
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,3 @@
globals:
environment: prod
subdomain: bytestash

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Jellyfin

View File

@@ -0,0 +1 @@
https://www.authelia.com/integration/openid-connect/clients/jellyfin/

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.environment }}'
redirectUris:
- path: /sso/OID/redirect/Authentik
subdomain: '{{ .Values.globals.subdomain }}'
matchingMode: strict

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-config'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.environment }}'

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 8096
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /config
name: config
- mountPath: /media/movies
name: movies
- mountPath: /media/tv-shows
name: tvshows
- mountPath: /media/music
name: music
volumes:
- name: config
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-config'
- name: movies
persistentVolumeClaim:
claimName: movies
- name: tvshows
persistentVolumeClaim:
claimName: tvshows
- name: music
persistentVolumeClaim:
claimName: music

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8096
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,7 @@
globals:
environment: prod
image:
repository: docker.io/jellyfin/jellyfin
tag: latest
pullPolicy: IfNotPresent
subdomain: jellyfin

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: ByteStash

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
redirectUris:
- path: /api/auth/oidc/callback
subdomain: bytestash
matchingMode: strict

View File

@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
serviceName: '{{ .Release.Name }}-headless'
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: ghcr.io/miniflux/miniflux:latest
ports:
- containerPort: 8080
name: http
env:
- name: ALLOW_NEW_ACCOUNTS
value: 'true'
- name: DISABLE_INTERNAL_ACCOUNTS
value: 'true'
- name: OIDC_ENABLED
value: 'true'
- name: OIDC_DISPLAY_NAME
value: OIDC
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientId
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientSecret
- name: OIDC_ISSUER_URL
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: configuration
volumeMounts:
- mountPath: /data/snippets
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-data'

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-data'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,3 @@
globals:
environment: prod
subdomain: miniflux

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Jellyfin

View File

@@ -0,0 +1,6 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: PostgresDatabase
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 5678
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /home/node/.n8n
name: data
env:
- name: TZ
value: '{{ .Values.globals.timezone }}'
- name: GENERIC_TIMEZONE
value: '{{ .Values.globals.timezone }}'
- name: N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS
value: 'true'
- name: N8N_RUNNERS_ENABLED
value: 'true'
- name: DB_TYPE
value: postgresdb
- name: DB_POSTGRESDB_DATABASE
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-pg-connection'
key: database
- name: DB_POSTGRESDB_HOST
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-pg-connection'
key: host
- name: DB_POSTGRESDB_PORT
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-pg-connection'
key: port
- name: DB_POSTGRESDB_USER
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-pg-connection'
key: user
- name: DB_POSTGRESDB_PASSWORD
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-pg-connection'
key: password
volumes:
- name: data
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-data'

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-data'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 5678
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,8 @@
globals:
environment: prod
timezone: Europe/Amsterdam
image:
repository: docker.n8n.io/n8nio/n8n
tag: latest
pullPolicy: IfNotPresent
subdomain: n8n

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: ollama

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
redirectUris:
- path: /oauth/oidc/callback
subdomain: '{{ .Values.subdomain }}'
matchingMode: strict

View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 11434
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /root/.ollama
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-data'

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-data'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 11434
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,7 @@
globals:
environment: prod
image:
repository: ollama/ollama
tag: 0.11.8
pullPolicy: IfNotPresent
subdomain: openwebui

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: openwebui

View File

@@ -0,0 +1,10 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
redirectUris:
- path: /oauth/oidc/callback
subdomain: '{{ .Values.subdomain }}'
matchingMode: strict

View File

@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: '{{ .Release.Name }}'
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: '{{ .Release.Name }}'
template:
metadata:
labels:
app: '{{ .Release.Name }}'
spec:
containers:
- name: '{{ .Release.Name }}'
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: '{{ .Values.image.pullPolicy }}'
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
tcpSocket:
port: http
readinessProbe:
tcpSocket:
port: http
volumeMounts:
- mountPath: /app/backend/data
name: data
env:
- name: ENABLE_SIGNUP
value: 'false'
- name: WEBUI_URL # TODO: remove
value: https://openwebui.olsen.cloud
- name: ENABLE_OAUTH_PERSISTENT_CONFIG
value: 'false'
- name: ENABLE_OAUTH_SIGNUP
value: 'true'
- name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL
value: 'true'
- name: OAUTH_PROVIDER_NAME
value: authentik
- name: OPENID_PROVIDER_URL
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: configuration
- name: OAUTH_CLIENT_ID
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientId
- name: OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: '{{ .Release.Name }}-client'
key: clientSecret
- name: ENABLE_LOGIN_FORM
value: 'false'
- name: OPENID_REDIRECT
value: https://openwebui.olsen.cloud/oauth/oidc/callback
volumes:
- name: data
persistentVolumeClaim:
claimName: '{{ .Release.Name }}-data'

View File

@@ -0,0 +1,11 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: ExternalHttpService
metadata:
name: '{{ .Release.Name }}'
spec:
environment: '{{ .Values.globals.environment }}'
subdomain: '{{ .Values.subdomain }}'
destination:
host: '{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local'
port:
number: 80

View File

@@ -0,0 +1,11 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: '{{ .Release.Name }}-data'
spec:
accessModes:
- 'ReadWriteOnce'
resources:
requests:
storage: '1Gi'
storageClassName: '{{ .Values.globals.environment }}'

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: '{{ .Release.Name }}'
labels:
app: '{{ .Release.Name }}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: '{{ .Release.Name }}'

View File

@@ -0,0 +1,7 @@
globals:
environment: prod
image:
repository: ghcr.io/open-webui/open-webui
tag: main
pullPolicy: IfNotPresent
subdomain: openwebui

View File

@@ -0,0 +1,12 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: {{ include "homelab-operator.fullname" . }}-local-path
labels:
{{- include "homelab-operator.labels" . | nindent 4 }}
provisioner: reuse-local-path-provisioner
parameters:
# Add any provisioner-specific parameters here
reclaimPolicy: {{ .Values.storage.reclaimPolicy | default "Retain" }}
allowVolumeExpansion: {{ .Values.storage.allowVolumeExpansion | default false }}
volumeBindingMode: {{ .Values.storage.volumeBindingMode | default "WaitForFirstConsumer" }}

View File

@@ -0,0 +1,32 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "homelab-operator.fullname" . }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "watch", "list"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["persistentvolumeclaims/status"]
verbs: ["update", "patch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "watch", "list", "patch", "create", "update", "replace"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "create", "update", "replace", "patch"]

View File

@@ -33,6 +33,14 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data-volumes
mountPath: {{ .Values.storage.path }}
volumes:
- name: data-volumes
hostPath:
path: {{ .Values.storage.path }}
type: DirectoryOrCreate
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View File

@@ -4,13 +4,19 @@
image:
repository: ghcr.io/morten-olsen/homelab-operator
pullPolicy: Always
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: main
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
nameOverride: ''
fullnameOverride: ''
storage:
path: /data/volumes
reclaimPolicy: Retain
allowVolumeExpansion: false
volumeBindingMode: WaitForFirstConsumer
serviceAccount:
# Specifies whether a service account should be created
@@ -19,7 +25,7 @@ serviceAccount:
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
name: ''
podAnnotations: {}

3
charts/root/Chart.yaml Normal file
View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: root

View File

@@ -0,0 +1,33 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: homelab-apps
namespace: '{{ .Values.env }}-argo'
spec:
generators:
- git:
repoURL: '{{ .Values.repo }}'
revision: '{{ .Values.ref }}'
directories:
- path: charts/apps/*
include: '.*'
exclude: '.*.disabled'
template:
metadata:
name: '{{`{{path.basename}}`}}'
spec:
project: default
source:
repoURL: '{{ .Values.repo }}'
targetRevision: '{{ .Values.ref }}'
path: charts/apps/{{`{{path.basename}}`}}
helm:
values: |
globals: {{ .Values.globals | toYaml | nindent 14 }}
destination:
server: https://kubernetes.default.svc
namespace: '{{ .Values.globals.env }}'
syncPolicy:
automated:
prune: true
selfHeal: true

View File

@@ -0,0 +1,21 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homelab-root
namespace: '{{ .Values.globals.env }}-argo'
spec:
project: default
source:
repoURL: '{{ .Values.repo }}'
targetRevision: '{{ .Values.ref }}'
path: charts/root
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: '{{ .Values.globals.env }}-argo'
syncPolicy:
automated:
prune: true
selfHeal: true

4
charts/root/values.yaml Normal file
View File

@@ -0,0 +1,4 @@
globals:
env: prod
repo: https://github.com/morten-olsen/homelab-operator.git
ref: HEAD

View File

@@ -0,0 +1,3 @@
apiVersion: v2
version: 1.0.0
name: Resources

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: books
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-books
nfs:
path: '{{ .Values.books.path }}'
server: '{{ .Values.host }}'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: books
spec:
storageClassName: manual-books
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: movies
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-movies
nfs:
path: '{{ .Values.movies.path }}'
server: '{{ .Values.host }}'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: movies
spec:
storageClassName: manual-movies
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: music
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-music
nfs:
path: '{{ .Values.music.path }}'
server: '{{ .Values.host }}'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: music
spec:
storageClassName: manual-music
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: podcasts
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-podcasts
nfs:
path: '{{ .Values.podcasts.path }}'
server: '{{ .Values.host }}'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: podcasts
spec:
storageClassName: manual-podcasts
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: tvshows
labels:
type: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-tvshows
nfs:
path: '{{ .Values.tvshows.path }}'
server: '{{ .Values.host }}'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tvshows
spec:
storageClassName: manual-tvshows
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi

View File

@@ -0,0 +1,11 @@
host: 192.168.20.106
movies:
path: /mnt/HDD/Movies
tvshows:
path: /mnt/HDD/TV-Shows
music:
path: /mnt/HDD/Music2
books:
path: /mnt/HDD/Books
podcasts:
path: /mnt/HDD/Podcasts

View File

@@ -1,12 +0,0 @@
name: homelab
services:
postgres:
image: postgres:17
ports:
- 5432:5432
environment:
POSTGRES_USER: $POSTGRES_USER
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_DB: ${POSTGRES_DB:-postgres}
volumes:
- $PWD/.data/local/postgres:/var/lib/postgresql/data

22
istio-test.yaml Normal file
View File

@@ -0,0 +1,22 @@
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: dev-authentik-override
namespace: dev
spec:
hosts:
- authentik.mortenolsen.nett
ports:
- number: 443
name: https
protocol: HTTPS
- number: 80
name: http
protocol: HTTP
location: MESH_EXTERNAL
resolution: STATIC
endpoints:
- address: 1.1.1.1
ports:
https: 443
http: 80

9
manifests/client.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: OidcClient
metadata:
name: test-client
spec:
environment: dev
redirectUris:
- url: https://localhost:3000/api/v1/authentik/oauth2/callback
matchingMode: strict

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
apiVersion: homelab.mortenolsen.pro/v1
kind: Environment
metadata:
name: prod
spec:
domain: olsen.cloud
networkIp: 192.168.20.180
tls:
issuer: lets-encrypt-prod

View File

@@ -0,0 +1,39 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: homelab-operator-local-path
---
apiVersion: v1
kind: Pod
metadata:
name: example-pod
namespace: default
spec:
containers:
- name: example-container
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]
volumeMounts:
- name: example-volume
mountPath: /data
resources:
limits:
memory: 100Mi
cpu: "0.1"
requests:
memory: 50Mi
cpu: "0.05"
volumes:
- name: example-volume
persistentVolumeClaim:
claimName: example-pvc

View File

@@ -0,0 +1,14 @@
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: test-example-com
namespace: dev
spec:
hosts:
- authentik.one.dev.olsen.cloud
# (the address field is optional if you use 'resolution: DNS')
ports:
- number: 80
name: https
protocol: HTTPS
resolution: DNS

35
operator.yaml Normal file
View File

@@ -0,0 +1,35 @@
apiVersion: v1
kind: Namespace
metadata:
name: homelab
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: homelab
namespace: homelab
spec:
interval: 60m
url: https://github.com/morten-olsen/homelab-operator
ref:
branch: main
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: operator
namespace: homelab
spec:
releaseName: operator
interval: 60m
chart:
spec:
chart: charts/operator
sourceRef:
kind: GitRepository
name: homelab
namespace: homelab

View File

@@ -6,10 +6,12 @@
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.32.0",
"@types/deep-equal": "^1.0.4",
"eslint": "9.32.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.3",
"json-schema-to-typescript": "^15.0.4",
"prettier": "3.6.2",
"typescript": "5.8.3",
"typescript-eslint": "8.38.0"
@@ -20,12 +22,27 @@
"dependencies": {
"@goauthentik/api": "2025.6.3-1751754396",
"@kubernetes/client-node": "^1.3.0",
"cloudflare": "^4.5.0",
"cron": "^4.3.3",
"debounce": "^2.2.0",
"deep-equal": "^2.2.3",
"dotenv": "^17.2.1",
"eventemitter3": "^5.0.1",
"execa": "^9.6.0",
"knex": "^3.1.0",
"p-queue": "^8.1.0",
"p-retry": "^6.2.1",
"pg": "^8.16.3",
"sqlite3": "^5.1.7",
"yaml": "^2.8.0",
"zod": "^4.0.14"
},
"imports": {
"#services/*": "./src/services/*",
"#resources/*": "./src/resources/*",
"#bootstrap/*": "./src/bootstrap/*",
"#utils/*": "./src/utils/*"
},
"packageManager": "pnpm@10.6.0",
"pnpm": {
"onlyBuiltDependencies": [

433
pnpm-lock.yaml generated
View File

@@ -14,9 +14,36 @@ importers:
'@kubernetes/client-node':
specifier: ^1.3.0
version: 1.3.0(encoding@0.1.13)
cloudflare:
specifier: ^4.5.0
version: 4.5.0(encoding@0.1.13)
cron:
specifier: ^4.3.3
version: 4.3.3
debounce:
specifier: ^2.2.0
version: 2.2.0
deep-equal:
specifier: ^2.2.3
version: 2.2.3
dotenv:
specifier: ^17.2.1
version: 17.2.1
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
execa:
specifier: ^9.6.0
version: 9.6.0
knex:
specifier: ^3.1.0
version: 3.1.0(pg@8.16.3)(sqlite3@5.1.7)
p-queue:
specifier: ^8.1.0
version: 8.1.0
p-retry:
specifier: ^6.2.1
version: 6.2.1
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -36,6 +63,9 @@ importers:
'@eslint/js':
specifier: 9.32.0
version: 9.32.0
'@types/deep-equal':
specifier: ^1.0.4
version: 1.0.4
eslint:
specifier: 9.32.0
version: 9.32.0
@@ -48,6 +78,9 @@ importers:
eslint-plugin-prettier:
specifier: 5.5.3
version: 5.5.3(eslint-config-prettier@10.1.8(eslint@9.32.0))(eslint@9.32.0)(prettier@3.6.2)
json-schema-to-typescript:
specifier: ^15.0.4
version: 15.0.4
prettier:
specifier: 3.6.2
version: 3.6.2
@@ -60,6 +93,10 @@ importers:
packages:
'@apidevtools/json-schema-ref-parser@11.9.3':
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
engines: {node: '>= 16'}
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -124,6 +161,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@jsdevtools/ono@7.1.3':
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
'@jsep-plugin/assignment@1.3.0':
resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==}
engines: {node: '>= 10.16.0'}
@@ -166,10 +206,20 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@tootallnate/once@1.1.2':
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
engines: {node: '>= 6'}
'@types/deep-equal@1.0.4':
resolution: {integrity: sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -182,12 +232,24 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/luxon@3.7.1':
resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==}
'@types/node-fetch@2.6.12':
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
'@types/node@18.19.123':
resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==}
'@types/node@22.16.5':
resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==}
'@types/retry@0.12.2':
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
'@types/stream-buffers@3.0.7':
resolution: {integrity: sha512-azOCy05sXVXrO+qklf0c/B07H/oHaIuDDAiHPVwlk3A9Ek+ksHyTeMajLZl3r76FxpPpxem//4Te61G1iW3Giw==}
@@ -253,6 +315,10 @@ packages:
abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -429,6 +495,9 @@ packages:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
cloudflare@4.5.0:
resolution: {integrity: sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -457,6 +526,10 @@ packages:
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
cron@4.3.3:
resolution: {integrity: sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==}
engines: {node: '>=18.x'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -473,6 +546,10 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
debounce@2.2.0:
resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==}
engines: {node: '>=18'}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -503,6 +580,10 @@ packages:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@@ -533,6 +614,10 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dotenv@17.2.1:
resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -565,6 +650,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-get-iterator@1.1.3:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -689,6 +777,17 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
execa@9.6.0:
resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
engines: {node: ^18.19.0 || >=20.5.0}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
@@ -715,6 +814,18 @@ packages:
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -741,10 +852,17 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -782,6 +900,10 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
get-symbol-description@1.1.0:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
@@ -867,6 +989,10 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
human-signals@8.0.1:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
@@ -922,6 +1048,10 @@ packages:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
is-arguments@1.2.0:
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
engines: {node: '>= 0.4'}
is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -985,6 +1115,10 @@ packages:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
engines: {node: '>= 0.4'}
is-network-error@1.1.0:
resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==}
engines: {node: '>=16'}
is-number-object@1.1.1:
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
engines: {node: '>= 0.4'}
@@ -993,6 +1127,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -1005,6 +1143,10 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
is-stream@4.0.1:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@@ -1017,6 +1159,10 @@ packages:
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
engines: {node: '>= 0.4'}
is-unicode-supported@2.1.0:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
@@ -1057,6 +1203,11 @@ packages:
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-schema-to-typescript@15.0.4:
resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
engines: {node: '>=16.0.0'}
hasBin: true
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -1121,6 +1272,10 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
luxon@3.7.1:
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
engines: {node: '>=12'}
make-fetch-happen@9.1.0:
resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==}
engines: {node: '>= 10'}
@@ -1222,6 +1377,11 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -1241,6 +1401,10 @@ packages:
engines: {node: '>=6'}
hasBin: true
npm-run-path@6.0.0:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'}
npmlog@6.0.2:
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -1253,6 +1417,10 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
object-is@1.1.6:
resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
engines: {node: '>= 0.4'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -1299,10 +1467,26 @@ packages:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
p-queue@8.1.0:
resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==}
engines: {node: '>=18'}
p-retry@6.2.1:
resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==}
engines: {node: '>=16.17'}
p-timeout@6.1.4:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -1315,6 +1499,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -1359,6 +1547,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -1397,6 +1589,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
pretty-ms@9.2.0:
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
engines: {node: '>=18'}
promise-inflight@1.0.1:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
@@ -1456,6 +1652,10 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -1540,6 +1740,10 @@ packages:
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@@ -1614,6 +1818,10 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
strip-final-newline@4.0.0:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -1662,6 +1870,10 @@ packages:
resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
engines: {node: '>=8'}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -1717,9 +1929,16 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
unique-filename@1.1.1:
resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
@@ -1732,6 +1951,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -1797,11 +2020,21 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yoctocolors@2.1.1:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
zod@4.0.14:
resolution: {integrity: sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==}
snapshots:
'@apidevtools/json-schema-ref-parser@11.9.3':
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.0
'@eslint-community/eslint-utils@4.7.0(eslint@9.32.0)':
dependencies:
eslint: 9.32.0
@@ -1864,6 +2097,8 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@jsdevtools/ono@7.1.3': {}
'@jsep-plugin/assignment@1.3.0(jsep@1.4.0)':
dependencies:
jsep: 1.4.0
@@ -1925,9 +2160,15 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/merge-streams@4.0.0': {}
'@tootallnate/once@1.1.2':
optional: true
'@types/deep-equal@1.0.4': {}
'@types/estree@1.0.8': {}
'@types/js-yaml@4.0.9': {}
@@ -1936,15 +2177,25 @@ snapshots:
'@types/json5@0.0.29': {}
'@types/lodash@4.17.20': {}
'@types/luxon@3.7.1': {}
'@types/node-fetch@2.6.12':
dependencies:
'@types/node': 22.16.5
form-data: 4.0.4
'@types/node@18.19.123':
dependencies:
undici-types: 5.26.5
'@types/node@22.16.5':
dependencies:
undici-types: 6.21.0
'@types/retry@0.12.2': {}
'@types/stream-buffers@3.0.7':
dependencies:
'@types/node': 22.16.5
@@ -2045,6 +2296,10 @@ snapshots:
abbrev@1.1.1:
optional: true
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -2063,7 +2318,6 @@ snapshots:
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
optional: true
aggregate-error@3.1.0:
dependencies:
@@ -2268,6 +2522,18 @@ snapshots:
clean-stack@2.2.0:
optional: true
cloudflare@4.5.0(encoding@0.1.13):
dependencies:
'@types/node': 18.19.123
'@types/node-fetch': 2.6.12
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2290,6 +2556,11 @@ snapshots:
console-control-strings@1.1.0:
optional: true
cron@4.3.3:
dependencies:
'@types/luxon': 3.7.1
luxon: 3.7.1
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2314,6 +2585,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
debounce@2.2.0: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
@@ -2330,6 +2603,27 @@ snapshots:
dependencies:
mimic-response: 3.1.0
deep-equal@2.2.3:
dependencies:
array-buffer-byte-length: 1.0.2
call-bind: 1.0.8
es-get-iterator: 1.1.3
get-intrinsic: 1.3.0
is-arguments: 1.2.0
is-array-buffer: 3.0.5
is-date-object: 1.1.0
is-regex: 1.2.1
is-shared-array-buffer: 1.0.4
isarray: 2.0.5
object-is: 1.1.6
object-keys: 1.1.1
object.assign: 4.1.7
regexp.prototype.flags: 1.5.4
side-channel: 1.1.0
which-boxed-primitive: 1.1.1
which-collection: 1.0.2
which-typed-array: 1.1.19
deep-extend@0.6.0: {}
deep-is@0.1.4: {}
@@ -2357,6 +2651,8 @@ snapshots:
dependencies:
esutils: 2.0.3
dotenv@17.2.1: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -2442,6 +2738,18 @@ snapshots:
es-errors@1.3.0: {}
es-get-iterator@1.1.3:
dependencies:
call-bind: 1.0.8
get-intrinsic: 1.3.0
has-symbols: 1.1.0
is-arguments: 1.2.0
is-map: 2.0.3
is-set: 2.0.3
is-string: 1.1.1
isarray: 2.0.5
stop-iteration-iterator: 1.1.0
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -2596,6 +2904,25 @@ snapshots:
esutils@2.0.3: {}
event-target-shim@5.0.1: {}
eventemitter3@5.0.1: {}
execa@9.6.0:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
cross-spawn: 7.0.6
figures: 6.1.0
get-stream: 9.0.1
human-signals: 8.0.1
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
pretty-ms: 9.2.0
signal-exit: 4.1.0
strip-final-newline: 4.0.0
yoctocolors: 2.1.1
expand-template@2.0.3: {}
fast-deep-equal@3.1.3: {}
@@ -2620,6 +2947,14 @@ snapshots:
dependencies:
reusify: 1.1.0
fdir@6.4.6(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -2646,6 +2981,8 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data-encoder@1.7.2: {}
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
@@ -2654,6 +2991,11 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
formdata-node@4.4.1:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
fs-constants@1.0.0: {}
fs-minipass@2.1.0:
@@ -2708,6 +3050,11 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
get-symbol-description@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -2797,10 +3144,11 @@ snapshots:
- supports-color
optional: true
human-signals@8.0.1: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
optional: true
iconv-lite@0.6.3:
dependencies:
@@ -2849,6 +3197,11 @@ snapshots:
jsbn: 1.1.0
sprintf-js: 1.1.3
is-arguments@1.2.0:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.8
@@ -2916,6 +3269,8 @@ snapshots:
is-negative-zero@2.0.3: {}
is-network-error@1.1.0: {}
is-number-object@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -2923,6 +3278,8 @@ snapshots:
is-number@7.0.0: {}
is-plain-obj@4.1.0: {}
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -2936,6 +3293,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-stream@4.0.1: {}
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -2951,6 +3310,8 @@ snapshots:
dependencies:
which-typed-array: 1.1.19
is-unicode-supported@2.1.0: {}
is-weakmap@2.0.2: {}
is-weakref@1.1.1:
@@ -2982,6 +3343,18 @@ snapshots:
json-buffer@3.0.1: {}
json-schema-to-typescript@15.0.4:
dependencies:
'@apidevtools/json-schema-ref-parser': 11.9.3
'@types/json-schema': 7.0.15
'@types/lodash': 4.17.20
is-glob: 4.0.3
js-yaml: 4.1.0
lodash: 4.17.21
minimist: 1.2.8
prettier: 3.6.2
tinyglobby: 0.2.14
json-schema-traverse@0.4.1: {}
json-stable-stringify-without-jsonify@1.0.1: {}
@@ -3040,6 +3413,8 @@ snapshots:
yallist: 4.0.0
optional: true
luxon@3.7.1: {}
make-fetch-happen@9.1.0:
dependencies:
agentkeepalive: 4.6.0
@@ -3151,6 +3526,8 @@ snapshots:
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {}
node-fetch@2.7.0(encoding@0.1.13):
dependencies:
whatwg-url: 5.0.0
@@ -3179,6 +3556,11 @@ snapshots:
abbrev: 1.1.1
optional: true
npm-run-path@6.0.0:
dependencies:
path-key: 4.0.0
unicorn-magic: 0.3.0
npmlog@6.0.2:
dependencies:
are-we-there-yet: 3.0.1
@@ -3191,6 +3573,11 @@ snapshots:
object-inspect@1.13.4: {}
object-is@1.1.6:
dependencies:
call-bind: 1.0.8
define-properties: 1.2.1
object-keys@1.1.1: {}
object.assign@4.1.7:
@@ -3259,10 +3646,25 @@ snapshots:
aggregate-error: 3.1.0
optional: true
p-queue@8.1.0:
dependencies:
eventemitter3: 5.0.1
p-timeout: 6.1.4
p-retry@6.2.1:
dependencies:
'@types/retry': 0.12.2
is-network-error: 1.1.0
retry: 0.13.1
p-timeout@6.1.4: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
parse-ms@4.0.0: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1:
@@ -3270,6 +3672,8 @@ snapshots:
path-key@3.1.1: {}
path-key@4.0.0: {}
path-parse@1.0.7: {}
pg-cloudflare@1.2.7:
@@ -3311,6 +3715,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.3: {}
possible-typed-array-names@1.1.0: {}
postgres-array@2.0.0: {}
@@ -3346,6 +3752,10 @@ snapshots:
prettier@3.6.2: {}
pretty-ms@9.2.0:
dependencies:
parse-ms: 4.0.0
promise-inflight@1.0.1:
optional: true
@@ -3414,6 +3824,8 @@ snapshots:
retry@0.12.0:
optional: true
retry@0.13.1: {}
reusify@1.1.0: {}
rfc4648@1.5.4: {}
@@ -3517,6 +3929,8 @@ snapshots:
signal-exit@3.0.7:
optional: true
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
@@ -3625,6 +4039,8 @@ snapshots:
strip-bom@3.0.0: {}
strip-final-newline@4.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {}
@@ -3687,6 +4103,11 @@ snapshots:
tildify@2.0.0: {}
tinyglobby@0.2.14:
dependencies:
fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -3765,8 +4186,12 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
undici-types@5.26.5: {}
undici-types@6.21.0: {}
unicorn-magic@0.3.0: {}
unique-filename@1.1.1:
dependencies:
unique-slug: 2.0.2
@@ -3783,6 +4208,8 @@ snapshots:
util-deprecate@1.0.2: {}
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
@@ -3854,4 +4281,6 @@ snapshots:
yocto-queue@0.1.0: {}
yoctocolors@2.1.1: {}
zod@4.0.14: {}

9
pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "homelab-operator"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"kubediagrams>=0.5.0",
]

View File

@@ -1,24 +0,0 @@
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import YAML from 'yaml';
import openapiTS, { astToString } from 'openapi-typescript';
const schemaRequest = await fetch('https://authentik.olsen.cloud/api/v3/schema/');
if (!schemaRequest.ok) {
console.error(schemaRequest.status, schemaRequest.statusText);
throw new Error('Failed to fetch schema');
}
const schemaYaml = await schemaRequest.text();
const schema = YAML.parse(schemaYaml);
const ast = await openapiTS(schema);
const contents = astToString(ast);
const targetLocation = resolve(import.meta.dirname, '..', 'src', 'clients', 'authentik', 'authentik.types.d.ts');
await mkdir(dirname(targetLocation), { recursive: true });
fs.writeFileSync(
targetLocation,
['// This file is generated by scripts/create-clients.ts', '/* eslint-disable */', contents].join('\n'),
);

View File

@@ -1,2 +0,0 @@
kubectl delete -f "$1"
kubectl apply -f "$1"

49
scripts/update-manifests.ts Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { compile } from 'json-schema-to-typescript';
import { K8sService } from '../src/services/k8s/k8s.ts';
import { Services } from '../src/utils/service.ts';
const services = new Services();
const k8s = services.get(K8sService);
const manifests = await k8s.extensionsApi.listCustomResourceDefinition();
const root = join(import.meta.dirname, '..', 'src', '__generated__', 'resources');
await mkdir(root, { recursive: true });
const firstUpsercase = (input: string) => {
const [first, ...rest] = input.split('');
return [first.toUpperCase(), ...rest].join('');
};
for (const manifest of manifests.items) {
for (const version of manifest.spec.versions) {
try {
const schema = version.schema?.openAPIV3Schema;
if (!schema) {
continue;
}
const cleanedSchema = JSON.parse(JSON.stringify(schema));
const kind = manifest.spec.names.kind;
const typeName = `K8S${kind}${firstUpsercase(version.name)}`;
const jsonLocation = join(root, `${typeName}.json`);
await writeFile(jsonLocation, JSON.stringify(schema, null, 2));
const file = await compile(cleanedSchema, typeName, {
declareExternallyReferenced: true,
additionalProperties: false,
$refOptions: {
continueOnError: true,
},
});
const fileLocation = join(root, `${typeName}.ts`);
await writeFile(fileLocation, file, 'utf8');
} catch (err) {
console.error(err);
console.error(`${manifest.metadata?.name} ${version.name} failed`);
}
}
}

25
skaffold.yaml Normal file
View File

@@ -0,0 +1,25 @@
apiVersion: skaffold/v4beta7
kind: Config
metadata:
name: homelab-operator
build:
cluster: {}
artifacts:
- image: homelaboperator
context: .
docker:
dockerfile: Dockerfile
manifests:
helm:
releases:
- name: homelab-operator
chartPath: charts/operator
setValueTemplates:
image.repository: '{{.IMAGE_REPO_homelaboperator}}'
image.tag: '{{.IMAGE_TAG_homelaboperator}}'
deploy:
# Use kubectl to apply the manifests.
kubectl: {}

View File

@@ -0,0 +1,31 @@
{
"description": "Addon is used to track application of a manifest file on disk. It mostly exists so that the wrangler DesiredSet\nApply controller has an object to track as the owner, and ensure that all created resources are tracked when the\nmanifest is modified or removed.",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"type": "object"
},
"spec": {
"description": "Spec provides information about the on-disk manifest backing this resource.",
"properties": {
"checksum": {
"description": "Checksum is the SHA256 checksum of the most recently successfully applied manifest file.",
"type": "string"
},
"source": {
"description": "Source is the Path on disk to the manifest file that this Addon tracks.",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}

View File

@@ -0,0 +1,43 @@
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
/**
* Addon is used to track application of a manifest file on disk. It mostly exists so that the wrangler DesiredSet
* Apply controller has an object to track as the owner, and ensure that all created resources are tracked when the
* manifest is modified or removed.
*/
export interface K8SAddonV1 {
/**
* APIVersion defines the versioned schema of this representation of an object.
* Servers should convert recognized schemas to the latest internal value, and
* may reject unrecognized values.
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
*/
apiVersion?: string;
/**
* Kind is a string value representing the REST resource this object represents.
* Servers may infer this from the endpoint the client submits requests to.
* Cannot be updated.
* In CamelCase.
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
*/
kind?: string;
metadata?: {};
/**
* Spec provides information about the on-disk manifest backing this resource.
*/
spec?: {
/**
* Checksum is the SHA256 checksum of the most recently successfully applied manifest file.
*/
checksum?: string;
/**
* Source is the Path on disk to the manifest file that this Addon tracks.
*/
source?: string;
};
}

View File

@@ -0,0 +1,376 @@
{
"description": "AppProject provides a logical grouping of applications, providing controls for:\n* where the apps may deploy to (cluster whitelist)\n* what may be deployed (repository whitelist, resource whitelist/blacklist)\n* who can access these applications (roles, OIDC group claims bindings)\n* and what they can do (RBAC policies)\n* automation access to these roles (JWT tokens)",
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"type": "object"
},
"spec": {
"description": "AppProjectSpec is the specification of an AppProject",
"properties": {
"clusterResourceBlacklist": {
"description": "ClusterResourceBlacklist contains list of blacklisted cluster level resources",
"items": {
"description": "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying\nconcepts during lookup stages without having partially valid types",
"type": "object",
"required": [
"group",
"kind"
],
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
}
}
},
"type": "array"
},
"clusterResourceWhitelist": {
"description": "ClusterResourceWhitelist contains list of whitelisted cluster level resources",
"items": {
"description": "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying\nconcepts during lookup stages without having partially valid types",
"type": "object",
"required": [
"group",
"kind"
],
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
}
}
},
"type": "array"
},
"description": {
"description": "Description contains optional project description",
"type": "string"
},
"destinationServiceAccounts": {
"description": "DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.",
"items": {
"description": "ApplicationDestinationServiceAccount holds information about the service account to be impersonated for the application sync operation.",
"type": "object",
"required": [
"defaultServiceAccount",
"server"
],
"properties": {
"defaultServiceAccount": {
"description": "DefaultServiceAccount to be used for impersonation during the sync operation",
"type": "string"
},
"namespace": {
"description": "Namespace specifies the target namespace for the application's resources.",
"type": "string"
},
"server": {
"description": "Server specifies the URL of the target cluster's Kubernetes control plane API.",
"type": "string"
}
}
},
"type": "array"
},
"destinations": {
"description": "Destinations contains list of destinations available for deployment",
"items": {
"description": "ApplicationDestination holds information about the application's destination",
"type": "object",
"properties": {
"name": {
"description": "Name is an alternate way of specifying the target cluster by its symbolic name. This must be set if Server is not set.",
"type": "string"
},
"namespace": {
"description": "Namespace specifies the target namespace for the application's resources.\nThe namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace",
"type": "string"
},
"server": {
"description": "Server specifies the URL of the target cluster's Kubernetes control plane API. This must be set if Name is not set.",
"type": "string"
}
}
},
"type": "array"
},
"namespaceResourceBlacklist": {
"description": "NamespaceResourceBlacklist contains list of blacklisted namespace level resources",
"items": {
"description": "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying\nconcepts during lookup stages without having partially valid types",
"type": "object",
"required": [
"group",
"kind"
],
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
}
}
},
"type": "array"
},
"namespaceResourceWhitelist": {
"description": "NamespaceResourceWhitelist contains list of whitelisted namespace level resources",
"items": {
"description": "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying\nconcepts during lookup stages without having partially valid types",
"type": "object",
"required": [
"group",
"kind"
],
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
}
}
},
"type": "array"
},
"orphanedResources": {
"description": "OrphanedResources specifies if controller should monitor orphaned resources of apps in this project",
"properties": {
"ignore": {
"description": "Ignore contains a list of resources that are to be excluded from orphaned resources monitoring",
"items": {
"description": "OrphanedResourceKey is a reference to a resource to be ignored from",
"type": "object",
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"type": "array"
},
"warn": {
"description": "Warn indicates if warning condition should be created for apps which have orphaned resources",
"type": "boolean"
}
},
"type": "object"
},
"permitOnlyProjectScopedClusters": {
"description": "PermitOnlyProjectScopedClusters determines whether destinations can only reference clusters which are project-scoped",
"type": "boolean"
},
"roles": {
"description": "Roles are user defined RBAC roles associated with this project",
"items": {
"description": "ProjectRole represents a role that has access to a project",
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"description": "Description is a description of the role",
"type": "string"
},
"groups": {
"description": "Groups are a list of OIDC group claims bound to this role",
"type": "array",
"items": {
"type": "string"
}
},
"jwtTokens": {
"description": "JWTTokens are a list of generated JWT tokens bound to this role",
"type": "array",
"items": {
"description": "JWTToken holds the issuedAt and expiresAt values of a token",
"type": "object",
"required": [
"iat"
],
"properties": {
"exp": {
"type": "integer",
"format": "int64"
},
"iat": {
"type": "integer",
"format": "int64"
},
"id": {
"type": "string"
}
}
}
},
"name": {
"description": "Name is a name for this role",
"type": "string"
},
"policies": {
"description": "Policies Stores a list of casbin formatted strings that define access policies for the role in the project",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"type": "array"
},
"signatureKeys": {
"description": "SignatureKeys contains a list of PGP key IDs that commits in Git must be signed with in order to be allowed for sync",
"items": {
"description": "SignatureKey is the specification of a key required to verify commit signatures with",
"type": "object",
"required": [
"keyID"
],
"properties": {
"keyID": {
"description": "The ID of the key in hexadecimal notation",
"type": "string"
}
}
},
"type": "array"
},
"sourceNamespaces": {
"description": "SourceNamespaces defines the namespaces application resources are allowed to be created in",
"items": {
"type": "string"
},
"type": "array"
},
"sourceRepos": {
"description": "SourceRepos contains list of repository URLs which can be used for deployment",
"items": {
"type": "string"
},
"type": "array"
},
"syncWindows": {
"description": "SyncWindows controls when syncs can be run for apps in this project",
"items": {
"description": "SyncWindow contains the kind, time, duration and attributes that are used to assign the syncWindows to apps",
"type": "object",
"properties": {
"andOperator": {
"description": "UseAndOperator use AND operator for matching applications, namespaces and clusters instead of the default OR operator",
"type": "boolean"
},
"applications": {
"description": "Applications contains a list of applications that the window will apply to",
"type": "array",
"items": {
"type": "string"
}
},
"clusters": {
"description": "Clusters contains a list of clusters that the window will apply to",
"type": "array",
"items": {
"type": "string"
}
},
"duration": {
"description": "Duration is the amount of time the sync window will be open",
"type": "string"
},
"kind": {
"description": "Kind defines if the window allows or blocks syncs",
"type": "string"
},
"manualSync": {
"description": "ManualSync enables manual syncs when they would otherwise be blocked",
"type": "boolean"
},
"namespaces": {
"description": "Namespaces contains a list of namespaces that the window will apply to",
"type": "array",
"items": {
"type": "string"
}
},
"schedule": {
"description": "Schedule is the time the window will begin, specified in cron format",
"type": "string"
},
"timeZone": {
"description": "TimeZone of the sync that will be applied to the schedule",
"type": "string"
}
}
},
"type": "array"
}
},
"type": "object"
},
"status": {
"description": "AppProjectStatus contains status information for AppProject CRs",
"properties": {
"jwtTokensByRole": {
"additionalProperties": {
"description": "JWTTokens represents a list of JWT tokens",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"description": "JWTToken holds the issuedAt and expiresAt values of a token",
"type": "object",
"required": [
"iat"
],
"properties": {
"exp": {
"type": "integer",
"format": "int64"
},
"iat": {
"type": "integer",
"format": "int64"
},
"id": {
"type": "string"
}
}
}
}
}
},
"description": "JWTTokensByRole contains a list of JWT tokens issued for a given role",
"type": "object"
}
},
"type": "object"
}
},
"required": [
"metadata",
"spec"
],
"type": "object"
}

View File

@@ -0,0 +1,233 @@
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
/**
* AppProject provides a logical grouping of applications, providing controls for:
* * where the apps may deploy to (cluster whitelist)
* * what may be deployed (repository whitelist, resource whitelist/blacklist)
* * who can access these applications (roles, OIDC group claims bindings)
* * and what they can do (RBAC policies)
* * automation access to these roles (JWT tokens)
*/
export interface K8SAppProjectV1Alpha1 {
/**
* APIVersion defines the versioned schema of this representation of an object.
* Servers should convert recognized schemas to the latest internal value, and
* may reject unrecognized values.
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
*/
apiVersion?: string;
/**
* Kind is a string value representing the REST resource this object represents.
* Servers may infer this from the endpoint the client submits requests to.
* Cannot be updated.
* In CamelCase.
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
*/
kind?: string;
metadata: {};
/**
* AppProjectSpec is the specification of an AppProject
*/
spec: {
/**
* ClusterResourceBlacklist contains list of blacklisted cluster level resources
*/
clusterResourceBlacklist?: {
group: string;
kind: string;
}[];
/**
* ClusterResourceWhitelist contains list of whitelisted cluster level resources
*/
clusterResourceWhitelist?: {
group: string;
kind: string;
}[];
/**
* Description contains optional project description
*/
description?: string;
/**
* DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.
*/
destinationServiceAccounts?: {
/**
* DefaultServiceAccount to be used for impersonation during the sync operation
*/
defaultServiceAccount: string;
/**
* Namespace specifies the target namespace for the application's resources.
*/
namespace?: string;
/**
* Server specifies the URL of the target cluster's Kubernetes control plane API.
*/
server: string;
}[];
/**
* Destinations contains list of destinations available for deployment
*/
destinations?: {
/**
* Name is an alternate way of specifying the target cluster by its symbolic name. This must be set if Server is not set.
*/
name?: string;
/**
* Namespace specifies the target namespace for the application's resources.
* The namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace
*/
namespace?: string;
/**
* Server specifies the URL of the target cluster's Kubernetes control plane API. This must be set if Name is not set.
*/
server?: string;
}[];
/**
* NamespaceResourceBlacklist contains list of blacklisted namespace level resources
*/
namespaceResourceBlacklist?: {
group: string;
kind: string;
}[];
/**
* NamespaceResourceWhitelist contains list of whitelisted namespace level resources
*/
namespaceResourceWhitelist?: {
group: string;
kind: string;
}[];
/**
* OrphanedResources specifies if controller should monitor orphaned resources of apps in this project
*/
orphanedResources?: {
/**
* Ignore contains a list of resources that are to be excluded from orphaned resources monitoring
*/
ignore?: {
group?: string;
kind?: string;
name?: string;
}[];
/**
* Warn indicates if warning condition should be created for apps which have orphaned resources
*/
warn?: boolean;
};
/**
* PermitOnlyProjectScopedClusters determines whether destinations can only reference clusters which are project-scoped
*/
permitOnlyProjectScopedClusters?: boolean;
/**
* Roles are user defined RBAC roles associated with this project
*/
roles?: {
/**
* Description is a description of the role
*/
description?: string;
/**
* Groups are a list of OIDC group claims bound to this role
*/
groups?: string[];
/**
* JWTTokens are a list of generated JWT tokens bound to this role
*/
jwtTokens?: {
exp?: number;
iat: number;
id?: string;
}[];
/**
* Name is a name for this role
*/
name: string;
/**
* Policies Stores a list of casbin formatted strings that define access policies for the role in the project
*/
policies?: string[];
}[];
/**
* SignatureKeys contains a list of PGP key IDs that commits in Git must be signed with in order to be allowed for sync
*/
signatureKeys?: {
/**
* The ID of the key in hexadecimal notation
*/
keyID: string;
}[];
/**
* SourceNamespaces defines the namespaces application resources are allowed to be created in
*/
sourceNamespaces?: string[];
/**
* SourceRepos contains list of repository URLs which can be used for deployment
*/
sourceRepos?: string[];
/**
* SyncWindows controls when syncs can be run for apps in this project
*/
syncWindows?: {
/**
* UseAndOperator use AND operator for matching applications, namespaces and clusters instead of the default OR operator
*/
andOperator?: boolean;
/**
* Applications contains a list of applications that the window will apply to
*/
applications?: string[];
/**
* Clusters contains a list of clusters that the window will apply to
*/
clusters?: string[];
/**
* Duration is the amount of time the sync window will be open
*/
duration?: string;
/**
* Kind defines if the window allows or blocks syncs
*/
kind?: string;
/**
* ManualSync enables manual syncs when they would otherwise be blocked
*/
manualSync?: boolean;
/**
* Namespaces contains a list of namespaces that the window will apply to
*/
namespaces?: string[];
/**
* Schedule is the time the window will begin, specified in cron format
*/
schedule?: string;
/**
* TimeZone of the sync that will be applied to the schedule
*/
timeZone?: string;
}[];
};
/**
* AppProjectStatus contains status information for AppProject CRs
*/
status?: {
/**
* JWTTokensByRole contains a list of JWT tokens issued for a given role
*/
jwtTokensByRole?: {
/**
* JWTTokens represents a list of JWT tokens
*/
[k: string]: {
items?: {
exp?: number;
iat: number;
id?: string;
}[];
};
};
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More