Compare commits
26 Commits
01b9a9ff08
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 73bf48d4d7 | |||
|
72e81ddb0f
|
|||
| 9b26840d0a | |||
|
7cf391f417
|
|||
|
828494c92a
|
|||
|
b6763f7483
|
|||
|
4326a2d92c
|
|||
|
60dcc30c0d
|
|||
| 36f2999cc9 | |||
|
7ea8e26660
|
|||
| 5ba2ea1233 | |||
|
573fcf0e65
|
|||
| 0dc8764cc5 | |||
|
787bcdc1a2
|
|||
| 19088219cb | |||
|
ec18f7b4e3
|
|||
| 0d872a5acc | |||
|
a80ad6927e
|
|||
|
f931e876f9
|
|||
| ab66cbb893 | |||
|
0227c83057
|
|||
|
f9714cbb53
|
|||
|
52ce172ef5
|
|||
|
bcb22f983e
|
|||
|
b6e0947c3c
|
|||
|
b473b7cce1
|
50
.gitea/COMMIT_GPT.md
Normal file
50
.gitea/COMMIT_GPT.md
Normal 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
198
.gitea/HOWTO_RELEASE.md
Normal 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.
|
||||
21
.gitea/scripts/get-release-id.sh
Executable file
21
.gitea/scripts/get-release-id.sh
Executable 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
40
.gitea/scripts/upload-asset.sh
Executable 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"
|
||||
59
.gitea/workflows/build-nightly.yml
Normal file
59
.gitea/workflows/build-nightly.yml
Normal 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 \
|
||||
.
|
||||
|
||||
78
.gitea/workflows/build-release.yml
Normal file
78
.gitea/workflows/build-release.yml
Normal 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 \
|
||||
.
|
||||
222
.gitea/workflows/release.yml
Normal file
222
.gitea/workflows/release.yml
Normal 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
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
coverage/
|
||||
logs/
|
||||
.locale/
|
||||
cache/
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
||||
53
CHANGELOG.md
Normal file
53
CHANGELOG.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
38
Dockerfile
Normal 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"]
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
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
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
|
||||
81
README.md
81
README.md
@@ -1,3 +1,82 @@
|
||||
# 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)
|
||||
|
||||
104
cliff.toml
Normal file
104
cliff.toml
Normal 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
30
deno.jsonc
Normal 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
69
deno.lock
generated
Normal 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
5
import_map.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"imports": {
|
||||
"http-kernel/": "https://git.0xmax42.io/maxp/http-kernel/raw/tag/v0.1.0/src/"
|
||||
}
|
||||
}
|
||||
88
src/__tests__/ltProxyAuth.test.ts
Normal file
88
src/__tests__/ltProxyAuth.test.ts
Normal 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');
|
||||
});
|
||||
50
src/__tests__/ltProxyHandler.test.ts
Normal file
50
src/__tests__/ltProxyHandler.test.ts
Normal 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
63
src/env.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/ltProxyAuth.ts
Normal file
29
src/ltProxyAuth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Middleware } from 'http-kernel/Types/mod.ts';
|
||||
import { Env } from './env.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)) {
|
||||
return new Response('Forbidden – Invalid API key', { status: 403 });
|
||||
}
|
||||
} else {
|
||||
return new Response('Unsupported content type', { status: 415 });
|
||||
}
|
||||
|
||||
return await next();
|
||||
};
|
||||
|
||||
export { authMiddleware as ltProxyAuth };
|
||||
53
src/ltProxyHandler.ts
Normal file
53
src/ltProxyHandler.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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');
|
||||
|
||||
const forwarded = await fetch(proxyUrl.toString(), {
|
||||
method: ctx.req.method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const respHeaders = new Headers(forwarded.headers);
|
||||
return new Response(forwarded.body, {
|
||||
status: forwarded.status,
|
||||
headers: respHeaders,
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as ltProxyHandler };
|
||||
19
src/main.ts
Normal file
19
src/main.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { HttpKernel } from 'http-kernel/mod.ts';
|
||||
import { Env } from './env.ts';
|
||||
import { ltProxyAuth } from './ltProxyAuth.ts';
|
||||
import { ltProxyHandler } from './ltProxyHandler.ts';
|
||||
|
||||
const httpKernel = new HttpKernel();
|
||||
|
||||
httpKernel.route({
|
||||
method: 'POST',
|
||||
path: '/*',
|
||||
}).middleware(ltProxyAuth).handle(ltProxyHandler);
|
||||
|
||||
Deno.serve({
|
||||
port: Env.proxyPort,
|
||||
hostname: Env.proxyHost,
|
||||
onListen: ({ hostname, port }) => {
|
||||
console.info(`lt-auth-proxy listening on ${hostname}:${port}`);
|
||||
},
|
||||
}, async (req) => await httpKernel.handle(req));
|
||||
Reference in New Issue
Block a user