enterprise/workers/step-resolver/README.md
Cloudflare Workers for Platforms dispatch worker for Step Resolver resolution.
enterprise/workers/step-resolver/
src/index.ts # HTTP routes + dispatch logic
src/auth/hmac.ts # request signature validation
src/utils/worker-id.ts # worker id mapping
wrangler.jsonc # worker + namespace config
This package is part of the pnpm workspace via enterprise/workers/*.
X-Novu-Signature in format t={timestamp},v1={hmac}).sr-${organizationId}-${stepResolverHash}.DISPATCHER binding).x-request-id.GET /health200 with JSON status payload.GET returns 405.POST /resolve/:organizationId/:stepResolverHash/:stepIdRoute validation (strict):
organizationId: lowercase hex, exactly 24 chars ([a-f0-9]{24})stepResolverHash: format sr-xxxxx-xxxxx (e.g., sr-abc12-def34)stepId: one URL path segment ([^/]+)Content-Type: must be application/json1MBAuth headers:
X-Novu-Signature: Signature header in format t={timestamp},v1={hmac}On success, request is forwarded as:
POST/resolve/... pathstep=<decoded stepId>x-novu-signature, authorization, x-internal-authUses the same signature format as @novu/framework Bridge authentication, but with a different secret for different trust boundaries:
NOVU_SECRET_KEY to authenticate Novu Cloud → Customer's Bridge EndpointSTEP_RESOLVER_HMAC_SECRET to authenticate Novu API → Novu's Cloudflare WorkersThis separation ensures customer secrets protect their infrastructure while platform secrets protect Novu's worker infrastructure, without requiring per-customer secret lookups in workers.
Signature format:
X-Novu-Signature: t={timestamp},v1={hmac}
HMAC computed over:
${timestamp}.${rawRequestBody}
Note: The HMAC is computed over the raw request body bytes (UTF-8 decoded string), not a re-serialized JSON object. This ensures canonical validation against the exact bytes received.
Validation notes:
300 seconds (5 minutes)import { createHmac } from 'node:crypto';
const secret = process.env.STEP_RESOLVER_HMAC_SECRET!;
const payload = {
payload: { firstName: 'Ada' },
subscriber: { email: '[email protected]' },
context: {},
steps: {},
};
const timestamp = Date.now();
const bodyString = JSON.stringify(payload);
const data = `${timestamp}.${bodyString}`;
const hmac = createHmac('sha256', secret).update(data).digest('hex');
const signature = `t=${timestamp},v1=${hmac}`;
// Send as headers:
// X-Novu-Signature: t=1234567890,v1=abc123...
// Body: <bodyString> (same string used in HMAC computation)
Install dependencies from repo root:
pnpm install
Run with workspace filter from repo root:
pnpm --filter @novu/step-resolver-worker dev
Or run directly from this folder:
pnpm run dev
For local wrangler dev, provide the secret (for example via .dev.vars):
STEP_RESOLVER_HMAC_SECRET=local-dev-secret
From enterprise/workers/step-resolver:
pnpm run namespace:create:staging
pnpm run namespace:create:production
pnpm run deploy:staging
pnpm run deploy:production
pnpm run secret:staging
pnpm run secret:production
pnpm run deploy:staging
pnpm run deploy:production
If namespace names differ from your Cloudflare account, update wrangler.jsonc.
DISPATCH_URL="https://step-resolver-dispatch-staging.<subdomain>.workers.dev"
ORGANIZATION_ID="696a21b632ef1f83460d584d"
STEP_RESOLVER_HASH="abc12-def34"
STEP_ID="welcome-email"
SECRET="${STEP_RESOLVER_HMAC_SECRET:?set STEP_RESOLVER_HMAC_SECRET}"
PATHNAME="/resolve/${ORGANIZATION_ID}/sr-${STEP_RESOLVER_HASH}/${STEP_ID}"
BODY='{"payload":{"firstName":"Ada"},"subscriber":{"email":"[email protected]"},"context":{},"steps":{}}'
# Create HMAC signature using Framework format
TIMESTAMP="$(node -e 'console.log(Date.now())')"
DATA="${TIMESTAMP}.${BODY}"
HMAC="$(printf '%s' "$DATA" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')"
SIGNATURE="t=${TIMESTAMP},v1=${HMAC}"
curl -i -X POST "${DISPATCH_URL}${PATHNAME}" \
-H "Content-Type: application/json" \
-H "X-Novu-Signature: ${SIGNATURE}" \
-d "$BODY"