35 Commits

Author SHA1 Message Date
9e1fefaa2f chore(changelog): update changelog for v0.4.0
All checks were successful
Build and upload Docker release image / upload-assets (release) Successful in 2m53s
2025-05-11 10:24:19 +00:00
a79eb29dab chore(version): bump to 0.4.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 10s
Build and upload Docker nightly image / build-and-push (push) Successful in 2m48s
2025-05-11 12:24:01 +02:00
5f28d5ca7a chore(changelog): update unreleased changelog 2025-05-11 10:23:58 +00:00
d57cc27e19 feat(server): add graceful shutdown handling
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / changelog-only (push) Successful in 8s
Auto Changelog & Release / release (push) Has been skipped
Build and upload Docker nightly image / build-and-push (push) Has been cancelled
- Introduce signal listeners for SIGINT and SIGTERM to handle shutdown
- Use AbortController to terminate the server gracefully
- Improve server reliability and resource cleanup during termination
2025-05-11 12:23:38 +02:00
05fcd4b0f8 chore(changelog): update changelog for v0.3.0
All checks were successful
Build and upload Docker release image / upload-assets (release) Successful in 2m44s
2025-05-11 09:17:10 +00:00
f7b55bb26c chore(version): bump to 0.3.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 11s
Build and upload Docker nightly image / build-and-push (push) Successful in 2m45s
2025-05-11 11:16:53 +02:00
0d26bf4cf8 chore(changelog): update unreleased changelog 2025-05-11 09:16:38 +00:00
3299419726 feat(logging): add debug logs for key validation and request handling
Some checks failed
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and upload Docker nightly image / build-and-push (push) Has been cancelled
- Add debug logs for invalid and valid API key masking
- Log unsupported content types in middleware
- Log forwarded request URLs and response statuses from LT server
2025-05-11 11:16:22 +02:00
79dfbcf053 feat(utils): add utility to mask API keys
- Introduces a function to mask API keys for improved security
- Masks null or short keys entirely with asterisks
- Partially masks longer keys, retaining the first five characters
2025-05-11 11:16:11 +02:00
73bf48d4d7 chore(changelog): update changelog for v0.2.1
All checks were successful
Build and upload Docker release image / upload-assets (release) Successful in 1m8s
2025-05-11 09:05:25 +00:00
72e81ddb0f chore(version): bump to 0.2.1
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 11s
Build and upload Docker nightly image / build-and-push (push) Successful in 1m9s
2025-05-11 11:05:07 +02:00
9b26840d0a chore(changelog): update changelog for v0.2.0 2025-05-11 09:00:48 +00:00
7cf391f417 chore(version): bump to 0.2.0
Some checks failed
Build and upload Docker nightly image / build-and-push (push) Successful in 2m34s
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Failing after 7s
2025-05-11 11:00:30 +02:00
828494c92a docs(readme): update Docker image size information
- Adjusts the reported size of the minimal Docker image from ~93 MB to ~166 MB.
- Reflects updated build size for accuracy and transparency.
2025-05-11 11:00:00 +02:00
b6763f7483 feat(handler): sanitize sensitive fields in form data
- Removes `username` and `apiKey` from form data to prevent errors
  from the LanguageTool server when these fields are present
- Updates test cases to reflect the new handling of form data bodies
2025-05-11 10:59:49 +02:00
4326a2d92c fix(auth): validate API key from POST body and handle content type
- Switch API key validation to use POST body parameters
- Add support for `application/x-www-form-urlencoded` content type
- Store parsed body in context state for further use
- Reject unsupported content types with 415 status
2025-05-11 10:59:39 +02:00
60dcc30c0d chore(dockerfile): switch base image to debian and update curl setup
- Replace Alpine with Debian Slim for the runtime environment
- Update curl installation process to use apt-get for compatibility
2025-05-11 10:59:26 +02:00
36f2999cc9 chore(changelog): update unreleased changelog
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 7s
Build and upload Docker nightly image / build-and-push (push) Successful in 2m24s
2025-05-11 07:04:45 +00:00
7ea8e26660 chore(readme): update image repository URL
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and upload Docker nightly image / build-and-push (push) Successful in 45s
- Updates the Docker image repository URL for the proxy service
- Chagnes some text in the README file
2025-05-11 09:04:03 +02:00
5ba2ea1233 chore(changelog): update unreleased changelog 2025-05-10 23:34:40 +00:00
573fcf0e65 docs(readme): enhance documentation with usage and features
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Successful in 8s
Auto Changelog & Release / release (push) Has been skipped
Build and upload Docker nightly image / build-and-push (push) Successful in 45s
- Add detailed description of the proxy's purpose and features
- Include usage instructions for Docker and Docker Compose
- Document environment variables and file structure
- Provide licensing information and relevant links
2025-05-11 01:34:24 +02:00
0dc8764cc5 chore(changelog): update unreleased changelog 2025-05-10 23:25:07 +00:00
787bcdc1a2 chore(workflows): rename build-docker.yml to build-nightly.yml
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 4s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and upload Docker nightly image / build-and-push (push) Successful in 46s
- Updates workflow file name to reflect nightly build process
- Ensures better alignment with intended functionality
2025-05-11 01:24:52 +02:00
19088219cb chore(changelog): update unreleased changelog 2025-05-10 23:24:13 +00:00
ec18f7b4e3 refactor(dockerfile): update runtime base image to alpine:latest
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 8s
Build and upload Docker nightly image / build-and-push (push) Successful in 47s
- Switches the runtime base image from denoland/deno:alpine to alpine:latest
  for a more minimal and customizable environment.
- Adds curl installation for optional health checks in containers.
2025-05-11 01:23:57 +02:00
0d872a5acc chore(changelog): update changelog for v0.1.1
All checks were successful
Build and upload Docker release image / upload-assets (release) Successful in 50s
2025-05-10 23:17:09 +00:00
a80ad6927e chore(version): bump version to 0.1.1
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 10s
Build and upload Docker nightly image / build-and-push (push) Successful in 47s
2025-05-11 01:16:51 +02:00
f931e876f9 chore(workflows): update Docker image repository path
- Change Docker image repository from `simdev` to `maxp`
- Aligns image labels with the new repository location
2025-05-11 01:16:50 +02:00
ab66cbb893 chore(changelog): update changelog for v0.1.0
All checks were successful
Build and upload Docker release image / upload-assets (release) Successful in 2m10s
2025-05-10 23:13:34 +00:00
0227c83057 feat(docker): add multi-stage build for Deno application
Some checks failed
Build and upload Docker nightly image / build-and-push (push) Failing after 14s
Auto Changelog & Release / detect-version-change (push) Successful in 5s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 2m5s
- Introduces a multi-stage Dockerfile to build and run a Deno app
- Uses a builder stage to compile the app into a self-contained binary
- Adds minimal runtime stage with environment variable configuration
- Improves container efficiency and reduces runtime dependencies
2025-05-11 01:11:05 +02:00
f9714cbb53 feat(proxy): add environment config and request handling
- Introduce environment-based configuration for proxy settings
- Add middleware for API key authentication
- Implement request forwarding to LanguageTool backend
- Set up server startup and routing logic
2025-05-11 01:10:54 +02:00
52ce172ef5 test(auth): add unit tests for ltProxyAuth and ltProxyHandler
- Add tests to validate ltProxyAuth behavior for valid, invalid, and missing API keys
- Add tests to verify ltProxyHandler correctly proxies requests and returns responses
2025-05-11 01:10:39 +02:00
bcb22f983e feat(vscode): customize activity bar and peacock colors
- Adds color customizations for the VS Code activity bar to improve UI
  appearance and consistency.
