Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6f30e9a
fix(traefik): deep merge routers/services/middlewares in register()
Jan 14, 2026
9299cf5
refactor(traefik): eliminate double YAML parse-dump cycle
Jan 14, 2026
b708a20
chore(traefik): remove unused lastUserData tracking
Jan 14, 2026
dd62a74
feat(traefik): warn on router/service/middleware name collisions
Jan 14, 2026
061dfb1
fix(backend): replace unsafe ! assertions with ensureBackend()
Jan 14, 2026
542afcc
perf(traefik): only cleanup temp files on first write
Jan 14, 2026
fcdc242
fix(lint): replace explicit 'any' types with specific Record types
Jan 14, 2026
4e14e9f
refactor(traefik): fix double-render, extract error helper, remove de…
Jan 14, 2026
85fbe58
fix(traefik): tighten Context typing and safe lookup in template parser
Jan 14, 2026
eeadb9e
refactor(traefik): extract shared helpers and improve readability
Jan 14, 2026
a1d817e
chore(merge): bring refactor/traefik-readability into dev
Jan 14, 2026
50b0ae3
refactor: consolidate types and remove duplicates
Jan 14, 2026
dcd91ba
refactor: clean up logging module and API middleware
Jan 14, 2026
dd997b1
refactor: improve config and backend plugin modules
Jan 14, 2026
2963195
refactor: clean up traefik manager imports
Jan 14, 2026
ba0a578
refactor: remove dead code and update documentation
Jan 14, 2026
e96659d
Initial plan
Copilot Jan 14, 2026
b2816e0
Initial plan for addressing review comments
Copilot Jan 14, 2026
d8ba03a
Address review comments: improve templateParser, add helpers tests, f…
Copilot Jan 14, 2026
66a5d19
Improve VALID_BACKENDS type to maintain literal type safety
Copilot Jan 14, 2026
071100d
Merge pull request #32 from stonegray/copilot/sub-pr-31
stonegray Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 1 addition & 11 deletions src/api/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { zone } from '../../logging/zone';
import { getClientIP } from './utils';

const log = zone('api:auth');

Expand Down Expand Up @@ -48,14 +49,3 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
// Key is valid, proceed
next();
}

/**
* Extract client IP from request
*/
function getClientIP(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return req.socket.remoteAddress || 'unknown';
}
1 change: 1 addition & 0 deletions src/api/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { apiLimiter } from './ratelimit';
export { authMiddleware, setAPIKey } from './auth';
export { errorHandler, notFoundHandler } from './errorHandler';
export { validateQuery, validateBodySize } from './validation';
export { getClientIP } from './utils';
13 changes: 1 addition & 12 deletions src/api/middleware/logging.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { zone } from '../../logging/zone';
import { getClientIP } from './utils';

const log = zone('api:request');

Expand Down Expand Up @@ -41,15 +42,3 @@ export function requestLogging(req: Request, res: Response, next: NextFunction):

next();
}

/**
* Extract client IP from request
* Handles X-Forwarded-For header if behind a proxy
*/
function getClientIP(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return req.socket.remoteAddress || 'unknown';
}
13 changes: 13 additions & 0 deletions src/api/middleware/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Request } from 'express';

/**
* Extract client IP from request.
* Handles X-Forwarded-For header when behind a proxy.
*/
export function getClientIP(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return req.socket.remoteAddress || 'unknown';
}
4 changes: 0 additions & 4 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export type { APIConfig } from '../types/config';

export interface FieldData {
[key: string]: unknown;
}
59 changes: 47 additions & 12 deletions src/backends/backendPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@ import { loadConfigFile } from '../config';
import { MagicProxyConfigFile } from '../types/config';
import { HostEntry } from '../types/host';

type BackendStatus = { registered?: string[]; outputFile?: string | null;[key: string]: unknown };
/** Status returned by backend getStatus() */
export interface BackendStatus {
registered?: string[];
outputFile?: string | null;
[key: string]: unknown;
}

