commit 47fe3c33e62f4b8302086d518eb558efddfa328a Author: Mathew Lewis Date: Wed Aug 26 22:08:17 2026 -0700 Add compose demo app for VeilStream preview testing web (node) renders the seeded customers table plus container hostname. db (postgres:16-alpine) seeds five PII-shaped rows from db/init.sql. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08afab3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +TOS.txt +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..73704a4 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# veilstream-demo + +Throwaway app for testing VeilStream preview environments. + +Two services: + +- `web` - Node, no framework. Renders the `customers` table plus the container + hostname, so you can tell one preview environment from another. +- `db` - `postgres:16-alpine`, seeded from `db/init.sql` with five rows of + deliberately PII-shaped data (name, email, phone) to give the sanitization + features something to act on. + +Primary service is `web` on port 3000. `/healthz` returns `ok`. + +Run locally: + + docker compose up --build + +Then open + +http://localhost:3000 diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 0000000..1fbe530 --- /dev/null +++ b/db/init.sql @@ -0,0 +1,16 @@ +CREATE TABLE customers ( + id serial PRIMARY KEY, + full_name text NOT NULL, + email text NOT NULL, + phone text NOT NULL, + city text NOT NULL, + plan text NOT NULL, + mrr_cents integer NOT NULL +); + +INSERT INTO customers (full_name, email, phone, city, plan, mrr_cents) VALUES + ('Ada Lovelace', 'ada@example.com', '+1 250 555 0101', 'Victoria', 'pro', 9900), + ('Grace Hopper', 'grace@example.com', '+1 250 555 0102', 'Nanaimo', 'team', 29900), + ('Alan Turing', 'alan@example.com', '+1 604 555 0103', 'Vancouver', 'free', 0), + ('Katherine Johnson','kj@example.com', '+1 250 555 0104', 'Sidney', 'pro', 9900), + ('Edsger Dijkstra', 'edsger@example.com', '+1 778 555 0105', 'Victoria', 'enterprise',99900); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d015091 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +services: + web: + build: + context: ./web + ports: + - "3000:3000" + environment: + DATABASE_URL: postgres://demo:demo@db:5432/demo + APP_LABEL: "VeilStream demo" + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: demo + POSTGRES_PASSWORD: demo + POSTGRES_DB: demo + volumes: + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U demo -d demo"] + interval: 5s + timeout: 3s + retries: 20 diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..72076eb --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev +COPY server.js ./ +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..3473684 --- /dev/null +++ b/web/package.json @@ -0,0 +1,9 @@ +{ + "name": "veilstream-demo-web", + "version": "1.0.0", + "private": true, + "main": "server.js", + "dependencies": { + "pg": "^8.13.1" + } +} diff --git a/web/server.js b/web/server.js new file mode 100644 index 0000000..7f02ad9 --- /dev/null +++ b/web/server.js @@ -0,0 +1,72 @@ +const http = require("http"); +const os = require("os"); +const { Pool } = require("pg"); + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const label = process.env.APP_LABEL || "VeilStream demo"; +const port = Number(process.env.PORT || 3000); + +const esc = (s) => + String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); + +async function page() { + let rows = []; + let dbError = null; + try { + const r = await pool.query( + "SELECT id, full_name, email, phone, city, plan, mrr_cents FROM customers ORDER BY id" + ); + rows = r.rows; + } catch (err) { + dbError = err.message; + } + + const body = dbError + ? `

Database unreachable: ${esc(dbError)}

` + : ` + + ${rows + .map( + (r) => + `` + ) + .join("\n ")} +
idnameemailphonecityplanmrr
${r.id}${esc(r.full_name)}${esc(r.email)}${esc( + r.phone + )}${esc(r.city)}${esc(r.plan)}$${(r.mrr_cents / 100).toFixed( + 2 + )}
+

${rows.length} rows. If these values look like real people, the seed was not sanitized.

`; + + return ` +${esc(label)} + +

${esc(label)}

+
+
hostname
${esc(os.hostname())}
+
served at
${new Date().toISOString()}
+
node
${esc(process.version)}
+
+${body} +`; +} + +http + .createServer(async (req, res) => { + if (req.url === "/healthz") { + res.writeHead(200, { "content-type": "text/plain" }); + return res.end("ok\n"); + } + res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + res.end(await page()); + }) + .listen(port, "0.0.0.0", () => console.log(`listening on ${port}`));