- Sets Peacock extension color for better workspace identification.
2025-05-11 01:10:21 +02:00
b6e0947c3c feat(ci): add automated workflows for releases and Docker builds
- Introduce auto-changelog and release workflow for version management
- Add Docker workflows for nightly and release image builds
- Include scripts for release ID retrieval and asset uploads
- Document release process and best practices in `.gitea` directory
2025-05-11 01:10:10 +02:00
b473b7cce1 chore(repo): initialize project settings and configuration
- Add .gitignore to exclude sensitive and unnecessary files
- Update LICENSE with correct copyright attribution
- Add VERSION file with initial version number
- Configure git-cliff for changelog generation
- Add deno.jsonc with tasks, compiler options, and formatting rules
- Add deno.lock to track dependencies
- Add import_map.json for module resolution
2025-05-11 01:09:51 +02:00
25 changed files with 1405 additions and 2 deletions

50
.gitea/COMMIT_GPT.md Normal file
View File

@@ -0,0 +1,50 @@
# GPT Prompt
This is a prompt for a GPT model to generate a commit message based on the changes made in the code.
```plaintext
# System
You are a highly precise Git commit message generator for changelog-driven repositories.
Your only task is to read the provided multi-file git diff and generate **one and only one** Conventional Commit message that strictly complies with the rules below.
# Language
You MUST write the entire message in English only.
# Commit Rules
1. Use exactly one of the following lowercase types:
feat, fix, docs, style, refactor, perf, test, chore, revert
2. Optionally add a `(<scope>)` after the type:
• Use a short lowercase identifier (e.g., directory, module, file group)
• Omit if no dominant scope exists
3. Follow this structure:
`type(scope): summary`
• The summary must be in imperative mood
• Max 72 characters
• No trailing period
• Must not contain emojis or casing deviations
4. If the body is needed:
• Add exactly one blank line after the subject
• Use `- ` bullet points
• Each line must be ≤ 72 characters
• Explain what was changed and (if shown in the diff) why
5. Do NOT include:
• `BREAKING CHANGE:` or any `!` markers
• PR numbers, issue IDs, author names, ticket references
• Markdown formatting, emojis, or code blocks
6. The following `chore(...)` patterns MUST be ignored and skipped from changelogs:
chore(changelog), chore(version), chore(release): prepare for, chore(deps), chore(pr), chore(pull)
7. If the commit body contains the word `security`, it will be classified as security-relevant (🛡️), regardless of type.
8. Output MUST consist only of the final commit message, as plain text.
No extra prose, no explanation, no formatting, no examples.
# Now, read the diff and generate the message.
Output only the valid Conventional Commit message, exactly.
```

198
.gitea/HOWTO_RELEASE.md Normal file
View File

