@deltabreen/help-server
v0.1.1
Published
Help by DeltaBreen for your server: sign the token for a logged-in user, and answer the calls Help makes to your URL for the side panel, buttons and webhooks. No dependencies.
Maintainers
Readme
Help by DeltaBreen for your server (Node)
Two things a site's server does for Help, in one file with no dependencies:
- Logged-in users. Sign the short token that tells the form who is logged in, so they write in without typing their email and see their own tickets.
- Your URL. Answer the calls Help makes to it: the side panel your team sees beside a ticket, the buttons they press, and your webhooks' events. The signature is checked before anything is read.
Runs on Node 22 and later, Next.js, Hono, Cloudflare Workers, Bun and Deno.
npm install @deltabreen/help-serverLogged-in users
Make the secret on your site's Install tab, under Recognise logged-in customers, and put it in your server's
environment as HELP_IDENTITY_SECRET. Then answer the address the script asks:
import { identityResponse } from '@deltabreen/help-server'
// Next.js: app/help-token/route.js
export async function GET() {
const user = await currentUser() // yours: null when nobody is logged in
return identityResponse(user && { id: user.id, email: user.email, facts: { plan: user.plan } })
}<script src="https://help.deltabreen.eu/embed/v1.js" data-help-key="pk_live_…" data-help-identity-url="/help-token" async></script>signIdentity(user, options) gives the token itself, for a page that hands it over with DeltaHelp.identify. A
token lasts ten minutes and works once; the form asks for a new one when it needs one. In an app's web view, which
hands one token over before the page loads, pass { once: false }.
| user | |
|---|---|
| id | Required. Your own id for them, text or a whole number, up to 128 characters. Your URL is sent it back. |
| email | Optional. Replies go here too. |
| facts | Optional. Up to ten short values your team sees on their tickets, like { plan: 'gold' }: names of lowercase letters, digits and _, values of text (up to 100 characters), a number, or true or false. |
Your URL
Add an integration on your site's Integrations page with your URL, and put the secret it shows you in your server's
environment as HELP_CONNECTOR_SECRET (a webhook's as HELP_WEBHOOK_SECRET; one URL can take both).
import { createHandler } from '@deltabreen/help-server'
const help = createHandler({
// The side panel: what your team sees beside this person's ticket, or null if you don't know them.
async hydrate({ user, mode }) {
const account = await accounts.find(user.id)
return account && { name: account.name, plan: account.plan, renews: account.renewsAt }
},
// A button your team pressed, by the key you gave it. Done once per press, however often the call arrives.
async action({ user, action }) {
if (action.key === 'cancel_subscription') {
const done = await billing.cancel(user.id)
return { ok: true, message: 'Cancelled.', reference: done.id }
}
return { ok: false, message: 'Not a button we know.' }
},
// A webhook's event, once per event.
async event(event) {
if (event.type === 'ticket.created') await chat.post(`New ticket #${event.ticket.number}`)
},
})Then give it the request as your framework has it, with the body unread, since the signature is over its bytes:
// Next.js App Router: app/support/hook/route.js
export const POST = (request) => help.fetch(request)
// Hono
app.post('/support/hook', (c) => help.fetch(c.req.raw))
// Cloudflare Workers: there is no process.env, so pass the secret
export default { fetch: (request, env) => createHandler({ secrets: env.HELP_CONNECTOR_SECRET, hydrate }).fetch(request) }
// Express: the raw body, never express.json(), on this route
app.post('/support/hook', express.raw({ type: '*/*' }), help.node)
// Node's own server
http.createServer(help.node).listen(8787)The connection test (Help's handshake) is answered for you. A call whose signature does not check out is answered
401 before its body is read. mode is live or test, the key the ticket came through; createHandler({ mode:
'live' }) answers only calls about live tickets.
Several copies of your server. Presses and events are remembered in memory, which is enough for one copy. With
more, give a store they share:
const store = {
async claim(key, ttl) {
if (await redis.set(key, 'busy', { NX: true, EX: 60 })) return { state: 'new' }
const had = await redis.get(key)
return had === 'busy' ? { state: 'busy' } : { state: 'done', result: had }
},
finish: (key, result, ttl) => redis.set(key, result, { EX: ttl }),
release: (key) => redis.del(key),
}The contract
What goes over the wire is in Integrations and the
form's guide. This library is held to the same published vectors as Help's
own server: signature-vectors.json and
identity-vectors.json.
