diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 59a95d5..8925599 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(npm test:*)",
- "Bash(npm install:*)"
+ "Bash(npm install:*)",
+ "Bash(find /c/Users/josh1/Documents/Code/Catalyst -type f \\\\\\(-name *.test.js -o -name *.spec.js -o -name .env* -o -name *.config.js \\\\\\))"
]
}
}
diff --git a/Dockerfile b/Dockerfile
index 77fa638..6c6bcd3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,6 @@
FROM node:lts-alpine
+RUN addgroup -S app && adduser -S app -G app
+
WORKDIR /app
COPY package*.json ./
@@ -8,5 +10,8 @@ COPY . .
RUN awk -F'"' '/"version"/{printf "const VERSION = \"%s\";\n", $4; exit}' \
package.json > js/version.js
+RUN chown -R app:app /app
+USER app
+
EXPOSE 3000
CMD ["node", "server/server.js"]
diff --git a/index.html b/index.html
index 5d1e275..485c45c 100644
--- a/index.html
+++ b/index.html
@@ -176,7 +176,6 @@
-
diff --git a/package-lock.json b/package-lock.json
index e8bf6c8..4992715 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,14 +1,15 @@
{
"name": "catalyst",
- "version": "1.0.3",
+ "version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "catalyst",
- "version": "1.0.3",
+ "version": "1.1.0",
"dependencies": {
- "express": "^4.18.0"
+ "express": "^4.18.0",
+ "helmet": "^8.1.0"
},
"devDependencies": {
"jsdom": "^25.0.0",
@@ -1958,6 +1959,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
diff --git a/package.json b/package.json
index eb9cea4..6aaae25 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "catalyst",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"scripts": {
"start": "node server/server.js",
@@ -9,7 +9,8 @@
"version:write": "node -e \"const {version}=JSON.parse(require('fs').readFileSync('package.json','utf8'));require('fs').writeFileSync('js/version.js','const VERSION = \\\"'+version+'\\\";\\n');\""
},
"dependencies": {
- "express": "^4.18.0"
+ "express": "^4.18.0",
+ "helmet": "^8.1.0"
},
"devDependencies": {
"jsdom": "^25.0.0",
diff --git a/server/routes.js b/server/routes.js
index 8b1e199..c75b578 100644
--- a/server/routes.js
+++ b/server/routes.js
@@ -22,6 +22,9 @@ function validate(body) {
errors.push(`state must be one of: ${VALID_STATES.join(', ')}`);
if (!VALID_STACKS.includes(body.stack))
errors.push(`stack must be one of: ${VALID_STACKS.join(', ')}`);
+ const ip = (body.tailscale_ip ?? '').trim();
+ if (ip && !/^(\d{1,3}\.){3}\d{1,3}$/.test(ip))
+ errors.push('tailscale_ip must be a valid IPv4 address or empty');
return errors;
}
diff --git a/server/server.js b/server/server.js
index a09660c..b14ab0f 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,4 +1,5 @@
import express from 'express';
+import helmet from 'helmet';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { router } from './routes.js';
@@ -8,6 +9,15 @@ const PORT = process.env.PORT ?? 3000;
export const app = express();
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ ...helmet.contentSecurityPolicy.getDefaultDirectives(),
+ 'style-src': ["'self'", 'https://fonts.googleapis.com'],
+ 'font-src': ["'self'", 'https://fonts.gstatic.com'],
+ },
+ },
+}));
app.use(express.json());
// API