@@ -0,0 +1,198 @@
# 📦 HOWTO: Release erstellen mit Auto-Changelog-Workflow
Dieses Repository nutzt einen automatisierten CI/CD-Workflow zur **Versionsverwaltung, Changelog-Generierung und Release-Erstellung**.
Der gesamte Prozess ist deklarativ und läuft automatisch – ausgelöst durch Änderungen an einer Datei: `VERSION`.
---
## 🧭 Was passiert automatisch?
Sobald Änderungen in `main` landen, prüft der Workflow:
- 🔍 **Hat sich die Datei `VERSION` geändert?**
-**Nein** → es wird nur das `CHANGELOG.md` aktualisiert (unreleased Abschnitt)
-**Ja** → es wird:
- ein vollständiger Changelog für diese Version erzeugt
- ein Git-Tag `vX.Y.Z` erstellt
- ein Release in Gitea veröffentlicht (inkl. Beschreibung aus dem Changelog)
---
## ✅ Wie erzeuge ich ein Release?
### 1. Erhöhe die Version in der Datei `VERSION`
Beispiel:
```txt
1.2.3
```
> Diese Datei muss **als eigene Commit-Änderung** erfolgen – idealerweise als letzter Commit in einem PR.
> Die Commit-Nachricht sollte mit `chore(version)` beginnen, damit dieser nicht im Changelog auftaucht.
---
### 2. Mergen in `main`
Sobald `main` den Commit mit neuer `VERSION` enthält, wird automatisch:
- das `CHANGELOG.md` regeneriert und committed
- der neue Git-Tag erstellt (`v1.2.3`)
- ein Gitea Release mit genau diesem Changelog erzeugt
---
## 🛡️ Hinweis zu Tokens & Webhooks
Damit das Release auch korrekt weitere Workflows auslösen kann (z. B. über `on: release`), ist **ein Personal Access Token notwendig**.
### 🔐 Secret: `RELEASE_PUBLISH_TOKEN`
> Lege ein Repository-Secret mit diesem Namen an.
> Es sollte ein **Gitea Personal Access Token** mit folgenden Berechtigungen sein:
- `write:repo`
- `write:release`
- idealerweise: keine Ablaufzeit
Wird dieser Token **nicht** gesetzt, fällt der Workflow auf `ACTIONS_RUNTIME_TOKEN` zurück, aber:
- Release wird trotzdem erstellt
- **⚠️ andere Workflows, die auf `release.published` reagieren, könnten nicht getriggert werden**
---
## 🧪 Debugging-Tipps
- Stelle sicher, dass `VERSION` exakt **eine gültige neue semver-Version** enthält
- Achte auf den Commit-Log: Changelog-Commits sind mit `chore(changelog):` gekennzeichnet
- Verwende nur `main` als Trigger-Zweig
---
## 🧩 Erweiterung
In `upload-assets.yml` kannst du beliebige Build-Artefakte automatisch an das Release anhängen, sobald es veröffentlicht ist.
Dafür:
- liegt das Script `.gitea/scripts/get-release-id.sh`
- sowie `.gitea/scripts/upload-asset.sh` bereit
Mehr dazu in der Datei: `.gitea/workflows/upload-assets.yml`
---
## 🧘 Best Practice
- Changelog-Generierung nie manuell ausführen
- Nur `VERSION` ändern, um ein neues Release auszulösen
- Auf `CHANGELOG.md` nie direkt committen
- Release-Daten niemals per Hand in Gitea pflegen
📎 Alles wird versioniert, automatisiert und reproduzierbar erzeugt.
---
## 🧠 Commit-Gruppierung & Changelog-Erzeugung
Der Changelog wird auf Basis definierter **Commit-Gruppen** erzeugt.
Diese Regeln sind in `cliff.toml` unter `commit_parsers` konfiguriert.
| Prefix / Muster | Gruppe | Beschreibung |
|-------------------------------|---------------------------|--------------------------------------------------|
| `feat:` | 🚀 Features | Neue Funktionalität |
| `fix:` | 🐛 Bug Fixes | Fehlerbehebungen |
| `doc:` | 📚 Documentation | Änderungen an Doku, Readmes etc. |
| `perf:` | ⚡ Performance | Leistungsverbesserungen |
| `refactor:` | 🚜 Refactor | Reorganisation ohne Verhaltensänderung |
| `style:` | 🎨 Styling | Formatierung, Whitespaces, Code-Style |
| `test:` | 🧪 Testing | Neue oder angepasste Tests |
| `ci:` oder `chore:` (ohne Spezifizierung) | ⚙️ Miscellaneous Tasks | CI-Änderungen, Aufgaben, Wartung etc. |
| `chore(changelog)`, `chore(version)`, `chore(release): prepare for`, `chore(deps...)`, `chore(pr)`, `chore(pull)` | *(ignoriert)* | Diese Commits werden im Changelog **ausgelassen** |
| Commit-Body enthält `security` | 🛡️ Security | Sicherheitsrelevante Änderungen |
| `revert:` | ◀️ Revert | Rückgängig gemachte Commits |
| alles andere | 💼 Other | Fallback für nicht erkannte Formate |
### ✍️ Beispiel:
```bash
git commit -m "feat: add login endpoint"
git commit -m "fix: prevent crash on null input"
git commit -m "chore(version): bump to 1.2.3"
```
> Nur die ersten beiden erscheinen im Changelog – der dritte wird **automatisch übersprungen**.
---
## 🧾 Umgang mit `CHANGELOG.md` beim Mergen und Releasen
Wenn du automatisiert einen Changelog mit `git-cliff` erzeugst, ist `CHANGELOG.md` ein **generiertes Artefakt** – und kein handgepflegter Quelltext.
Beim Mergen von Feature-Branches in `main` kann es deshalb zu **unnötigen Konflikten** in dieser Datei kommen, obwohl der Inhalt später sowieso neu erzeugt wird.
---
## 🧼 Umgang mit `CHANGELOG.md` in Feature-Branches
Wenn du mit **Feature-Branches** arbeitest, wird `CHANGELOG.md` dort oft automatisch erzeugt.
Das kann beim späteren Merge in `main` zu **unnötigen Merge-Konflikten** führen.
### ✅ Empfohlene Vorgehensweise
**Bevor du den Branch mit `main` zusammenführst** (Merge oder Cherry-Pick):
```bash
git rm CHANGELOG.md
git commit -m "chore(changelog): remove generated CHANGELOG.md before merge"
git push
```
Dadurch:
* verhinderst du Merge-Konflikte mit `CHANGELOG.md`
* wird die Datei bei Feature-Branches nicht mehr automatisch erzeugt
* bleibt deine Historie sauber und konfliktfrei
> 💡 Der Workflow erzeugt `CHANGELOG.md` automatisch **nur**, wenn:
>
> * die Datei schon vorhanden ist **oder**
> * der Branch `main` heißt
---
## 🧩 Merge-Konflikte verhindern mit `.gitattributes`
Damit Git bei Konflikten in `CHANGELOG.md` **automatisch deine Version bevorzugt**, kannst du folgende Zeile in die Datei `.gitattributes` aufnehmen:
```gitattributes
CHANGELOG.md merge=ours
```
Das bedeutet:
* Beim Merge wird die Version aus dem aktuellen Branch (`ours`) behalten
* Änderungen aus dem Ziel-Branch (`theirs`) werden verworfen
### ✅ So verwendest du es richtig:
1. **Füge die Regel in `main` hinzu**:
```bash
echo "CHANGELOG.md merge=ours" >> .gitattributes
git add .gitattributes
git commit -m "chore(git): prevent merge conflicts in CHANGELOG.md"
git push origin main
```
2. **Hole sie in deinen Feature-Branch**:
```bash
git checkout feature/xyz
git rebase origin/main
```
3. **Ab sofort werden Konflikte in `CHANGELOG.md` automatisch aufgelöst** – lokal.
> ⚠️ Hinweis: Plattformen wie **Gitea, GitHub oder GitLab ignorieren `.gitattributes` beim Merge über die Web-Oberfläche**.
> Führe Merges daher **lokal** durch, wenn du Konflikte verhindern willst.

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingaben
TAG="$1"
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
RESPONSE=$(curl -sf \
-H "Authorization: token $TOKEN" \
"$API/repos/$OWNER/$NAME/releases/tags/$TAG")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Release-ID für $TAG ist: $RELEASE_ID"
# Für GitHub Actions als Umgebungsvariable
echo "GT_RELEASE_ID=$RELEASE_ID" >> "$GITHUB_ENV"

40
.gitea/scripts/upload-asset.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingabeparameter
FILE_PATH="$1" # z. B. ./dist/build.zip
CUSTOM_NAME="${2:-}" # optional: anderer Name unter dem das Asset gespeichert werden soll
RELEASE_ID="${GT_RELEASE_ID:-}" # aus Umgebung
# Validierung
if [[ -z "$RELEASE_ID" ]]; then
echo "❌ RELEASE_ID ist nicht gesetzt. Abbruch."
exit 1
fi
if [[ ! -f "$FILE_PATH" ]]; then
echo "❌ Datei '$FILE_PATH' existiert nicht. Abbruch."
exit 1
fi
# Default-Konfiguration
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
# Owner/Repo auflösen
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
# Dateiname setzen
FILENAME="${CUSTOM_NAME:-$(basename "$FILE_PATH")}"
echo "🔼 Uploading '$FILE_PATH' as '$FILENAME' to release ID $RELEASE_ID"
# Upload
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$FILE_PATH" \
"$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets?name=$FILENAME"
echo "✅ Upload abgeschlossen: $FILENAME"

View File

