// A customer's site for Help by DeltaBreen, in one file with no dependencies: a page with the support form on it, // and the connector endpoint. It is the sample the docs point to and the stand-in customer the dev checks run against. // // HELP_CONNECTOR_SECRET=hcs_… HELP_KEY=pk_test_… HELP_IDENTITY_SECRET=hsk_test_… node server.mjs // // GET / the support page, for a visitor who is not signed in // GET /?user=42 the same page for a signed-in customer (42 and 43 exist) // GET /help-token?user= the identity token the page's embed fetches for that customer // POST /support/hook the connector endpoint, and where a webhook's events are taken // // Set HELP_WEBHOOK_SECRET as well to take a webhook's events here: they are signed the same way, with the // webhook's own secret. // // The page is sent with a Content-Security-Policy that allows nothing but its own stylesheet and our script and // API: if the form works here, it works on a site that locks itself down. // // What a real one must do, and this does: verify the signature over the RAW body before parsing it, // answer a handshake with the keyed HMAC of the challenge, act on an action's idempotency key once, and act on // a webhook's event once, by its id. import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto' import { createServer } from 'node:http' import { fileURLToPath } from 'node:url' export const SIGNATURE_HEADER = 'x-deltabreen-signature' export const REPLAY_WINDOW_SECS = 300 const hex = (secret, ...parts) => parts.reduce((mac, p) => mac.update(p), createHmac('sha256', secret)).digest('hex') /** True when the header carries a `v1` that is right for this secret and a `t` within five minutes of now. */ export function verify(header, rawBody, secret, nowSecs = Date.now() / 1000) { if (!secret || typeof header !== 'string') return false const parts = header.split(',').map((p) => p.trim()) const t = parts.find((p) => p.startsWith('t='))?.slice(2) if (!t || !/^\d+$/.test(t) || Math.abs(nowSecs - Number(t)) > REPLAY_WINDOW_SECS) return false const want = Buffer.from(hex(secret, `${t}.`, rawBody)) return parts .filter((p) => p.startsWith('v1=')) .map((p) => Buffer.from(p.slice(3))) .some((got) => got.length === want.length && timingSafeEqual(got, want)) } /** What a handshake is answered with: proof of holding the secret, which an echo of the challenge is not. */ export const handshakeAnswer = (secret, challenge) => hex(secret, challenge) const b64url = (data) => Buffer.from(data).toString('base64url') /** * The identity token for a signed-in customer: a JWT signed with the product's identity secret (HS256). `sub` is * your own id for the person; `exp` a few minutes away, since the embed asks for a fresh one whenever it needs * one; `jti` makes it good for one use. `user` is optional: short facts you vouch for about the person, such as * their plan, which each of their tickets then carries. This is all a real site needs to write, in whatever * language it runs. */ export function identityToken(secret, user, nowSecs = Math.floor(Date.now() / 1000)) { const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) const claims = b64url(JSON.stringify({ sub: String(user.id), email: user.email, ...(user.plan ? { user: { plan: user.plan } } : {}), iat: nowSecs, exp: nowSecs + 300, jti: randomUUID() })) return `${header}.${claims}.${createHmac('sha256', secret).update(`${header}.${claims}`).digest('base64url')}` } // The sample's "database": two customers, and the actions already carried out, by idempotency key. const CUSTOMERS = { 42: { name: 'Ada Example', email: 'ada@example.com', plan: 'Pro', since: '2025-03-14', mrr: { amount: 2900, currency: 'EUR' }, orders: [{ id: 'ord_1001', total: 2900, status: 'paid' }, { id: 'ord_1002', total: 2900, status: 'paid' }], cancelled: false }, 43: { name: 'Bo Sample', email: 'bo@example.com', plan: 'Free', since: '2026-01-02', mrr: { amount: 0, currency: 'EUR' }, orders: [], cancelled: false }, } const done = new Map() // The events a webhook told us, by id: one told again after a retry is not acted on twice. const told = new Set() /** The answer to a verified envelope: `[status, body]`. */ export function answer(envelope, secret) { if (envelope.type === 'handshake') return [200, { challenge_response: handshakeAnswer(secret, envelope.challenge ?? '') }] // A webhook's event: a test (`ping`) or something that happened on a ticket. Answer 2xx quickly and do the // work afterwards; anything else and it is sent again. if (envelope.type === 'ping' || String(envelope.type).startsWith('ticket.')) { const again = told.has(envelope.id) told.add(envelope.id) return [200, { ok: true, again }] } const customer = CUSTOMERS[envelope.user?.id] if (!customer) return [404, { error: 'no such customer' }] if (envelope.type === 'hydrate') return [200, customer] if (envelope.type === 'action') { const { key, idempotency_key: once } = envelope.action ?? {} if (!once) return [400, { error: 'an action carries an idempotency_key' }] // The same press retried is answered again, not carried out again. if (done.has(once)) return [200, done.get(once)] if (key !== 'cancel_subscription') return [400, { error: `unknown action ${key}` }] customer.cancelled = true customer.plan = 'Free' const result = { ok: true, reference: `cancel_${envelope.user.id}`, message: 'Subscription cancelled.' } done.set(once, result) return [200, result] } return [400, { error: `unknown type ${envelope.type}` }] } const attr = (s) => String(s).replace(/[&"<>]/g, (c) => ({ '&': '&', '"': '"', '<': '<', '>': '>' })[c]) // The page also lists the product's help articles (``), which shows nothing until one is // published in the portal. An article's own link is this page's address followed by `#help-article=`. // The policy needs nothing more for them: a picture in an article comes from this site, which `img-src 'self'` allows. /** The support page and the policy it is sent with. `help` is where the embed is loaded from; `api` only differs when the two are run apart locally; `lang` is the page's language, which the form's words follow. */ export function page({ key, help = 'https://help.deltabreen.eu', api = help, user = null, lang = 'en' }) { // 'self' is for the page's own /help-token; everything else the embed needs is our one host. const sources = ["'self'", ...new Set([help, api])].join(' ') const csp = `default-src 'none'; script-src ${help}; connect-src ${sources}; style-src 'self'; img-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'` const signedIn = user && CUSTOMERS[user] ? CUSTOMERS[user] : null const language = /^[a-z]{2}(-[A-Za-z]{2})?$/.test(lang ?? '') ? lang : 'en' const html = ` Acme shop: support

