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