@@ -0,0 +1,59 @@
name: Build and upload Docker nightly image
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- 'CHANGELOG.md'
jobs:
build-and-push:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Restore Docker cache
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: buildx-general
paths: |
/tmp/.buildx-cache
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to Gitea Docker Registry
env:
REGISTRY: git.0xmax42.io
USERNAME: ${{ secrets.PACKAGE_USER }}
PASSWORD: ${{ secrets.PACKAGE_TOKEN }}
run: |
echo "$PASSWORD" | docker login $REGISTRY --username "$USERNAME" --password-stdin
- name: Build Docker Image
run: |
CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
VERSION_TAG=nightly-$(date +%Y%m%d)
docker buildx build \
--tag git.0xmax42.io/maxp/lt-auth-proxy:nightly \
--platform linux/amd64,linux/arm64 \
--label org.opencontainers.image.description="Lightweight LanguageTool Auth Proxy" \
--label org.opencontainers.image.documentation="https://git.0xmax42.io/maxp/lt-auth-proxy" \
--label org.opencontainers.image.authors="0xMax42 <mail@0xmax42.io>" \
--label org.opencontainers.image.created=$CREATED \
--label org.opencontainers.image.version=$VERSION_TAG \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache,mode=max \
--builder ${{ steps.buildx.outputs.name }} \
--push \
--progress=plain \
.

View File

@@ -0,0 +1,78 @@
# ========================
# 📦 Upload Assets Template
# ========================
# Dieser Workflow wird automatisch ausgelöst, wenn ein Release
# in Gitea veröffentlicht wurde (event: release.published).
#
# Er dient dem Zweck, Release-Artefakte (wie z. B. Binary-Dateien,
# Changelogs oder Build-Zips) nachträglich mit dem Release zu verknüpfen.
#
# Voraussetzung: Zwei Shell-Skripte liegen im Projekt:
# - .gitea/scripts/get-release-id.sh → ermittelt Release-ID per Tag
# - .gitea/scripts/upload-asset.sh → lädt Datei als Release-Asset hoch
#
# --------------------------------------
name: Build and upload Docker release image
on:
release:
types: [published] # Nur bei Veröffentlichung eines Releases (nicht bei Entwürfen)
jobs:
upload-assets:
runs-on: ubuntu-latest
env:
VERSION: ${{ github.event.release.tag_name }}
steps:
# 📥 Checke den Stand des Repos aus, exakt auf dem veröffentlichten Tag
# So ist garantiert, dass die Artefakte dem Zustand des Releases entsprechen.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }} # z. B. "v1.2.3"
- name: Strip "v" from tag
id: version
run: |
echo "VERSION_STRIPPED=${VERSION#v}" >> $GITHUB_OUTPUT
- name: Restore Docker cache
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: buildx-general
paths: |
/tmp/.buildx-cache
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
install: true
- name: Login to Gitea Docker Registry
env:
REGISTRY: git.0xmax42.io
USERNAME: ${{ secrets.PACKAGE_USER }}
PASSWORD: ${{ secrets.PACKAGE_TOKEN }}
run: |
echo "$PASSWORD" | docker login $REGISTRY --username "$USERNAME" --password-stdin
- name: Build Docker Image
run: |
docker buildx build \
--tag git.0xmax42.io/maxp/lt-auth-proxy:${{ steps.version.outputs.VERSION_STRIPPED }} \
--tag git.0xmax42.io/maxp/lt-auth-proxy:latest \
--label org.opencontainers.image.description="Lightweight LanguageTool Auth Proxy" \
--label org.opencontainers.image.documentation="https://git.0xmax42.io/maxp/lt-auth-proxy" \
--label org.opencontainers.image.authors="0xMax42 <mail@0xmax42.io>" \
--label org.opencontainers.image.version=${VERSION} \
--platform linux/amd64,linux/arm64 \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache,mode=max \
--builder ${{ steps.buildx.outputs.name }} \
--push \
--progress=plain \
.

View File

