updated network for proxy use

This commit is contained in:
2025-11-01 11:28:16 +01:00
parent 00b48c1f41
commit 0614b2f3f8
8 changed files with 252 additions and 35 deletions

View File

@@ -14,3 +14,14 @@ PORT=${PORT:-3002}
# App Configuration
APP_TITLE="Shift Planning App"
ENABLE_PRO=${ENABLE_PRO:-false}
# Trust Proxy Configuration
TRUST_PROXY_ENABLED=false
TRUSTED_PROXY_IPS= # Your load balancer/reverse proxy IPs
TRUSTED_PROXY_HEADER=x-forwarded-for
# Rate Limiting Configuration
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_SKIP_PATHS=/api/health,/api/status
RATE_LIMIT_WHITELIST=127.0.0.1,::1

View File

@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "npm run build && npx tsx src/server.ts",
"dev:single": "cross-env NODE_ENV=development npx tsx src/server.ts",
"dev:single": "cross-env NODE_ENV=development TRUST_PROXY_ENABLED=false npx tsx src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"prestart": "npm run build",

View File

@@ -52,3 +52,43 @@ export const requireRole = (roles: string[]) => {
next();
};
};
// Add this function to your existing auth.ts
export const getClientIP = (req: Request): string => {
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
const forwarded = req.headers[trustedHeader];
const realIp = req.headers['x-real-ip'];
if (forwarded) {
if (Array.isArray(forwarded)) {
return forwarded[0].split(',')[0].trim();
} else if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
}
if (realIp) {
return realIp.toString();
}
return req.socket.remoteAddress || req.ip || 'unknown';
};
// Add IP-based security checks
export const ipSecurityCheck = (req: AuthRequest, res: Response, next: NextFunction): void => {
const clientIP = getClientIP(req);
// Log suspicious activity
const suspiciousPaths = ['/api/auth/login', '/api/auth/register'];
if (suspiciousPaths.includes(req.path)) {
console.log(`🔐 Auth attempt from IP: ${clientIP}, Path: ${req.path}`);
}
// Block known malicious IPs (you can expand this)
const blockedIPs = process.env.BLOCKED_IPS?.split(',') || [];
if (blockedIPs.includes(clientIP)) {
console.warn(`🚨 Blocked request from banned IP: ${clientIP}`);
res.status(403).json({ error: 'Access denied' });
return;
}
}

View File

@@ -1,6 +1,46 @@
import rateLimit from 'express-rate-limit';
import { Request } from 'express';
// Secure IP extraction that works with proxy settings
const getClientIP = (req: Request): string => {
// Read from environment which header to trust
const trustedHeader = process.env.TRUSTED_PROXY_HEADER || 'x-forwarded-for';
const forwarded = req.headers[trustedHeader];
const realIp = req.headers['x-real-ip'];
const cfConnectingIp = req.headers['cf-connecting-ip']; // Cloudflare
// If we have a forwarded header and trust proxy is configured
if (forwarded) {
if (Array.isArray(forwarded)) {
const firstIP = forwarded[0].split(',')[0].trim();
console.log(`🔍 Extracted IP from ${trustedHeader}: ${firstIP} (from: ${forwarded[0]})`);
return firstIP;
} else if (typeof forwarded === 'string') {
const firstIP = forwarded.split(',')[0].trim();
console.log(`🔍 Extracted IP from ${trustedHeader}: ${firstIP} (from: ${forwarded})`);
return firstIP;
}
}
// Cloudflare support
if (cfConnectingIp) {
console.log(`🔍 Using Cloudflare IP: ${cfConnectingIp}`);
return cfConnectingIp.toString();
}
// Fallback to x-real-ip
if (realIp) {
console.log(`🔍 Using x-real-ip: ${realIp}`);
return realIp.toString();
}
// Final fallback to connection remote address
const remoteAddress = req.socket.remoteAddress || req.ip || 'unknown';
console.log(`🔍 Using remote address: ${remoteAddress}`);
return remoteAddress;
};
// Helper to check if request should be limited
const shouldSkipLimit = (req: Request): boolean => {
const skipPaths = [
@@ -14,35 +54,92 @@ const shouldSkipLimit = (req: Request): boolean => {
return true;
}
// Skip for whitelisted IPs from environment
const whitelist = process.env.RATE_LIMIT_WHITELIST?.split(',') || [];
const clientIP = getClientIP(req);
if (whitelist.includes(clientIP)) {
console.log(`✅ IP whitelisted: ${clientIP}`);
return true;
}
return skipPaths.includes(req.path);
};
// Environment-based configuration
const getRateLimitConfig = () => {
const isProduction = process.env.NODE_ENV === 'production';
return {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes default
max: isProduction
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100') // Stricter in production
: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '1000'), // More lenient in development
// Development-specific relaxations
skip: (req: Request) => {
// Skip all GET requests in development for easier testing
if (!isProduction && req.method === 'GET') {
return true;
}
return shouldSkipLimit(req);
}
};
};
// Main API limiter - nur für POST/PUT/DELETE
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 200, // 200 non-GET requests per 15 minutes
...getRateLimitConfig(),
message: {
error: 'Zu viele Anfragen, bitte verlangsamen Sie Ihre Aktionen'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
// ✅ Skip für GET requests (Data Fetching)
if (req.method === 'GET') return true;
keyGenerator: (req) => getClientIP(req),
handler: (req, res) => {
const clientIP = getClientIP(req);
console.warn(`🚨 Rate limit exceeded for IP: ${clientIP}, Path: ${req.path}, Method: ${req.method}`);
// ✅ Skip für Health/Status Checks
return shouldSkipLimit(req);
res.status(429).json({
error: 'Zu viele Anfragen',
message: 'Bitte versuchen Sie es später erneut',
retryAfter: '15 Minuten',
clientIP: process.env.NODE_ENV === 'development' ? clientIP : undefined // Only expose IP in dev
});
}
});
// Strict limiter for auth endpoints
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX_REQUESTS || '5'),
message: {
error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut'
},
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true,
keyGenerator: (req) => getClientIP(req),
handler: (req, res) => {
const clientIP = getClientIP(req);
console.warn(`🚨 Auth rate limit exceeded for IP: ${clientIP}`);
res.status(429).json({
error: 'Zu viele Login-Versuche',
message: 'Aus Sicherheitsgründen wurde Ihr Konto temporär gesperrt',
retryAfter: '15 Minuten'
});
}
});
// Separate limiter for expensive endpoints
export const expensiveEndpointLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: parseInt(process.env.EXPENSIVE_ENDPOINT_LIMIT || '10'),
message: {
error: 'Zu viele Anfragen für diese Ressource'
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => getClientIP(req)
});

