diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..119fb8b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +node_modules +npm-debug.log +data +coverage +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6878f3b --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Runtime +NODE_ENV=production +PORT=3000 +STATE_FILE_PATH=/data/state.json + +# Webhook secrets +X_WEBHOOK_SECRET=replace-me +POLAR_WEBHOOK_SECRET=replace-me + +# Credit policy +BASE_CREDITS=1 +INCLUDED_CHARS=25000 +STEP_CHARS=10000 +STEP_CREDITS=1 +MAX_CHARS_PER_ARTICLE=120000 + +# Rate limits (requests per minute) +WEBHOOK_RPM=120 +AUTH_RPM=30 +ACTION_RPM=60 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..543618b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-alpine + +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV STATE_FILE_PATH=/data/state.json + +COPY package.json ./ +COPY src ./src +COPY README.md ./README.md +COPY spec.md ./spec.md + +EXPOSE 3000 +VOLUME ["/data"] + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["node", "src/server.js"] diff --git a/test/deployment.test.js b/test/deployment.test.js new file mode 100644 index 0000000..3f4895d --- /dev/null +++ b/test/deployment.test.js @@ -0,0 +1,22 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); + +test("Dockerfile contains production container essentials", () => { + const dockerfile = fs.readFileSync("Dockerfile", "utf8"); + assert.match(dockerfile, /FROM node:22-alpine/); + assert.match(dockerfile, /EXPOSE 3000/); + assert.match(dockerfile, /STATE_FILE_PATH=\/data\/state\.json/); + assert.match(dockerfile, /HEALTHCHECK/); + assert.match(dockerfile, /CMD \["node", "src\/server\.js"\]/); +}); + +test("env example includes required webhook and credit settings", () => { + const envFile = fs.readFileSync(".env.example", "utf8"); + assert.match(envFile, /X_WEBHOOK_SECRET=/); + assert.match(envFile, /POLAR_WEBHOOK_SECRET=/); + assert.match(envFile, /INCLUDED_CHARS=/); + assert.match(envFile, /WEBHOOK_RPM=/); +});