@@ -0,0 +1,222 @@
name: Auto Changelog & Release
on:
push:
branches:
- main
- '**'
jobs:
detect-version-change:
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.set.outputs.version_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if VERSION file changed
if: github.ref == 'refs/heads/main'
run: |
echo "🔍 Vergleich mit github.event.before:"
echo "Before: ${{ github.event.before }}"
echo "After: ${{ github.sha }}"
echo "📄 Changed files between before and after:"
git diff --name-only ${{ github.event.before }} ${{ github.sha }} || echo "(diff failed)"
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q '^VERSION$'; then
echo "✅ VERSION file was changed"
echo "VERSION_CHANGED=true" >> $GITHUB_ENV
else
echo "ℹ️ VERSION file not changed"
echo "VERSION_CHANGED=false" >> $GITHUB_ENV
fi
- name: Set output (always)
id: set
run: |
echo "version_changed=${VERSION_CHANGED:-false}" >> $GITHUB_OUTPUT
changelog-only:
needs: detect-version-change
if: github.ref != 'refs/heads/main' || needs.detect-version-change.outputs.version_changed == 'false'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Git Author
run: |
git config user.name "$CI_COMMIT_AUTHOR_NAME"
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
- name: Read CLIFF_VERSION from cliff.toml
id: cliff_version
run: |
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
- name: Restore git-cliff cache
id: restore-cliff
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
paths: |
/root/.cargo/bin
- name: Install git-cliff
if: steps.restore-cliff.outputs.cache-hit != 'true'
run: |
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
- name: Generate unreleased changelog (if file exists or on main)
run: |
if [[ -f CHANGELOG.md || "${GITHUB_REF##refs/heads/}" == "main" ]]; then
echo "Generating CHANGELOG.md..."
git-cliff -c cliff.toml -o CHANGELOG.md
else
echo "CHANGELOG.md does not exist and this is not 'main'. Skipping generation."
fi
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore(changelog): update unreleased changelog"
git push origin "${GITHUB_REF##refs/heads/}"
fi
release:
needs: detect-version-change
if: needs.detect-version-change.outputs.version_changed == 'true' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Git Author
run: |
git config user.name "$CI_COMMIT_AUTHOR_NAME"
git config user.email "$CI_COMMIT_AUTHOR_EMAIL"
- name: Read VERSION
id: version
run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Read CLIFF_VERSION from cliff.toml
id: cliff_version
run: |
echo "version=$(awk -F '=' '/^# CLIFF_VERSION=/ { gsub(/[" ]/, "", $2); print $2 }' cliff.toml)" >> $GITHUB_OUTPUT
- name: Restore git-cliff cache
id: restore-cliff
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: cargo-cliff-${{ steps.cliff_version.outputs.version }}
paths: |
/root/.cargo/bin
- name: Install git-cliff
if: steps.restore-cliff.outputs.cache-hit != 'true'
run: |
cargo install git-cliff --version "${{ steps.cliff_version.outputs.version }}" --features gitea
- name: Generate changelog for release and tag
id: generate-changelog
run: |
VERSION=${{ steps.version.outputs.value }}
git-cliff -c cliff.toml -t "v$VERSION" -o CHANGELOG.md
BODY=$(mktemp)
ESCAPED_VERSION=$(echo "$VERSION" | sed 's/\./\\./g')
awk -v ver="$ESCAPED_VERSION" '
$0 ~ "^## \\[" ver "\\]" {
print_flag=1
line = $0
sub(/^## /, "", line)
sub(/\\s*\\(.*\\)/, "", line) # entfernt z. B. "(...)" oder "(*)"
print line
next
}
$0 ~ "^## \\[" && $0 !~ "^## \\[" ver "\\]" {
print_flag=0
}
print_flag
' CHANGELOG.md > "$BODY"
echo "changelog_body_path=$BODY" >> $GITHUB_OUTPUT
- name: Commit updated CHANGELOG
run: |
git add CHANGELOG.md
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore(changelog): update changelog for v${{ steps.version.outputs.value }}"
git push origin main
fi
- name: Create Git tag (if not exists)
run: |
VERSION=${{ steps.version.outputs.value }}
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists, skipping tag creation."
else
git tag -a "v$VERSION" -F "${{ steps.generate-changelog.outputs.changelog_body_path }}" --cleanup=verbatim
git push origin "v$VERSION"
fi
- name: Create Gitea release
env:
RELEASE_PUBLISH_TOKEN: ${{ secrets.RELEASE_PUBLISH_TOKEN }}
run: |
VERSION=${{ steps.version.outputs.value }}
BODY_FILE="${{ steps.generate-changelog.outputs.changelog_body_path }}"
OWNER=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1)
REPO=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)
# Token-Auswahl
TOKEN="${RELEASE_PUBLISH_TOKEN:-$ACTIONS_RUNTIME_TOKEN}"
if [[ -z "${RELEASE_PUBLISH_TOKEN:-}" ]]; then
echo "::warning title=Limited Release Propagation::"
echo "RELEASE_PUBLISH_TOKEN is not set. Using ACTIONS_RUNTIME_TOKEN instead."
echo "⚠️ Release events may not trigger other workflows if created with the runtime token."
echo
fi
# Prüfe, ob der Release schon existiert
if curl -sf "$GITHUB_API_URL/repos/$OWNER/$REPO/releases/tags/v$VERSION" \
-H "Authorization: token $TOKEN" > /dev/null; then
echo "🔁 Release for tag v$VERSION already exists, skipping."
exit 0
fi
echo "🚀 Creating Gitea release for v$VERSION"
# Release-Beschreibung vorbereiten
RELEASE_BODY=$(tail -n +2 "$BODY_FILE" | jq -Rs .)
curl -X POST "$GITHUB_API_URL/repos/$OWNER/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"tag_name": "v$VERSION",
"target_commitish": "main",
"name": "Release v$VERSION",
"body": $RELEASE_BODY,
"draft": false,
"prerelease": false
}
EOF
echo "✅ Release for tag $VERSION created successfully."

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.env
coverage/
logs/
.locale/
cache/

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#6e95c4",
"activityBar.background": "#6e95c4",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#edd2de",
"activityBarBadge.foreground": "#15202b"
},
"peacock.color": "#4a7bb5"
}

66
CHANGELOG.md Normal file
View File

