Private beta: this contract may change before launch. Join the waitlist
Hijinks
Guides

Verify signatures

Both directions carry the same header, signed with the workspace's one secret:

Code
Hijinks-Signature: t=<unix seconds>,v1=<hex>

The hex is HMAC-SHA256 over <t>.<raw body> with your secret. Three rules make it safe:

  • Sign and verify the raw bytes, never a re-serialised parse.
  • Compare in constant time.
  • Refuse timestamps more than 5 minutes off.

During a secret rotation the header may carry two v1= entries; either one matching is enough.

A poll has no body: sign the query string exactly as sent (everything after ?, or the empty string), so the signed text is <t>.<query>. The poll's answer is signed over its body like a push.

Each snippet has a verify function for events Hijinks sends you (and poll answers), a sign function for events your bridge sends, and a sign function for polls.

import { createHmac, timingSafeEqual } from 'node:crypto'; const TOLERANCE_SECONDS = 300; /** rawBody: the exact bytes received (e.g. express.raw({ type: 'application/json' })). */ export function verifyHijinks(rawBody, header, secret, now = Date.now()) { if (!header) return false; const pairs = header.split(',').map((part) => { const eq = part.indexOf('='); return [part.slice(0, eq).trim(), part.slice(eq + 1).trim()]; }); const t = pairs.find(([key]) => key === 't')?.[1]; if (!t || !/^\d+$/.test(t)) return false; if (Math.abs(Math.floor(now / 1000) - Number(t)) > TOLERANCE_SECONDS) return false; const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest(); return pairs.some( ([key, value]) => key === 'v1' && /^[0-9a-f]{64}$/i.test(value) && timingSafeEqual(Buffer.from(value, 'hex'), expected), ); } /** The header for a request your bridge sends; body is the exact string you POST. */ export function signHijinks(body, secret, now = Date.now()) { const t = Math.floor(now / 1000); const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex'); return `t=${t},v1=${v1}`; } /** The header for a poll; query is everything after `?` exactly as sent ('' for none). */ export const signPoll = (query, secret, now = Date.now()) => signHijinks(query, secret, now);

To check your code end to end without a bridge, sign a request with openssl as in the Quickstart.

Rotating the secret

Rotate secret in the workspace's settings shows a new secret once. The old one keeps working for 24 hours, and every event Hijinks sends carries a v1= for each, so you can redeploy the bridge with the new secret at leisure.

If the old secret leaked, don't wait: after rotating, press Expire the old secret now beside the new one. From then on only the new secret verifies, in both directions, and a bridge still holding the old one gets 401 until you redeploy it.

Last modified on