19 Commits

Author SHA1 Message Date
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
10 changed files with 202 additions and 26 deletions

View File

@@ -43,7 +43,7 @@ jobs:
CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
VERSION_TAG=nightly-$(date +%Y%m%d)
docker buildx build \
--tag git.0xmax42.io/simdev/lt-auth-proxy:nightly \
--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" \

View File

@@ -63,8 +63,8 @@ jobs:
- name: Build Docker Image
run: |
docker buildx build \
--tag git.0xmax42.io/simdev/lt-auth-proxy:${{ steps.version.outputs.VERSION_STRIPPED }} \
--tag git.0xmax42.io/simdev/lt-auth-proxy:latest \
--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>" \

View File

@@ -2,6 +2,37 @@
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

View File

@@ -14,10 +14,12 @@ COPY import_map.json ./import_map.json
RUN deno task compile
# -------- Stage 2: Minimal runtime environment --------
FROM denoland/deno:alpine
FROM debian:bookworm-slim
# Optional: Install curl for container-level health checks (not needed for production-only binaries)
RUN apk add --no-cache curl
# 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

View File

@@ -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)

View File

@@ -1 +1 @@
0.1.0
0.2.1

View File

@@ -6,11 +6,19 @@ import { ltProxyAuth } from '../ltProxyAuth.ts';
Deno.test('ltProxyAuth: accepts valid API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const req = new Request('http://localhost/?apiKey=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: { apiKey: 'valid123' },
query: {},
state: {},
};
@@ -26,11 +34,19 @@ Deno.test('ltProxyAuth: accepts valid API key', async () => {
Deno.test('ltProxyAuth: rejects invalid API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const req = new Request('http://localhost/?apiKey=invalid456');
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: { apiKey: 'invalid456' },
query: {},
state: {},
};
@@ -46,7 +62,15 @@ Deno.test('ltProxyAuth: rejects invalid API key', async () => {
Deno.test('ltProxyAuth: rejects missing API key', async () => {
Deno.env.set('API_KEYS', 'valid123');
const req = new Request('http://localhost/');
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: {},
@@ -60,4 +84,5 @@ Deno.test('ltProxyAuth: rejects missing API key', async () => {
);
assertEquals(response.status, 403);
assertEquals(await response.text(), 'Forbidden – Invalid API key');
});

View File

@@ -16,12 +16,16 @@ Deno.test('ltProxyHandler: proxies request and returns response', async () => {
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',
body: new TextEncoder().encode('text=Hallo+Welt'),
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: bodyBytes,
});
const ctx: IContext = {
@@ -30,7 +34,9 @@ Deno.test('ltProxyHandler: proxies request and returns response', async () => {
query: {
language: 'de-DE',
},
state: {},
state: {
body: bodyBytes,
},
};
const response = await ltProxyHandler(ctx);

View File

@@ -2,17 +2,25 @@ import { Middleware } from 'http-kernel/Types/mod.ts';
import { Env } from './env.ts';
/**
* Middleware that checks for a valid API key via ?apiKey=... query/form param.
* Rejects request with 403 if the key is missing or invalid.
* 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 key = ctx.query.apiKey;
const contentType = ctx.req.headers.get('content-type') || '';
// Support both ?apiKey=... and form body with apiKey=...
const extractedKey = Array.isArray(key) ? key[0] : key;
if (contentType.includes('application/x-www-form-urlencoded')) {
const bodyBuffer = await ctx.req.arrayBuffer();
ctx.state.body = new Uint8Array(bodyBuffer);
if (!extractedKey || !Env.apiKeys.includes(extractedKey)) {
return new Response('Forbidden – Invalid API key', { status: 403 });
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();

View File

@@ -4,6 +4,7 @@ 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);
@@ -12,16 +13,40 @@ export const handler: Handler = async (ctx) => {
`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: ctx.req.headers,
body: ctx.req.body,
headers,
body,
});
const headers = new Headers(forwarded.headers);
const respHeaders = new Headers(forwarded.headers);
return new Response(forwarded.body, {
status: forwarded.status,
headers,
headers: respHeaders,
});
};