@@ -0,0 +1,66 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.4.0](https://git.0xmax42.io/maxp/lt-auth-proxy/compare/v0.3.0..v0.4.0) - 2025-05-11
### 🚀 Features
- *(server)* Add graceful shutdown handling - ([d57cc27](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/d57cc27e19e68c13ac08af223c3721a9c45fafd1))
## [0.3.0](https://git.0xmax42.io/maxp/lt-auth-proxy/compare/v0.2.1..v0.3.0) - 2025-05-11
### 🚀 Features
- *(logging)* Add debug logs for key validation and request handling - ([3299419](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/32994197261e9ab5a46df5f90f2faed89cd68558))
- *(utils)* Add utility to mask API keys - ([79dfbcf](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/79dfbcf053d613fe3fff63bfd24537a1665c9389))
## [0.2.1](https://git.0xmax42.io/maxp/lt-auth-proxy/compare/v0.1.1..v0.2.1) - 2025-05-11
### 🚀 Features
- *(handler)* Sanitize sensitive fields in form data - ([b6763f7](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/b6763f748325bf9c4129c5230c5e8101f93a2388))
### 🐛 Bug Fixes
- *(auth)* Validate API key from POST body and handle content type - ([4326a2d](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/4326a2d92cb789b8bbed95d9e72b5cbadbafd93a))
### 🚜 Refactor
- *(dockerfile)* Update runtime base image to alpine:latest - ([ec18f7b](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/ec18f7b4e39bbfa88ba23ef3fdf825917ac5303b))
### 📚 Documentation
- *(readme)* Update Docker image size information - ([828494c](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/828494c92a3d517a35f2feece48af5cd6116f1d4))
- *(readme)* Enhance documentation with usage and features - ([573fcf0](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/573fcf0e65b3446b7efdce6b1695722c9d757410))
### ⚙️ Miscellaneous Tasks
- *(dockerfile)* Switch base image to debian and update curl setup - ([60dcc30](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/60dcc30c0d9bad5dc9c0b1e4f79a4cb53330a965))
- *(readme)* Update image repository URL - ([7ea8e26](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/7ea8e26660b9c29ead8f5597647a96c27cc9dcb5))
- *(workflows)* Rename build-docker.yml to build-nightly.yml - ([787bcdc](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/787bcdc1a20e85699b810082895d2a461216c9cf))
## [0.1.1](https://git.0xmax42.io/maxp/lt-auth-proxy/compare/v0.1.0..v0.1.1) - 2025-05-10
### ⚙️ Miscellaneous Tasks
- *(workflows)* Update Docker image repository path - ([f931e87](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/f931e876f9d99c40a29f9aca8626af22b9f0772e))
## [0.1.0] - 2025-05-10
### 🚀 Features
- *(docker)* Add multi-stage build for Deno application - ([0227c83](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/0227c8305727a82e9d4f20c7e2245e8daf4f0cc5))
- *(proxy)* Add environment config and request handling - ([f9714cb](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/f9714cbb5378cbba5721df3c258114656bb607f4))
- *(vscode)* Customize activity bar and peacock colors - ([bcb22f9](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/bcb22f983e869e99299c62b431269826f1ae8ad3))
- *(ci)* Add automated workflows for releases and Docker builds - ([b6e0947](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/b6e0947c3c3572a553c67fc8c33d1f887d6788e1))
### 🧪 Testing
- *(auth)* Add unit tests for ltProxyAuth and ltProxyHandler - ([52ce172](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/52ce172ef55779e014e9c177ce540dbb96f5c677))
### ⚙️ Miscellaneous Tasks
- *(repo)* Initialize project settings and configuration - ([b473b7c](https://git.0xmax42.io/maxp/lt-auth-proxy/commit/b473b7cce15c2d6703dd9eb32aa384634e68103c))

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# -------- Stage 1: Build the Deno application --------
FROM denoland/deno:latest AS builder
# Set the working directory inside the builder container
WORKDIR /app
# Copy the application source code and configuration files
COPY src/ ./src/
COPY deno.jsonc ./deno.jsonc
COPY import_map.json ./import_map.json
# Compile the application to a self-contained native binary
# Permissions and flags should be specified during compile time
RUN deno task compile
# -------- Stage 2: Minimal runtime environment --------
FROM debian:bookworm-slim
# Optional: Install curl for container-level health checks
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Copy only the compiled application binary from the builder stage
COPY --from=builder /app/app /app/app
# Set the working directory for the runtime container
WORKDIR /app
# Define expected environment variables (can be overridden at runtime)
ENV PROXY_HOST=0.0.0.0
ENV PROXY_PORT=8011
ENV LT_SERVER_HOST=localhost
ENV LT_SERVER_PORT=8010
ENV API_KEYS=demo-key
# Define the default command to run the application binary
CMD ["/app/app"]

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 maxp Copyright (c) 2025 0xMax42
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including associated documentation files (the "Software"), to deal in the Software without restriction, including

View File

@@ -1,3 +1,82 @@
# lt-auth-proxy # lt-auth-proxy
Language Tool authentication proxy A lightweight, production-ready reverse proxy for [LanguageTool](https://languagetool.org) with API key authentication.
This service acts as a transparent gateway that verifies an `apiKey` before forwarding requests to a running LanguageTool server instance. It is fully self-contained, built in Deno, and distributed as a minimal multi-architecture Docker image.
---
## ✨ Features
* 🔐 **API key authentication** via query or form body
* 📡 **Transparent proxying** to any LanguageTool backend
* 🐳 **Minimal Docker image (\~166 MB)**
* 🧱 **Statically compiled** Deno binary
* 🧪 **Unit tested** middleware and proxy logic
* 🛠️ Compatible with regular LT clients
---
## 🚀 Usage
You can run the proxy via Docker:
```bash
docker run -p 8011:8011 \
-e API_KEYS="demo-key,another-key" \
-e LT_SERVER_HOST=lt-server \
-e LT_SERVER_PORT=8010 \
git.0xmax42.io/simdev/lt-auth-proxy:latest
```
---
## 🧪 Supported Environment Variables
| Variable | Required | Default | Description |
| ---------------- | -------- | ----------- | ---------------------------------------- |
| `API_KEYS` | ✅ yes | – | Comma-separated list of valid API tokens |
| `PROXY_HOST` | ❌ no | `0.0.0.0` | Host/IP address to bind the proxy to |
| `PROXY_PORT` | ❌ no | `8011` | Port the proxy listens on |
| `LT_SERVER_HOST` | ❌ no | `localhost` | Hostname of the LanguageTool backend |
| `LT_SERVER_PORT` | ❌ no | `8010` | Port of the LanguageTool backend |
---
## 📁 File Structure
```
src/
├── main.ts # Entry point, sets up the HTTP kernel
├── env.ts # Lazy-loaded environment access
├── ltProxyAuth.ts # Middleware to check the apiKey
├── ltProxyHandler.ts# Handler to forward the request to LT
```
---
## 🐳 Docker Compose Example
```yaml
services:
lt-server:
image: languagetool/languagetool:latest
ports:
- "8010:8010"
proxy:
image: git.0xmax42.io/maxp/lt-auth-proxy:latest
ports:
- "8011:8011"
environment:
- API_KEYS=demo-key
- LT_SERVER_HOST=lt-server
- LT_SERVER_PORT=8010
```
---
## 📖 License
MIT © 0xMax42
[https://git.0xmax42.io/maxp/lt-auth-proxy](https://git.0xmax42.io/maxp/lt-auth-proxy)

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.4.0

104
cliff.toml Normal file
View File

@@ -0,0 +1,104 @@
# CLIFF_VERSION=2.8.0
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[remote.gitea]
owner = "maxp"
repo = "lt-auth-proxy"
[changelog]
# postprocessors
postprocessors = [
{ pattern = '<GITEA_URL>', replace = "https://git.0xmax42.io" }, # replace gitea url
]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
<GITEA_URL>/{{ remote.gitea.owner }}/{{ remote.gitea.repo }}
{%- endmacro -%}
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]\
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing s
trim = true
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(version\\)", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Regex to select git tags that represent releases.
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

30
deno.jsonc Normal file
View File

@@ -0,0 +1,30 @@
{
"tasks": {
"start": "deno run --allow-net --allow-env --allow-import --env-file src/main.ts -- --verbose",
"watch": "deno run --watch --allow-net --allow-env --allow-import --env-file src/main.ts -- --verbose",
"test": "deno test --allow-net --allow-env --allow-import --coverage **/__tests__/*.test.ts",
"compile": "deno compile --allow-net --allow-env --import-map=import_map.json --allow-import --output=app src/main.ts"
},
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext",
"deno.ns"
],
"strict": true
},
"fmt": {
"useTabs": false,
"lineWidth": 80,
"indentWidth": 4,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve",
"include": [
"src/",
"main.ts"
]
},
"importMap": "./import_map.json"
}

69
deno.lock generated Normal file
View File

@@ -0,0 +1,69 @@
{
"version": "5",
"redirects": {
"https://git.0xmax42.io/maxp/HttpKernel/raw/branch/main/src/types/mod.ts": "https://git.0xmax42.io/maxp/http-kernel/raw/branch/main/src/types/mod.ts"
},
"remote": {
"https://deno.land/std@0.204.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9",
"https://deno.land/std@0.204.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48",
"https://deno.land/std@0.204.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
"https://deno.land/std@0.204.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
"https://deno.land/std@0.204.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c",
"https://deno.land/std@0.204.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9",
"https://deno.land/std@0.204.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227",
"https://deno.land/std@0.204.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7",
"https://deno.land/std@0.204.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6",
"https://deno.land/std@0.204.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63",
"https://deno.land/std@0.204.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c",
"https://deno.land/std@0.204.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c",
"https://deno.land/std@0.204.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b",
"https://deno.land/std@0.204.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4",
"https://deno.land/std@0.204.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848",
"https://deno.land/std@0.204.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b",
"https://deno.land/std@0.204.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754",
"https://deno.land/std@0.204.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22",
"https://deno.land/std@0.204.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0",
"https://deno.land/std@0.204.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad",
"https://deno.land/std@0.204.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54",
"https://deno.land/std@0.204.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057",
"https://deno.land/std@0.204.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265",
"https://deno.land/std@0.204.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c",
"https://deno.land/std@0.204.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd",
"https://deno.land/std@0.204.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
"https://deno.land/std@0.204.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece",
"https://deno.land/std@0.204.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278",
"https://deno.land/std@0.204.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085",
"https://deno.land/std@0.204.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a",
"https://deno.land/std@0.204.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536",
"https://deno.land/std@0.204.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/HttpKernel.ts": "f66aa1e66702bf4fedebf3fcda0efa4f560dacb97ac48520419820da48346a70",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IContext.ts": "6e67cd2a422d797442e70af4131ebcc5998d4e55fc2d4e6028999c0dd41d3e8a",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IHttpErrorHandlers.ts": "6ac7c1b52cdf6cdcc031269a00562d1294385ffe0cb9496cffab411c995e286f",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IHttpKernel.ts": "ed26ca3cdf096f0fa5d7294275a568006e41769b4ed6f387db5dc9a7ef57a4e3",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IHttpKernelConfig.ts": "ede49690832ecbd452b3c163099452b94c8d62284328dd4d2417fddd7c77373d",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IInternalRoute.ts": "c22d3da104f3f94e3c7224a30fe8ad42cd8a6a863bc48826c6dd81654bc9d41f",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IRouteBuilder.ts": "a52ab252996843a35f3887d20bd76774d26b63cec2507bcd79c7ac651c6247e6",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IRouteDefinition.ts": "7bb1a3d87cc20c93832836a7d9172897f3a7ab1c28b3cf9842ed13b0c5035329",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IRouteMatch.ts": "a3ddc6100f3469a36cf40e21ed25d220d5047d5fe04adcecc12e82bc9f29ca58",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/IRouteMatcher.ts": "90022166a2c114a2ccb501a72d921a7346b8c1cd722373100dcc1f78674fd69d",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Interfaces/mod.ts": "1403fd1447fa8acdb940135966449384488b99d88a42f372a0b3c78c74cbadd2",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/RouteBuilder.ts": "0140d3f2275d663f322266cb31c340f3749c87fc9b0e3a0a5e671d1ab3786906",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/DeepPartial.ts": "6fa5e7c94b40465476484d9f437d72e693db3745e88c6c20f64bda58673c5b8e",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/Handler.ts": "a0ef23c8295bdfafe0b7bb49479d517ce5ecce8f28c1e40872f64e764ae16bb4",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/HttpErrorHandler.ts": "b3920b9fd40e456fe53a46a7fc7752ae064731709edd23aafcc5b14257e6a305",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/HttpMethod.ts": "95bee5e29fdbf7a242f3f02dad62619f83640b3601754e33bf5a56b977d3d008",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/HttpStatusCode.ts": "b50e6bc79977f423e85610f4567ca16998705a8c58cbc56444d5099ef0e61b4a",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/Middleware.ts": "12e24cb313b051522ca66ff0e52b3e90bd7950fc4cce2077708533a9cc9ecfd6",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/Params.ts": "080af10c47086b18b26a1488d8d5f145650e0830e9f348f9bcca52fade57011f",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/Query.ts": "38fb74c6b1736cf342b00819b09d0fd2f49d814f703c1c8c73afadb2ff3e3e74",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/RegisterRoute.ts": "7e0f4ee07cef5022cd3929e161c5ccaa7dd4a265a1c563f755739a07a1fb3408",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/ResponseDecorator.ts": "9ed2f79f4a88526c33aac5ea00de546792e100df5b58865550477791e0c493ff",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/State.ts": "70b2e507a45752764d6fc6dd7e73dd3d20dab0ab862f36b7b3fcba5aa64755a0",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Types/mod.ts": "89ee878e08be35b72b13771ea661f10a438c9eaf5fc9e2f236273a5602908e84",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Utils/createEmptyContext.ts": "f8acd6e532fb01ccd0b634795c025cc19217cb3ca8962a6451b621ceb1d87298",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Utils/createRouteMatcher.ts": "62b023a6c95c8116cd6109a9927fa04888f86d094544ebb672d6f4bf77a8e441",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Utils/mod.ts": "975b7fa61ba679f75fbbac081b465311dae3a8df732d0a799e9a9a771fb29681",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/Utils/normalizeError.ts": "364a370f421f56b6a548a4dfdb1c4a6f4f907f7febb94222412073a147ef1f6e",
"https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/mod.ts": "7ad1d9d44dd10595dd2f6493589487dbd2f15b78c830be5b316268af214e30e4"
}
}

5
import_map.json Normal file
View File

@@ -0,0 +1,5 @@
{
"imports": {
"http-kernel/": "https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/"
}
}

View File

@@ -0,0 +1,88 @@
// deno-lint-ignore-file require-await
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import type { IContext } from 'http-kernel/Interfaces/mod.ts';
import { ltProxyAuth } from '../ltProxyAuth.ts';
Deno.test('ltProxyAuth: accepts valid API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const body = new URLSearchParams({ apiKey: 'valid123' });
const req = new Request('http://localhost/', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body,
});
const ctx: IContext = {
req,
params: {},
query: {},
state: {},
};
const response = await ltProxyAuth(
ctx,
async () => new Response('OK', { status: 200 }),
);
assertEquals(response.status, 200);
assertEquals(await response.text(), 'OK');
});
Deno.test('ltProxyAuth: rejects invalid API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const body = new URLSearchParams({ apiKey: 'invalid456' });
const req = new Request('http://localhost/', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body,
});
const ctx: IContext = {
req,
params: {},
query: {},
state: {},
};
const response = await ltProxyAuth(
ctx,
async () => new Response('SHOULD NOT HAPPEN'),
);
assertEquals(response.status, 403);
assertEquals(await response.text(), 'Forbidden – Invalid API key');
});
Deno.test('ltProxyAuth: rejects missing API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const body = new URLSearchParams({ text: 'nur text ohne apiKey' });
const req = new Request('http://localhost/', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body,
});
const ctx: IContext = {
req,
params: {},
query: {},
state: {},
};
const response = await ltProxyAuth(
ctx,
async () => new Response('SHOULD NOT HAPPEN'),
);
assertEquals(response.status, 403);
assertEquals(await response.text(), 'Forbidden – Invalid API key');
});

