Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ export const ChannelPreferencesForm = (props: ConfigureWorkflowFormProps) => {
new_status: checked,
});
}}
disabled={isReadOnly}
/>
</FormControl>
</FormItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export function ChannelPreferences() {
return null;
}

const isReadOnly =
workflow.origin === ResourceOriginEnum.EXTERNAL || currentEnvironment?.type !== EnvironmentTypeEnum.DEV;
const isReadOnly = currentEnvironment?.type !== EnvironmentTypeEnum.DEV;

return <ChannelPreferencesForm workflow={workflow} update={update} isReadOnly={isReadOnly} />;
}
168 changes: 168 additions & 0 deletions enterprise/workers/step-resolver/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Logs

logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Runtime data

pids
_.pid
_.seed
\*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
\*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

\*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

\*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.cache
.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

.cache/

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp
.cache

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

# wrangler project

.dev.vars
.wrangler/
188 changes: 188 additions & 0 deletions enterprise/workers/step-resolver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Step Resolver Dispatch Worker

Cloudflare Workers for Platforms dispatch worker for Step Resolver resolution.

## Repository structure

```text
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/*`.

## What the worker does

- Exposes a public dispatch endpoint for resolving step output.
- Validates HMAC auth header (`X-Novu-Signature` in format `t={timestamp},v1={hmac}`).
- Maps tenant worker id as `sr-${organizationId}-${stepResolverHash}`.
- Dispatches into a Workers for Platforms namespace (`DISPATCHER` binding).
- Preserves downstream response status/body and adds `x-request-id`.

## API contract

### `GET /health`

- Returns `200` with JSON status payload.
- Any method other than `GET` returns `405`.

### `POST /resolve/:organizationId/:stepResolverHash/:stepId`

Route 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/json`
- Body size: max `1MB`

Auth headers:

- `X-Novu-Signature`: Signature header in format `t={timestamp},v1={hmac}`

On success, request is forwarded as:

- method: `POST`
- path: original `/resolve/...` path
- query param: `step=<decoded stepId>`
- stripped headers before forwarding: `x-novu-signature`, `authorization`, `x-internal-auth`

## HMAC signing format

Uses the same signature format as `@novu/framework` Bridge authentication, but with a **different secret** for different trust boundaries:

- **Framework Bridge**: Uses per-customer `NOVU_SECRET_KEY` to authenticate Novu Cloud → Customer's Bridge Endpoint
- **Step Resolver Worker**: Uses platform-level `STEP_RESOLVER_HMAC_SECRET` to authenticate Novu API → Novu's Cloudflare Workers

This 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:

```text
X-Novu-Signature: t={timestamp},v1={hmac}
```

HMAC computed over:

```text
${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:

- allowed clock skew: `300` seconds (5 minutes)
- signature comparison is constant-time
- replay protection is timestamp-window only (no nonce store)

### Node signing example

```ts
import { createHmac } from 'node:crypto';

const secret = process.env.STEP_RESOLVER_HMAC_SECRET!;
const payload = {
payload: { firstName: 'Ada' },
subscriber: { email: 'ada@example.com' },
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)
```

## Local development

Install dependencies from repo root:

```bash
pnpm install
```

Run with workspace filter from repo root:

```bash
pnpm --filter @novu/step-resolver-worker dev
```

Or run directly from this folder:

```bash
pnpm run dev
```

For local `wrangler dev`, provide the secret (for example via `.dev.vars`):

```bash
STEP_RESOLVER_HMAC_SECRET=local-dev-secret
```

## Cloudflare setup and deploy

From `enterprise/workers/step-resolver`:

1. Create dispatch namespaces (one-time):

```bash
pnpm run namespace:create:staging
pnpm run namespace:create:production
```

2. Deploy worker service:

```bash
pnpm run deploy:staging
pnpm run deploy:production
```

3. Set secrets per environment:

```bash
pnpm run secret:staging
pnpm run secret:production
```

4. Deploy updates:

```bash
pnpm run deploy:staging
pnpm run deploy:production
```

If namespace names differ from your Cloudflare account, update `wrangler.jsonc`.

## Curl smoke test

```bash
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":"ada@example.com"},"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"
```
22 changes: 22 additions & 0 deletions enterprise/workers/step-resolver/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@novu/step-resolver-worker",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"cf-typegen": "wrangler types",
"namespace:create:staging": "wrangler dispatch-namespace create novu-step-resolvers-staging",
"namespace:create:production": "wrangler dispatch-namespace create novu-step-resolvers-production",
"secret:staging": "wrangler secret put STEP_RESOLVER_HMAC_SECRET --env staging",
"secret:production": "wrangler secret put STEP_RESOLVER_HMAC_SECRET --env production",
"deploy:staging": "wrangler deploy --env staging",
"deploy:production": "wrangler deploy --env production"
},
"devDependencies": {
"typescript": "^5.5.2",
"wrangler": "^4.49.0"
}
}
Loading
Loading