View File

@@ -14,7 +14,12 @@ import shiftPlanRoutes from './routes/shiftPlans.js';
import setupRoutes from './routes/setup.js';
import scheduledShifts from './routes/scheduledShifts.js';
import schedulingRoutes from './routes/scheduling.js';
import { authLimiter, apiLimiter } from './middleware/rateLimit.js';
import {
apiLimiter,
authLimiter,
expensiveEndpointLimiter
} from './middleware/rateLimit.js';
import { ipSecurityCheck as authIpCheck } from './middleware/auth.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -23,6 +28,8 @@ const app = express();
const PORT = 3002;
const isDevelopment = process.env.NODE_ENV === 'development';
app.use(authIpCheck);
let vite: ViteDevServer | undefined;
if (isDevelopment) {
@@ -79,8 +86,6 @@ const configureStaticFiles = () => {
return null;
};
app.set('trust proxy', true);
// Security configuration
if (process.env.NODE_ENV === 'production') {
console.info('Checking for JWT_SECRET');
@@ -91,6 +96,48 @@ if (process.env.NODE_ENV === 'production') {
}
}
const configureTrustProxy = (): string | string[] | boolean | number => {
const trustedProxyIps = process.env.TRUSTED_PROXY_IPS;
const trustProxyEnabled = process.env.TRUST_PROXY_ENABLED !== 'false'; // Default true for production
// If explicitly disabled
if (!trustProxyEnabled) {
console.log('🔒 Trust proxy: Disabled');
return false;
}
// If specific IPs are provided via environment variable
if (trustedProxyIps) {
console.log('🔒 Trust proxy: Using configured IPs:', trustedProxyIps);
// Handle comma-separated list of IPs/CIDR ranges
if (trustedProxyIps.includes(',')) {
return trustedProxyIps.split(',').map(ip => ip.trim());
}
// Handle single IP/CIDR
return trustedProxyIps.trim();
}
// Default behavior based on environment
if (process.env.NODE_ENV === 'production') {
console.log('🔒 Trust proxy: Using production defaults (private networks)');
return [
'loopback',
'linklocal',
'uniquelocal',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16'
];
} else {
console.log('🔒 Trust proxy: Development mode (disabled)');
return false;
}
};
app.set('trust proxy', configureTrustProxy());
// Security headers
app.use(helmet({
contentSecurityPolicy: {
@@ -123,9 +170,12 @@ app.use(express.json());
// Rate limiting - weniger restriktiv in Development
if (process.env.NODE_ENV === 'production') {
console.log('🔒 Applying production rate limiting');
app.use('/api/', apiLimiter);
} else {
console.log('🔧 Development: Rate limiting relaxed');
console.log('🔧 Development: Relaxed rate limiting applied');
// In development, you might want to be more permissive
app.use('/api/', apiLimiter);
}
// API Routes
@@ -134,7 +184,7 @@ app.use('/api/auth', authLimiter, authRoutes);
app.use('/api/employees', employeeRoutes);
app.use('/api/shift-plans', shiftPlanRoutes);
app.use('/api/scheduled-shifts', scheduledShifts);
app.use('/api/scheduling', schedulingRoutes);
app.use('/api/scheduling', expensiveEndpointLimiter, schedulingRoutes);
// Health route
app.get('/api/health', (req: express.Request, res: express.Response) => {
@@ -279,7 +329,7 @@ const initializeApp = async () => {
if (frontendBuildPath) {
console.log(`📍 Frontend: http://localhost:${PORT}`);
} else if (isDevelopment) {
console.log(`📍 Frontend (Vite): http://localhost:3002`);
console.log(`📍 Frontend (Vite): http://localhost:3003`);
}
console.log(`📍 API: http://localhost:${PORT}/api`);
});

View File

@@ -28,7 +28,7 @@
"framer-motion": "12.23.24"
},
"scripts": {
"dev": "vite",
"dev": "vite dev",
"build": "tsc && vite build",
"preview": "vite preview"
}

View File

@@ -14,14 +14,14 @@ export default defineConfig(({ mode }) => {
NODE_ENV: mode,
ENABLE_PRO: env.ENABLE_PRO || 'false',
VITE_APP_TITLE: env.APP_TITLE || 'Shift Planning App',
VITE_API_URL: isProduction ? '/api' : 'http://localhost:3002/api',
VITE_API_URL: isProduction ? '/api' : '/api',
}
return {
plugins: [react()],
server: isDevelopment ? undefined : {
port: 3002,
server: isDevelopment ? {
port: 3003,
host: true,
open: true,
proxy: {
@@ -31,7 +31,7 @@ export default defineConfig(({ mode }) => {
secure: false,
}
}
},
} : undefined,
build: {
outDir: 'dist',

45
package-lock.json generated
View File

@@ -260,7 +260,6 @@
"backend/node_modules/@types/node": {
"version": "24.7.0",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.14.0"
}
@@ -1857,7 +1856,6 @@
"version": "7.28.5",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2288,6 +2286,10 @@
"win32"
]
},
"node_modules/@schichtenplaner/premium": {
"resolved": "premium",
"link": true
},
"node_modules/@sinclair/typebox": {
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
@@ -2308,6 +2310,7 @@
"integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
},
@@ -2551,6 +2554,7 @@
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
@@ -2561,7 +2565,8 @@
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
@@ -2623,7 +2628,6 @@
"version": "19.2.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2704,6 +2708,7 @@
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.4",
@@ -2721,6 +2726,7 @@
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
@@ -2748,6 +2754,7 @@
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tinyrainbow": "^2.0.0"
},
@@ -2761,6 +2768,7 @@
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tinyspy": "^4.0.3"
},
@@ -2774,6 +2782,7 @@
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
@@ -2857,6 +2866,7 @@
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -2867,6 +2877,7 @@
"integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.0.1"
},
@@ -2959,7 +2970,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -3044,6 +3054,7 @@
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
@@ -3078,6 +3089,7 @@
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 16"
}
@@ -3255,6 +3267,7 @@
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@@ -3416,6 +3429,7 @@
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -3430,6 +3444,7 @@
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -3994,7 +4009,8 @@
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lru-cache": {
"version": "5.1.1",
@@ -4020,6 +4036,7 @@
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
@@ -4237,6 +4254,7 @@
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.16"
}
@@ -4367,7 +4385,6 @@
"node_modules/react": {
"version": "19.2.0",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4375,7 +4392,6 @@
"node_modules/react-dom": {
"version": "19.2.0",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4432,6 +4448,7 @@
"integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ast-types": "^0.16.1",
"esprima": "~4.0.0",
@@ -4770,6 +4787,7 @@
"integrity": "sha512-WQMfNsbyHO8fx5bLatsLATzvFwzG42xHnq8W9s2HzjhGzQ2Pp77k7nRWXMuRNvvv+4ypKhJdqeDl8S9kwWuz2A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.6.0",
@@ -4805,6 +4823,7 @@
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -4844,7 +4863,6 @@
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -4863,7 +4881,8 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tinyglobby": {
"version": "0.2.15",
@@ -4886,6 +4905,7 @@
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -4896,6 +4916,7 @@
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -4946,7 +4967,6 @@
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5029,7 +5049,6 @@
"version": "6.4.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -5176,6 +5195,7 @@
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -5200,7 +5220,6 @@
"premium": {
"name": "@schichtenplaner/premium",
"version": "1.0.0",
"extraneous": true,
"workspaces": [
"backendPRO",
"frontendPRO"