View File

@@ -0,0 +1,50 @@
// deno-lint-ignore-file require-await
import { ltProxyHandler } from '../ltProxyHandler.ts';
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import type { IContext } from 'http-kernel/Interfaces/mod.ts';
Deno.test('ltProxyHandler: proxies request and returns response', async () => {
const expectedResponse = new Response('Mocked LT Response', {
status: 200,
headers: {
'content-type': 'text/plain',
'x-mocked': 'true',
},
});
// Backup and mock global fetch
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => expectedResponse;
// Form body wie bei echtem Request
const formData = new URLSearchParams({ text: 'Hallo Welt' });
const bodyBytes = new TextEncoder().encode(formData.toString());
const req = new Request('http://localhost/v2/check?language=de-DE', {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: bodyBytes,
});
const ctx: IContext = {
req,
params: {},
query: {
language: 'de-DE',
},
state: {
body: bodyBytes,
},
};
const response = await ltProxyHandler(ctx);
assertEquals(response.status, 200);
assertEquals(await response.text(), 'Mocked LT Response');
assertEquals(response.headers.get('x-mocked'), 'true');
// Restore fetch
globalThis.fetch = originalFetch;
});

63
src/env.ts Normal file
View File

@@ -0,0 +1,63 @@
/**
* Environment configuration for lt-auth-proxy.
* All properties are lazily evaluated and cached on first access.
*/
export class Env {
private static _proxyHost?: string;
private static _proxyPort?: number;
private static _apiKeys?: string[];
private static _ltServerHost?: string;
private static _ltServerPort?: number;
private static getEnv(key: string, required = false): string | undefined {
const value = Deno.env.get(key);
if (required && (!value || value.trim() === '')) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
/** Hostname for the proxy (default: 0.0.0.0) */
static get proxyHost(): string {
if (this._proxyHost === undefined) {
this._proxyHost = this.getEnv('PROXY_HOST') || '0.0.0.0';
}
return this._proxyHost;
}
/** Port for the proxy (default: 8011) */
static get proxyPort(): number {
if (this._proxyPort === undefined) {
this._proxyPort = Number(this.getEnv('PROXY_PORT') || 8011);
}
return this._proxyPort;
}
/** List of allowed API keys (required) */
static get apiKeys(): string[] {
if (this._apiKeys === undefined) {
const raw = this.getEnv('API_KEYS', true)!;
this._apiKeys = raw.split(',').map((k) => k.trim()).filter((k) =>
k.length > 0
);
}
return this._apiKeys;
}
/** Hostname of the LanguageTool backend (default: localhost) */
static get ltServerHost(): string {
if (this._ltServerHost === undefined) {
this._ltServerHost = this.getEnv('LT_SERVER_HOST') || 'localhost';
}
return this._ltServerHost;
}
/** Port of the LanguageTool backend (default: 8010) */
static get ltServerPort(): number {
if (this._ltServerPort === undefined) {
this._ltServerPort = Number(this.getEnv('LT_SERVER_PORT') || 8010);
}
return this._ltServerPort;
}
}

33
src/ltProxyAuth.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Middleware } from 'http-kernel/Types/mod.ts';
import { Env } from './env.ts';
import { maskApiKey } from './utils.ts';
/**
* Middleware that checks for a valid API key via form param.
* Also stores the body in ctx.state.body for later use.
*/
export const authMiddleware: Middleware = async (ctx, next) => {
const contentType = ctx.req.headers.get('content-type') || '';
if (contentType.includes('application/x-www-form-urlencoded')) {
const bodyBuffer = await ctx.req.arrayBuffer();
ctx.state.body = new Uint8Array(bodyBuffer);
const text = new TextDecoder().decode(ctx.state.body as Uint8Array);
const params = new URLSearchParams(text);
const key = params.get('apiKey');
if (!key || !Env.apiKeys.includes(key)) {
console.debug('Invalid API key:', maskApiKey(key));
return new Response('Forbidden – Invalid API key', { status: 403 });
}
} else {
console.debug('Unsupported content type:', contentType);
return new Response('Unsupported content type', { status: 415 });
}
console.debug('Valid API key:', maskApiKey(ctx.req.headers.get('apiKey')));
return await next();
};
export { authMiddleware as ltProxyAuth };