type BackendModule = {
/** Interface that all backend modules must implement */
export interface BackendModule {
initialize: (config?: MagicProxyConfigFile) => Promise<void>;
addProxiedApp: (entry: HostEntry) => Promise<void>;
removeProxiedApp: (appName: string) => Promise<void>;
getStatus: () => Promise<BackendStatus>;
};
}

let activeBackend: BackendModule | null = null;
let activeName: string | null = null;

/**
* Load a backend module by name.
*/
async function loadBackend(name: string): Promise<BackendModule> {
switch (name) {
case 'traefik': {
// dynamic import to avoid load-time side effects
const mod = await import('./traefik/traefik');
return {
initialize: mod.initialize,
Expand All @@ -31,11 +39,16 @@ async function loadBackend(name: string): Promise<BackendModule> {
}
}

/**
* Initialize the backend from configuration.
*/
export async function initialize(config?: MagicProxyConfigFile): Promise<void> {
const cfg = config || await loadConfigFile();
const backendName: string = cfg.proxyBackend;
const backendName = cfg.proxyBackend;

if (!backendName) throw new Error('No proxyBackend configured');
if (!backendName) {
throw new Error('No proxyBackend configured');
}

if (!activeBackend || activeName !== backendName) {
activeBackend = await loadBackend(backendName);
Expand All @@ -45,17 +58,39 @@ export async function initialize(config?: MagicProxyConfigFile): Promise<void> {
await activeBackend.initialize(cfg);
}

/**
* Get the active backend, initializing if needed.
*/
async function ensureBackend(): Promise<BackendModule> {
if (!activeBackend) {
await initialize();
}
if (!activeBackend) {
throw new Error('Backend initialization failed - no active backend');
}
return activeBackend;
}

/**
* Add or update a proxied application.
*/
export async function addProxiedApp(entry: HostEntry): Promise<void> {
if (!activeBackend) await initialize();
return activeBackend!.addProxiedApp(entry);
const backend = await ensureBackend();
return backend.addProxiedApp(entry);
}

/**
* Remove a proxied application.
*/
export async function removeProxiedApp(appName: string): Promise<void> {
if (!activeBackend) await initialize();
return activeBackend!.removeProxiedApp(appName);
const backend = await ensureBackend();
return backend.removeProxiedApp(appName);
}

/**
* Get the current backend status.
*/
export async function getStatus(): Promise<BackendStatus> {
if (!activeBackend) await initialize();
return activeBackend!.getStatus();
const backend = await ensureBackend();
return backend.getStatus();
}
109 changes: 55 additions & 54 deletions src/backends/readme.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,57 @@
# Adding a new proxy backend

magic-proxy is more or less setup for being fully proxy-agnostic.

Summary
- Backends are loaded dynamically by [`loadBackend`](src/backends/backendPlugin.ts) and must match the backend shape defined by [`BackendModule`](src/backends/backendPlugin.ts).
- The platform initializes the selected backend via [`initialize`](src/backends/backendPlugin.ts) (which is called from [`startApp`](src/index.ts) after config is loaded with [`loadConfigFile`](src/config.ts) and validated by [`validateConfig`](src/config.ts)).

Backend API (required)
- Export an object that implements the [`BackendModule`](src/backends/backendPlugin.ts) shape:
- initialize(config?: [`MagicProxyConfigFile`](src/types/config.d.ts)): Promise<void>
- addProxiedApp(entry: [`HostEntry`](src/types/host.d.ts)): Promise<void>
- removeProxiedApp(appName: string): Promise<void>
- getStatus(): Promise<{ registered?: string[]; outputFile?: string | null; [key: string]: unknown }>

Practical guidance / checklist
1. Module location & loading
- Add a module under `src/backends/<your-backend>/` and export the required functions.
- Add a case in [`loadBackend`](src/backends/backendPlugin.ts) to dynamically import your module by `proxyBackend` name.

2. Initialization
- Load any backend-specific configuration from the provided [`MagicProxyConfigFile`](src/types/config.d.ts).
- Validate required config and fail clearly (throw or log + exit). See the [`traefik`](src/backends/traefik/traefik.ts) behavior for examples.
- Set up any on-disk output paths or runtime state the backend needs.

3. Registry / state & atomic writes
- Provide deterministic IDs for registered apps so getStatus and subsequent calls are stable.
- If writing files (e.g., dynamic proxy config): write atomically (tmp file + rename) and validate output where possible. See [`register`](src/backends/traefik/traefikManager.ts) and [`flushToDisk`](src/backends/traefik/traefikManager.ts) for patterns.

4. addProxiedApp / removeProxiedApp behavior
- Accept a [`HostEntry`](src/types/host.d.ts) and perform idempotent registration.
- Ensure remove cleans up state so `getStatus()` reflects current registrations.

5. getStatus
- Return an object containing `registered` (array of app names) and optionally `outputFile` (or other runtime metadata).

6. Tests
- Add unit tests covering:
- Template loading and rendering.
- Registration/unregistration behavior.
- Output format validation (if generating files).
- Reuse helpers in [test/helpers/mockHelpers.ts](test/helpers/mockHelpers.ts) (FS mocks like `setupFSMocks` and `mockFileWrite`) and follow existing test patterns (see [test/legacy/backend.test.ts](test/legacy/backend.test.ts) and [test/legacy/traefik-file.test.ts](test/legacy/traefik-file.test.ts)).

7. Config schema
- If new config fields are required, update [`src/types/config.d.ts`](src/types/config.d.ts) and ensure [`validateConfig`](src/config.ts) accepts the backend name (add to valid backends if needed).

8. Error handling & startup
- Be explicit on fatal vs recoverable errors. The application startup (`startApp` in [`src/index.ts`](src/index.ts)) will exit on uncaught initialization errors; handle accordingly.

Examples & references
- Reference backend implementation: [`src/backends/traefik/traefik.ts`](src/backends/traefik/traefik.ts)
- Manager utilities: [`src/backends/traefik/traefikManager.ts`](src/backends/traefik/traefikManager.ts)
- Backend plugin loader and API: [`src/backends/backendPlugin.ts`](src/backends/backendPlugin.ts)
- Types: [`MagicProxyConfigFile`](src/types/config.d.ts), [`HostEntry`](src/types/host.d.ts)

If you want, I can scaffold a minimal backend module (with tests) using these patterns.
magic-proxy supports pluggable proxy backends.

## Summary
- Backends are loaded dynamically by `loadBackend` in `backendPlugin.ts`
- Backends must implement the `BackendModule` interface exported from `backendPlugin.ts`
- The platform initializes the selected backend via `initialize()` during startup

## Backend API (required)

Export a module that implements the `BackendModule` interface:

```typescript
interface BackendModule {
initialize(config?: MagicProxyConfigFile): Promise<void>;
addProxiedApp(entry: HostEntry): Promise<void>;
removeProxiedApp(appName: string): Promise<void>;
getStatus(): Promise<BackendStatus>;
}
```

## Implementation Checklist

1. **Module location & loading**
- Add a module under `src/backends/<your-backend>/`
- Add a case in `loadBackend()` to dynamically import your module

2. **Initialization**
- Load backend-specific configuration from `MagicProxyConfigFile`
- Validate required config and fail clearly (throw or log + exit)

3. **Registry & atomic writes**
- Provide deterministic IDs for registered apps
- Write files atomically (tmp file + rename) and validate output

4. **addProxiedApp / removeProxiedApp**
- Accept a `HostEntry` and perform idempotent registration
- Ensure remove cleans up state so `getStatus()` reflects current registrations

5. **getStatus**
- Return an object with `registered` (array of app names) and optionally `outputFile`

6. **Tests**
- Add unit tests for template loading/rendering, registration, and output validation
- Reuse helpers in `test/helpers/mockHelpers.ts`

7. **Config schema**
- Update `src/types/config.d.ts` if new config fields are required
- Update `validateConfig()` to accept the backend name

## Reference Implementation

See the Traefik backend:
- `src/backends/traefik/traefik.ts` - Main backend module
- `src/backends/traefik/traefikManager.ts` - Registry and file management
- `src/backends/backendPlugin.ts` - Plugin loader and interface
Loading
Loading