Acme shop

${signedIn ? `Signed in as ${attr(signedIn.name)}. ` : ''}Something not right with an order? Write to us here.

${signedIn ? '

Your tickets

\n' : ''}
` return { csp, html } } const SITE_CSS = 'body{font:16px/1.5 system-ui,sans-serif;margin:0;background:#fafaf7;color:#1c1c1a}main{max-width:34rem;margin:3rem auto;padding:0 1rem}h1{margin:0 0 .25rem}h2{margin:2.5rem 0 .5rem;font-size:1.2rem}db-help-articles{display:block;margin-top:2.5rem}' export function handler(secret, log = console.log, site = null) { // One secret, or several: the integration's and a webhook's each sign their own requests. const secrets = [secret].flat().filter(Boolean) return (req, res) => { const reply = (status, body) => { res.writeHead(status, { 'content-type': 'application/json' }) res.end(JSON.stringify(body)) } const url = new URL(req.url, 'http://sample.invalid') if (site && req.method === 'GET' && url.pathname === '/') { // `?lang=de` shows the form as a German page would. const { csp, html } = page({ ...site, user: url.searchParams.get('user'), lang: url.searchParams.get('lang') }) res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': csp, 'cache-control': 'no-store' }) return res.end(html) } // A real site reads who is signed in from its own session cookie; the sample takes it from the address. if (site && req.method === 'GET' && url.pathname === '/help-token') { const user = url.searchParams.get('user') const customer = CUSTOMERS[user] if (!customer || !site.identitySecret) { res.writeHead(204, { 'cache-control': 'no-store' }) return res.end() } res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }) return res.end(JSON.stringify({ token: identityToken(site.identitySecret, { id: user, email: customer.email, plan: customer.plan }) })) } if (site && req.method === 'GET' && req.url === '/site.css') { res.writeHead(200, { 'content-type': 'text/css; charset=utf-8' }) return res.end(SITE_CSS) } if (req.method !== 'POST' || req.url !== '/support/hook') return reply(404, { error: 'not found' }) const chunks = [] req.on('data', (c) => chunks.push(c)) req.on('end', () => { const raw = Buffer.concat(chunks) const held = secrets.find((s) => verify(req.headers[SIGNATURE_HEADER], raw, s)) if (!held) { log('refused: bad or missing signature') return reply(401, { error: 'bad signature' }) } let envelope try { envelope = JSON.parse(raw.toString('utf8')) } catch { return reply(400, { error: 'not JSON' }) } log(`valid signature: ${envelope.type} ${envelope.id}`) reply(...answer(envelope, held)) }) } } if (process.argv[1] === fileURLToPath(import.meta.url)) { const secret = process.env.HELP_CONNECTOR_SECRET if (!secret) { console.error('Set HELP_CONNECTOR_SECRET to the connector secret shown in Help.') process.exit(1) } const port = Number(process.env.PORT ?? 8787) const { HELP_KEY: key, HELP_ORIGIN: help, HELP_API: api, HELP_IDENTITY_SECRET: identitySecret } = process.env const site = key ? { key, identitySecret, ...(help ? { help } : {}), ...(api ? { api } : {}) } : null createServer(handler([secret, process.env.HELP_WEBHOOK_SECRET], console.log, site)).listen(port, () => console.log(`${site ? 'support page on / and ' : ''}connector endpoint on :${port}, POST /support/hook`)) }