57
src/ltProxyHandler.ts Normal file
View File

@@ -0,0 +1,57 @@
import { Handler } from 'http-kernel/Types/mod.ts';
import { Env } from './env.ts';
/**
* Forwards the incoming request to the actual LanguageTool server.
* Dynamically passes through path and query string.
* Removes `username` and `apiKey` from the FormData body if present.
*/
export const handler: Handler = async (ctx) => {
const originalUrl = new URL(ctx.req.url);
const proxyUrl = new URL(
`${originalUrl.pathname}${originalUrl.search}`,
`http://${Env.ltServerHost}:${Env.ltServerPort}`,
);
const contentType = ctx.req.headers.get('content-type') ?? '';
let body: BodyInit | null = null;
if (
contentType.includes('application/x-www-form-urlencoded') &&
ctx.state.body
) {
const text = new TextDecoder().decode(ctx.state.body as Uint8Array);
const params = new URLSearchParams(text);
// Remove `apiKey` and `username` from the params
// LanguageTool will react with a error if they are present
params.delete('apiKey');
params.delete('username');
body = params.toString();
} else {
console.debug('Unsupported content type:', contentType);
body = ctx.state.body as BodyInit | null;
}
const headers = new Headers(ctx.req.headers);
headers.delete('content-length');
console.debug('Forwarding request to:', proxyUrl.toString());
const forwarded = await fetch(proxyUrl.toString(), {
method: ctx.req.method,
headers,
body,
});
console.debug('Received response from LT server:', forwarded.status);
const respHeaders = new Headers(forwarded.headers);
return new Response(forwarded.body, {
status: forwarded.status,
headers: respHeaders,
});
};
export { handler as ltProxyHandler };

30
src/main.ts Normal file
View File

@@ -0,0 +1,30 @@
import { HttpKernel } from 'http-kernel/mod.ts';
import { Env } from './env.ts';
import { ltProxyAuth } from './ltProxyAuth.ts';
import { ltProxyHandler } from './ltProxyHandler.ts';
const ac = new AbortController();
const httpKernel = new HttpKernel();
httpKernel.route({
method: 'POST',
path: '/*',
}).middleware(ltProxyAuth).handle(ltProxyHandler);
Deno.serve({
signal: ac.signal,
port: Env.proxyPort,
hostname: Env.proxyHost,
onListen: ({ hostname, port }) => {
console.info(`lt-auth-proxy listening on ${hostname}:${port}`);
},
}, async (req) => await httpKernel.handle(req));
const shutdown = () => {
console.info('Shutting down the server...');
ac.abort();
console.info('Server shut down successfully.');
};
Deno.addSignalListener('SIGINT', shutdown);
Deno.addSignalListener('SIGTERM', shutdown);

6
src/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
export const maskApiKey = (key: string | null): string => {
if (!key) return '*****';
return key.length <= 5
? '*'.repeat(key.length)
: key.slice(0, 5) + '*'.repeat(key.length - 5);
};