Skip to content

feat(framework): Add Cloudflare Workers adapter and example#10775

Open
scopsy wants to merge 4 commits intonextfrom
cloudflare-integration
Open

feat(framework): Add Cloudflare Workers adapter and example#10775
scopsy wants to merge 4 commits intonextfrom
cloudflare-integration

Conversation

@scopsy
Copy link
Copy Markdown
Contributor

@scopsy scopsy commented Apr 17, 2026

Introduce Cloudflare Workers support for Novu agents: add a cloudflare adapter (router, withNovuAgent mixin, helpers) and signature validation util. Add NovuConversationRef and AgentContext.serialize for serializable conversation refs, and refactor handler to use validateNovuSignature. Include tests for Cloudflare behavior, helpers, and signature validation. Add docs and a runnable example project (examples/cloudflare-agent) plus a dashboard integration guide component. Update package.json exports/peerDependencies and tsup config to build the new Cloudflare artifacts.

What changed? Why was the change needed?

@coderabbitai summary

Screenshots

Expand for optional sections

Related enterprise PR

Special notes for your reviewer

Introduce Cloudflare Workers support for Novu agents: add a cloudflare adapter (router, withNovuAgent mixin, helpers) and signature validation util. Add NovuConversationRef and AgentContext.serialize for serializable conversation refs, and refactor handler to use validateNovuSignature. Include tests for Cloudflare behavior, helpers, and signature validation. Add docs and a runnable example project (examples/cloudflare-agent) plus a dashboard integration guide component. Update package.json exports/peerDependencies and tsup config to build the new Cloudflare artifacts.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for dashboard-v2-novu-staging failed. Why did it fail? →

Name Link
🔨 Latest commit 0901d61
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69e2400cf62f930009a4cfdc

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: feat(framework): Add Cloudflare Workers adapter and example

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Adds Cloudflare Workers support for Novu Agents: new Cloudflare server implementation (router, Durable Object mixin), signature validation utility, Durable Object helpers and state utilities, message-history conversion, framework exports/packaging, docs, dashboard integration guide component, example Cloudflare app, and corresponding tests.

Changes

Cohort / File(s) Summary
Cloudflare Server Core
packages/framework/src/servers/cloudflare/index.ts, packages/framework/src/servers/cloudflare/router.ts, packages/framework/src/servers/cloudflare/with-novu-agent.ts
New Cloudflare router, request-to-Durable-Object dispatch, signature verification integration, Durable Object mixin with bridge handlers, reply/trigger helper methods, and fallthrough routing.
Cloudflare Utilities & Helpers
packages/framework/src/servers/cloudflare/convert-history.ts, packages/framework/src/servers/cloudflare/helpers.ts, packages/framework/cloudflare/helpers/package.json
Added history→LLM converter, Durable Object state helpers (rememberLastRef, replyToLastConversation), STATE_KEY constant, and helpers package manifest.
Cloudflare Tests
packages/framework/src/servers/cloudflare/cloudflare.test.ts
New Vitest coverage for signature validation, AgentContext.serialize(), withNovuAgent flows, replyFromRef, helper state functions, and toMessageList conversions.
Framework Agent Serialization
packages/framework/src/resources/agent/agent.context.ts, packages/framework/src/resources/agent/agent.types.ts, packages/framework/src/resources/agent/index.ts
Exported serializeContent, added AgentContextImpl.serialize() returning NovuConversationRef, introduced NovuConversationRef type, and re-exported these.
Framework Core Exports & Types
packages/framework/src/index.ts, packages/framework/src/utils/index.ts, packages/framework/src/types/server.types.ts
Re-exported NovuConversationRef, re-exported signature utils, and added 'cloudflare' to SupportedFrameworkName.
Signature Validation Utility
packages/framework/src/utils/signature.utils.ts
New async validateNovuSignature(payload, hmacHeader, secretKey, hmacEnabled) with header parsing, timestamp tolerance, and HMAC comparison; used by router/handler.
Handler Refactor
packages/framework/src/handler.ts
Replaced inlined HMAC parsing/validation with delegated validateNovuSignature call; removed related inline logic and imports.
Framework Packaging & Exports
packages/framework/package.json, packages/framework/tsup.config.ts, packages/framework/cloudflare/package.json
Added ./cloudflare and ./cloudflare/helpers exports and cloudflare build entries; updated tsup entries and added cloudflare package manifests and optional peer deps.
Documentation & Examples
packages/framework/docs/cloudflare-agents.md, examples/cloudflare-agent/*
New Cloudflare agents guide, sample Cloudflare Worker example (index.ts, wrangler.jsonc, package.json, README) demonstrating Durable Object agent, scheduling, and AI integration.
Dashboard Integration Guide
apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx
New React component rendering a step-by-step "Deploy on Cloudflare Workers" guide with templated Worker TypeScript and wrangler.jsonc snippets.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(0,128,255,0.5) Client
    participant Channel as Channel / User
    end
    rect rgba(0,200,83,0.5) Novu
    participant Novu as Novu API
    end
    rect rgba(124,77,255,0.5) Cloudflare
    participant Worker as Cloudflare Worker
    participant DO as Durable Object
    participant LLM as Workers AI
    end

    Channel->>Novu: Send message
    Novu->>Worker: POST AgentBridgeRequest (signed)
    Worker->>Worker: validateNovuSignature(payload, header, secret, strict)
    alt signature invalid
        Worker-->>Novu: 4xx error
    else signature valid
        Worker->>Worker: determine agentId -> DO stub
        Worker->>DO: forward original request to DO stub
        DO->>DO: withNovuAgent._handleNovuBridgeRequest() -> onNovuMessage(ctx)
        DO->>LLM: toMessageList(ctx) -> generate text
        LLM-->>DO: response
        DO->>Novu: POST reply via ref.replyUrl (ApiKey auth)
        DO-->>Worker: complete
        Worker-->>Novu: { status: "ok" }
    end
    Novu->>Channel: Deliver reply
Loading
sequenceDiagram
    rect rgba(255,193,7,0.5) DurableObject
    participant DO as Durable Object
    participant State as DO State
    end
    rect rgba(233,30,99,0.5) Scheduler / Novu
    participant Scheduler as Cloudflare Scheduler
    participant Novu as Novu API
    end

    DO->>State: rememberLastRef(ctx.serialize())
    State-->>DO: stored ref
    DO->>Scheduler: this.schedule(delay, 'sendReminder', ctx.serialize())
    Note over Scheduler: timer fires later
    Scheduler->>DO: invoke scheduled action with ref
    DO->>Novu: replyFromRef(ref, content) (POST with ApiKey)
    Novu-->>DO: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format with valid type 'feat' and scope 'framework', has lowercase imperative description, and clearly summarizes the main changes in the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/framework/src/resources/agent/agent.types.ts (1)

127-148: ⚠️ Potential issue | 🟠 Major

Adding a required serialize() to the public AgentContext is a breaking change for external implementers.

AgentContext is an exported interface in @novu/framework. Any consumer that implements it directly (for tests, mocks, adapters) will break when upgrading to a version that requires serialize(). Current package version is 2.10.1-alpha.1, i.e. a pre-release; just ensure the stable bump carries the major-version implication, or make the method optional until the next major.

As per coding guidelines: "Treat all exported symbols in packages as public API; follow semver conventions with breaking changes requiring major bumps, new exports as minor versions, and fixes as patches".

🔧 Non-breaking alternative
-  /** Return a JSON-safe ref that can be passed to schedule payloads, setState, etc. */
-  serialize(): NovuConversationRef;
+  /** Return a JSON-safe ref that can be passed to schedule payloads, setState, etc. */
+  serialize?(): NovuConversationRef;

…then narrow it to required at the next major bump.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/resources/agent/agent.types.ts` around lines 127 -
148, The new required serialize() method on the exported AgentContext interface
is a breaking API change; make serialize optional to preserve backward
compatibility (change AgentContext.serialize(): NovuConversationRef to
serialize?(): NovuConversationRef) so external implementers/mocks won't break,
and keep the method required only in the next major release; update references
in code that assume serialize exists to handle its possible absence (e.g., in
functions using AgentContext.serialize, guard or fallback) and ensure the symbol
name AgentContext and method serialize are the ones modified.
packages/framework/package.json (1)

50-67: ⚠️ Potential issue | 🟡 Minor

durable-objects keyword added without a corresponding export/files entry.

The keyword suggests a ./durable-objects subpath, but the only new exports are ./cloudflare and ./cloudflare/helpers, and there's no durable-objects folder in files. Either drop the keyword or plan the export in the same release so documentation/discoverability doesn't promise something that isn't shipped. Low priority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/package.json` around lines 50 - 67, The package.json
contains a "durable-objects" keyword but no matching export or files entry;
either remove "durable-objects" from the "keywords" array or add the shipped
path and export for it: create the durable-objects folder, add the public
entrypoint (e.g., ./durable-objects) and a corresponding export entry alongside
the existing "./cloudflare" and "./cloudflare/helpers" exports, and include that
folder in the package "files" list so the keyword accurately reflects an
exported subpath.
🧹 Nitpick comments (9)
packages/framework/src/handler.ts (1)

336-338: Refactor LGTM — consider inlining at call site.

Delegation to validateNovuSignature is clean. Since validateHmac is now a one-line passthrough with only one caller (line 143), you could eliminate the indirection entirely and call validateNovuSignature directly from handleAction, matching the pattern already used in the Cloudflare router. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/handler.ts` around lines 336 - 338, The validateHmac
method is a one-line passthrough to validateNovuSignature and has a single
caller; remove the validateHmac method and change its caller in handleAction to
call validateNovuSignature(payload, hmacHeader, this.client.secretKey,
this.hmacEnabled) directly (preserving the same arguments and behavior), then
delete the now-unused validateHmac definition to eliminate the indirection.
examples/cloudflare-agent/wrangler.jsonc (1)

1-22: Consider renaming the Durable Object binding to avoid shadowing the class.

The binding name: "WineBot" shadows the exported WineBot class inside the Worker's runtime env (env.WineBotDurableObjectNamespace, not the class). Cloudflare convention is to use SCREAMING_SNAKE_CASE for bindings (e.g. WINE_BOT) to make it obvious at the call site that you're dereferencing a binding vs. a class. Non-blocking; the current setup works since access is always via env.WineBot.

Also, since NOVU_SECRET_KEY is declared in the Env interface (examples/cloudflare-agent/src/index.ts:7-11), consider documenting that it must be provided via wrangler secret put NOVU_SECRET_KEY — there's no vars entry here, which is correct for secrets, but new users may miss it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cloudflare-agent/wrangler.jsonc` around lines 1 - 22, The Durable
Object binding name "WineBot" shadows the exported class WineBot; rename the
binding to a SCREAMING_SNAKE_CASE identifier (e.g. WINE_BOT) in the
durable_objects bindings and update any runtime references from env.WineBot to
env.WINE_BOT (keep the exported class name WineBot and
migrations/new_sqlite_classes as "WineBot"). Also add a short note (or README
line) that NOVU_SECRET_KEY declared on the Env interface must be provisioned as
a secret via wrangler secret put NOVU_SECRET_KEY so new users know to set it.
packages/framework/src/servers/cloudflare/convert-history.ts (1)

38-48: Consider a satisfies/switch-based mapping for clarity and future-proofing.

Minor: as the role set grows (e.g. tool, function, developer), an if-chain that defaults everything unknown to 'user' can silently mis-route new roles. A switch with an explicit default, or a lookup table, makes the fallback intentional.

function mapHistoryEntry(entry: AgentHistoryEntry): LLMMessage {
  switch (entry.role) {
    case 'agent':
    case 'assistant':
      return { role: 'assistant', content: entry.content };
    case 'system':
      return { role: 'system', content: entry.content };
    default:
      return { role: 'user', content: entry.content };
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/convert-history.ts` around lines 38
- 48, The current mapHistoryEntry function uses an if-chain that silently
defaults unknown AgentHistoryEntry.role values to 'user'; change it to an
explicit switch or a role->LLMMessage lookup so new roles won't be
mis-routed—update the mapHistoryEntry implementation to switch on entry.role
(handling 'agent' and 'assistant' -> assistant, 'system' -> system) and have an
explicit default branch for 'user' (or throw/log for unrecognized roles) so the
fallback is intentional and easier to extend.
examples/cloudflare-agent/src/index.ts (2)

29-34: Non-escalate actions silently no-op.

When ctx.action?.actionId !== 'escalate', the handler returns without acknowledging or logging anything. That's fine for a demo, but consider a brief comment explaining the deliberate drop (or a default reply) so users don't copy this as "action handler must handle only the happy path".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cloudflare-agent/src/index.ts` around lines 29 - 34, The
onNovuAction handler only handles the 'escalate' branch and silently returns for
all other ctx.action?.actionId values; update onNovuAction to either add a brief
comment explaining the intentional early return for non-escalate actions or
implement a default acknowledgement/log (e.g., using ctx.log or
ctx.resolve/ctx.reject) so callers aren’t left hanging — locate the onNovuAction
function and adjust the non-escalate path to include that explicit comment or a
default response.

5-16: Hoist the AgentContext type import to the top of the file.

Using import('@novu/framework').AgentContext inline at every method signature is noisy, and the top-level import already pulls from @novu/framework/cloudflare. Adding AgentContext to a top-level import type { ... } makes the example read cleaner for users copying it as a starting point.

♻️ Suggested change
-import { withNovuAgent, createNovuRouter, toMessageList, type NovuConversationRef } from '@novu/framework/cloudflare';
+import { withNovuAgent, createNovuRouter, toMessageList, type NovuConversationRef } from '@novu/framework/cloudflare';
+import type { AgentContext } from '@novu/framework';
@@
-  async onNovuMessage(ctx: import('@novu/framework').AgentContext) {
+  async onNovuMessage(ctx: AgentContext) {
@@
-  async onNovuAction(ctx: import('@novu/framework').AgentContext) {
+  async onNovuAction(ctx: AgentContext) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cloudflare-agent/src/index.ts` around lines 5 - 16, Hoist the inline
AgentContext type by adding a top-level type import (e.g. import type {
AgentContext } from '@novu/framework';) and update the WineBot.onNovuMessage
signature to use the imported AgentContext instead of the inline
import('@novu/framework').AgentContext; this keeps the class declaration (class
WineBot extends withNovuAgent(AIChatAgent)<Env>) and the onNovuMessage method
consistent and cleaner for copy-paste.
packages/framework/src/servers/cloudflare/router.ts (1)

146-151: (err as any)?.statusCode is unsound if err is a thrown primitive.

err instanceof Error already narrows for the message path on Line 147; please prefer the same guard here instead of any. Also consider clamping status to a valid range (e.g. 400–599) before returning so a non-numeric statusCode on a custom error doesn't produce an invalid Response status.

♻️ Proposed fix
   } catch (err) {
-    const message = err instanceof Error ? err.message : 'Internal error';
-    const status = (err as any)?.statusCode ?? 500;
+    const message = err instanceof Error ? err.message : 'Internal error';
+    const rawStatus =
+      err && typeof err === 'object' && 'statusCode' in err ? (err as { statusCode: unknown }).statusCode : undefined;
+    const status = typeof rawStatus === 'number' && rawStatus >= 400 && rawStatus < 600 ? rawStatus : 500;

     return Response.json({ error: message }, { status });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/router.ts` around lines 146 - 151,
The catch block currently reads status from (err as any)?.statusCode which is
unsafe for thrown primitives; update the handler to first check err instanceof
Error and, if true, extract a numeric statusCode (e.g., const raw = (err as
any).statusCode), coerce/validate it to a number, and clamp it into the 400–599
range before using it; otherwise default status to 500. Ensure the message still
uses err instanceof Error to get err.message and return Response.json({ error:
message }, { status }) with the validated/clamped status.
packages/framework/src/servers/cloudflare/helpers.ts (1)

45-74: Consider deduplicating with replyFromRef in the mixin.

This function is almost byte-for-byte identical to NovuAgentMixin.replyFromRef in with-novu-agent.ts (same payload shape, headers, error handling). Extracting a small internal postReply(replyUrl, secretKey, content) helper and reusing it from both places would keep auth/error semantics in lockstep if the wire format ever evolves.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/helpers.ts` around lines 45 - 74,
The replyToLastConversation implementation duplicates
NovuAgentMixin.replyFromRef; extract a shared helper (e.g., postReply(replyUrl:
string, secretKey: string, content: MessageContent)) that performs the POST with
the same body shape, headers (Authorization: `ApiKey ${secretKey}`), response.ok
checking and error text extraction, then replace the bodies of
replyToLastConversation and NovuAgentMixin.replyFromRef to call
postReply(ref.replyUrl, secretKey, content) after computing
ref/secretKey/serialized content; keep function names replyToLastConversation
and NovuAgentMixin.replyFromRef unchanged and ensure the helper serializes
content the same way (serializeContent) and throws the same styled Error on
non-OK responses.
packages/framework/src/servers/cloudflare/with-novu-agent.ts (2)

89-114: Duplication with helpers.replyToLastConversation.

This body is the same POST shape as replyToLastConversation in helpers.ts (same headers, same AgentReplyPayload construction, same error message pattern). Extract a private postAgentReply(replyUrl, secretKey, content) in resources/agent (or a local util) and reuse it from both — a single place to evolve auth headers, retry, and error surfaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/with-novu-agent.ts` around lines 89
- 114, Extract the duplicated POST logic into a single utility function named
postAgentReply(replyUrl: string, secretKey: string, body: AgentReplyPayload)
placed under resources/agent (or a local util), and replace the inlined fetch in
with-novu-agent.ts.replyFromRef and helpers.replyToLastConversation with calls
to this helper; ensure postAgentReply constructs the same Authorization and
Content-Type headers, stringifies the body, preserves the existing error
handling (reads response.text() on non-ok and throws Error with `replyFromRef
failed (${response.status}): ${text}`-style message or a generic message if
called from helpers), and export/import the helper so both replyFromRef and
helpers.replyToLastConversation delegate to it.

160-170: Unknown bridge events silently succeed.

If event is anything other than the four known AgentEventEnum values (typo, future event Novu starts sending, misconfigured query string), handler is undefined, the hook is skipped, ctx.flush() runs, and the request gets a 200 {"status":"ok"}. That makes diagnosis painful — the Novu side thinks everything is fine while nothing ran.

At minimum, console.warn with the unknown event; better, return 400 so the backend can surface it.

♻️ Suggested change
-        const handler = handlerMap[event as AgentEventEnum];
-        if (handler) {
-          await handler(ctx);
-        }
+        const handler = handlerMap[event as AgentEventEnum];
+        if (!handler) {
+          console.warn(`[novu-agent] Unknown bridge event "${event}" — no hook invoked`);
+          return Response.json({ error: `Unknown event: ${event}` }, { status: 400 });
+        }
+        await handler(ctx);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/with-novu-agent.ts` around lines
160 - 170, The code currently ignores unknown AgentEventEnum values (handler
undefined) and continues to ctx.flush(), returning 200; change the logic after
computing handler (from handlerMap and AgentEventEnum) to detect when handler is
undefined, log a warning including the received event string, and return a 400
response immediately (do not call ctx.flush or any handler). Update the branch
around handler (and the symbols this.onNovuMessage / onNovuAction /
onNovuReaction / onNovuResolve) so unknown events produce a clear console.warn
and an HTTP 400 with a short JSON error body indicating "unknown event".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx`:
- Around line 36-37: The worker snippet interpolation in
CloudflareAgentHostingGuide currently injects agentIdentifier directly into
WORKER_SNIPPET which breaks when the ID contains single quotes, backslashes, or
newlines; update the replacement to safely serialize the value (e.g., use
JSON.stringify(agentIdentifier) or otherwise escape it) when constructing
workerCode so the substituted value is valid JavaScript literal; adjust the call
in CloudflareAgentHostingGuide that does WORKER_SNIPPET.replace('{{AGENT_ID}}',
agentIdentifier) to replace with the JSON-stringified/escaped string instead.
- Around line 39-91: Wrap the guide JSX inside AgentIntegrationGuideLayout
(matching other guides) and update AgentIntegrationGuideStep so its description
prop accepts React.ReactNode instead of string; then replace the raw <pre>
blocks for workerCode and WRANGLER_SNIPPET with CopyButton-wrapped pre elements
(use the existing CopyButton component) so code snippets get copy affordances
and types align (update the prop type in AgentIntegrationGuideStep and any
usages).

In `@examples/cloudflare-agent/package.json`:
- Around line 9-19: Update package.json dependencies to avoid "latest": replace
each "latest" for "@cloudflare/ai-chat", "agents", "ai", and
"workers-ai-provider" with pinned, tested caret ranges (e.g., "^X.Y.Z") you
verified; change "@novu/framework": "latest" to "@novu/framework": "workspace:*"
so it resolves to the local workspace; keep devDependencies pinned as well if
needed and ensure versions comply with minimumReleaseAge policy.

In `@packages/framework/package.json`:
- Around line 240-241: The peerDependency ranges for the Cloudflare 0.x packages
are too loose; change the entries for "agents" and "@cloudflare/ai-chat" in
package.json from open-ended ">=0.1.0" to caret-capped ranges that pin to the
tested minors (e.g. "agents": "^0.11.0" and "@cloudflare/ai-chat": "^0.4.0" or
whichever exact versions you validated) so npm won't pull incompatible 1.x or
untested 0.x minors; update those two dependency values and run npm/pnpm/yarn
install and tests to confirm compatibility.

In `@packages/framework/src/servers/cloudflare/cloudflare.test.ts`:
- Around line 256-275: Test fake setState currently merges into storedState;
change the fake to mimic Cloudflare Agents' replace semantics by making setState
replace the whole storedState (e.g., assign storedState = patch and update
this.state to that new object). Also seed storedState with an unrelated key like
other: 'keep-me' before calling rememberLastRef and add an assertion that other
survives after calling rememberLastRef so the test will catch regressions unless
rememberLastRef correctly spreads agent.state; reference fakeAgent.setState,
storedState, and rememberLastRef to locate where to change and add the new
assertion.
- Around line 85-90: Update both the test and the parser: change the test helper
buildSignatureHeader to emit the production format "t=<timestamp>,v1=<hash>"
instead of "<timestamp>=<timestamp>,v1=<hash>" and modify the signature parser
function (the function that currently splits timestampPart by '=' and assigns to
timestamp) to correctly extract the numeric timestamp by checking the key (e.g.,
if the left side is "t" use the right side as the timestamp), fall back to the
existing behavior for legacy formats if needed, and ensure you convert that
right-side string to Number before performing the expiry comparison so NaN
cannot bypass validation.

In `@packages/framework/src/servers/cloudflare/helpers.ts`:
- Around line 27-39: The rememberLastRef function currently calls agent.setState
with only {[STATE_KEY]: ctx.serialize()}, which on Cloudflare Agents SDK
replaces the entire state and will wipe other fields; update rememberLastRef to
merge the existing agent.state with the new key (e.g., call setState with an
object that spreads agent.state and then sets STATE_KEY to ctx.serialize()), and
update the StatefulAgent.setState param name (avoid "patch")/docs to reflect it
replaces the whole state so callers understand behavior; also adjust or add
tests to use a fake setState that simulates replacement semantics rather than
Object.assign merging.

In `@packages/framework/src/servers/cloudflare/router.ts`:
- Around line 68-95: The Durable Object currently trusts that HMAC validation
performed in the Worker router was sufficient, which is brittle; update the DO's
entry handler _handleNovuBridgeRequest to enforce verification as well by either
calling validateNovuSignature on incoming agent-event POSTs or by requiring an
internal header (e.g. x-novu-router-verified: 1) that the router sets before
forwarding; specifically modify the DO to reject requests missing the internal
header or failing validateNovuSignature and ensure any router code that forwards
requests for action=agent-event sets the chosen header when it has performed
validation.
- Around line 105-114: The signature verification is using the parsed body
object instead of the raw request bytes, which can cause HMAC mismatches; change
the call that currently passes body to validateNovuSignature to pass the raw
bodyText (from request.text()) and only JSON.parse after successful
validateNovuSignature, or update validateNovuSignature to accept a string
payload (string | object) and prefer the raw string for HMAC; refer to bodyText,
body, validateNovuSignature, and request.text() when making this change.

In `@packages/framework/src/servers/cloudflare/with-novu-agent.ts`:
- Around line 44-46: The mixin return type for withNovuAgent incorrectly omits
the instance API, causing subclasses (e.g., WineBot extends
withNovuAgent(AIChatAgent)) to lack NovuAgentInstance methods like replyFromRef,
triggerWorkflow, and onNovuMessage; update the factory's declared return type
(the cast returned at the end that currently uses TBase & NovuAgentStatics) to
include NovuAgentInstance on the instance side (e.g., (TBase & NovuAgentStatics)
& Constructor<NovuAgentInstance> or equivalent) so that instances inherit the
NovuAgentInstance methods; locate withNovuAgent, NovuAgentInstance, and
NovuAgentStatics in the file and change the exported mixin's typed return to
incorporate NovuAgentInstance, then run tsc --noEmit in the package and example
to verify.

In `@packages/framework/src/utils/signature.utils.ts`:
- Around line 37-42: In validateNovuSignature fix three issues: correctly parse
the timestamp/value by changing the destructuring of timestampPart to capture
the key and value (use timestampPart.split('=') -> [key, timestampPayload] and
base expiry check on Number(timestampPayload)), correct
SIGNATURE_TIMESTAMP_TOLERANCE to be minutes→ms (multiply minutes by 60 * 1000,
not 5), and validate the signature version by extracting the version from
signaturePart (e.g., split signaturePart into [version, signaturePayload] or
check that signaturePart.startsWith('v1=')) and throw
SignatureVersionInvalidError if the version is not 'v1'; ensure HMAC still uses
timestampPayload for the hash computation.

---

Outside diff comments:
In `@packages/framework/package.json`:
- Around line 50-67: The package.json contains a "durable-objects" keyword but
no matching export or files entry; either remove "durable-objects" from the
"keywords" array or add the shipped path and export for it: create the
durable-objects folder, add the public entrypoint (e.g., ./durable-objects) and
a corresponding export entry alongside the existing "./cloudflare" and
"./cloudflare/helpers" exports, and include that folder in the package "files"
list so the keyword accurately reflects an exported subpath.

In `@packages/framework/src/resources/agent/agent.types.ts`:
- Around line 127-148: The new required serialize() method on the exported
AgentContext interface is a breaking API change; make serialize optional to
preserve backward compatibility (change AgentContext.serialize():
NovuConversationRef to serialize?(): NovuConversationRef) so external
implementers/mocks won't break, and keep the method required only in the next
major release; update references in code that assume serialize exists to handle
its possible absence (e.g., in functions using AgentContext.serialize, guard or
fallback) and ensure the symbol name AgentContext and method serialize are the
ones modified.

---

Nitpick comments:
In `@examples/cloudflare-agent/src/index.ts`:
- Around line 29-34: The onNovuAction handler only handles the 'escalate' branch
and silently returns for all other ctx.action?.actionId values; update
onNovuAction to either add a brief comment explaining the intentional early
return for non-escalate actions or implement a default acknowledgement/log
(e.g., using ctx.log or ctx.resolve/ctx.reject) so callers aren’t left hanging —
locate the onNovuAction function and adjust the non-escalate path to include
that explicit comment or a default response.
- Around line 5-16: Hoist the inline AgentContext type by adding a top-level
type import (e.g. import type { AgentContext } from '@novu/framework';) and
update the WineBot.onNovuMessage signature to use the imported AgentContext
instead of the inline import('@novu/framework').AgentContext; this keeps the
class declaration (class WineBot extends withNovuAgent(AIChatAgent)<Env>) and
the onNovuMessage method consistent and cleaner for copy-paste.

In `@examples/cloudflare-agent/wrangler.jsonc`:
- Around line 1-22: The Durable Object binding name "WineBot" shadows the
exported class WineBot; rename the binding to a SCREAMING_SNAKE_CASE identifier
(e.g. WINE_BOT) in the durable_objects bindings and update any runtime
references from env.WineBot to env.WINE_BOT (keep the exported class name
WineBot and migrations/new_sqlite_classes as "WineBot"). Also add a short note
(or README line) that NOVU_SECRET_KEY declared on the Env interface must be
provisioned as a secret via wrangler secret put NOVU_SECRET_KEY so new users
know to set it.

In `@packages/framework/src/handler.ts`:
- Around line 336-338: The validateHmac method is a one-line passthrough to
validateNovuSignature and has a single caller; remove the validateHmac method
and change its caller in handleAction to call validateNovuSignature(payload,
hmacHeader, this.client.secretKey, this.hmacEnabled) directly (preserving the
same arguments and behavior), then delete the now-unused validateHmac definition
to eliminate the indirection.

In `@packages/framework/src/servers/cloudflare/convert-history.ts`:
- Around line 38-48: The current mapHistoryEntry function uses an if-chain that
silently defaults unknown AgentHistoryEntry.role values to 'user'; change it to
an explicit switch or a role->LLMMessage lookup so new roles won't be
mis-routed—update the mapHistoryEntry implementation to switch on entry.role
(handling 'agent' and 'assistant' -> assistant, 'system' -> system) and have an
explicit default branch for 'user' (or throw/log for unrecognized roles) so the
fallback is intentional and easier to extend.

In `@packages/framework/src/servers/cloudflare/helpers.ts`:
- Around line 45-74: The replyToLastConversation implementation duplicates
NovuAgentMixin.replyFromRef; extract a shared helper (e.g., postReply(replyUrl:
string, secretKey: string, content: MessageContent)) that performs the POST with
the same body shape, headers (Authorization: `ApiKey ${secretKey}`), response.ok
checking and error text extraction, then replace the bodies of
replyToLastConversation and NovuAgentMixin.replyFromRef to call
postReply(ref.replyUrl, secretKey, content) after computing
ref/secretKey/serialized content; keep function names replyToLastConversation
and NovuAgentMixin.replyFromRef unchanged and ensure the helper serializes
content the same way (serializeContent) and throws the same styled Error on
non-OK responses.

In `@packages/framework/src/servers/cloudflare/router.ts`:
- Around line 146-151: The catch block currently reads status from (err as
any)?.statusCode which is unsafe for thrown primitives; update the handler to
first check err instanceof Error and, if true, extract a numeric statusCode
(e.g., const raw = (err as any).statusCode), coerce/validate it to a number, and
clamp it into the 400–599 range before using it; otherwise default status to
500. Ensure the message still uses err instanceof Error to get err.message and
return Response.json({ error: message }, { status }) with the validated/clamped
status.

In `@packages/framework/src/servers/cloudflare/with-novu-agent.ts`:
- Around line 89-114: Extract the duplicated POST logic into a single utility
function named postAgentReply(replyUrl: string, secretKey: string, body:
AgentReplyPayload) placed under resources/agent (or a local util), and replace
the inlined fetch in with-novu-agent.ts.replyFromRef and
helpers.replyToLastConversation with calls to this helper; ensure postAgentReply
constructs the same Authorization and Content-Type headers, stringifies the
body, preserves the existing error handling (reads response.text() on non-ok and
throws Error with `replyFromRef failed (${response.status}): ${text}`-style
message or a generic message if called from helpers), and export/import the
helper so both replyFromRef and helpers.replyToLastConversation delegate to it.
- Around line 160-170: The code currently ignores unknown AgentEventEnum values
(handler undefined) and continues to ctx.flush(), returning 200; change the
logic after computing handler (from handlerMap and AgentEventEnum) to detect
when handler is undefined, log a warning including the received event string,
and return a 400 response immediately (do not call ctx.flush or any handler).
Update the branch around handler (and the symbols this.onNovuMessage /
onNovuAction / onNovuReaction / onNovuResolve) so unknown events produce a clear
console.warn and an HTTP 400 with a short JSON error body indicating "unknown
event".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6adbe3ed-aaa0-4f84-b098-a1284ab10c9e

📥 Commits

Reviewing files that changed from the base of the PR and between cee52b8 and 17e2602.

📒 Files selected for processing (22)
  • apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx
  • examples/cloudflare-agent/README.md
  • examples/cloudflare-agent/package.json
  • examples/cloudflare-agent/src/index.ts
  • examples/cloudflare-agent/wrangler.jsonc
  • packages/framework/docs/cloudflare-agents.md
  • packages/framework/package.json
  • packages/framework/src/handler.ts
  • packages/framework/src/index.ts
  • packages/framework/src/resources/agent/agent.context.ts
  • packages/framework/src/resources/agent/agent.types.ts
  • packages/framework/src/resources/agent/index.ts
  • packages/framework/src/servers/cloudflare/cloudflare.test.ts
  • packages/framework/src/servers/cloudflare/convert-history.ts
  • packages/framework/src/servers/cloudflare/helpers.ts
  • packages/framework/src/servers/cloudflare/index.ts
  • packages/framework/src/servers/cloudflare/router.ts
  • packages/framework/src/servers/cloudflare/with-novu-agent.ts
  • packages/framework/src/types/server.types.ts
  • packages/framework/src/utils/index.ts
  • packages/framework/src/utils/signature.utils.ts
  • packages/framework/tsup.config.ts

Comment on lines +36 to +37
export function CloudflareAgentHostingGuide({ agentIdentifier }: CloudflareAgentHostingGuideProps) {
const workerCode = WORKER_SNIPPET.replace('{{AGENT_ID}}', agentIdentifier);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

agentIdentifier is substituted into a single-quoted JS literal without escaping.

If agentIdentifier contains ', \, or line breaks, the generated workerCode snippet becomes invalid JS that a user would paste straight into their Worker. Probably rare for real agent IDs, but since this is rendered as copy-pasteable code it's worth hardening — either validate upstream, JSON-stringify the value, or switch the template to use JSON.stringify(agentIdentifier).

🔧 Suggested diff
-  static novuAgentId = '{{AGENT_ID}}';
+  static novuAgentId = {{AGENT_ID_LITERAL}};
-  const workerCode = WORKER_SNIPPET.replace('{{AGENT_ID}}', agentIdentifier);
+  const workerCode = WORKER_SNIPPET.replace('{{AGENT_ID_LITERAL}}', JSON.stringify(agentIdentifier));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx`
around lines 36 - 37, The worker snippet interpolation in
CloudflareAgentHostingGuide currently injects agentIdentifier directly into
WORKER_SNIPPET which breaks when the ID contains single quotes, backslashes, or
newlines; update the replacement to safely serialize the value (e.g., use
JSON.stringify(agentIdentifier) or otherwise escape it) when constructing
workerCode so the substituted value is valid JavaScript literal; adjust the call
in CloudflareAgentHostingGuide that does WORKER_SNIPPET.replace('{{AGENT_ID}}',
agentIdentifier) to replace with the JSON-stringified/escaped string instead.

Comment on lines +39 to +91
return (
<div className="flex flex-col gap-4">
<AgentIntegrationGuideSection title="Deploy on Cloudflare Workers">
<p>
Use <code className="font-code text-[12px]">@novu/framework/cloudflare</code> to run your agent as a
Cloudflare Durable Object with built-in per-conversation state, scheduling, and multi-channel delivery through
Novu.
</p>
</AgentIntegrationGuideSection>

<div className="flex flex-col gap-3">
<p className="text-text-strong text-label-sm font-medium">Steps</p>

<AgentIntegrationGuideStep
step={1}
title="Install dependencies"
description="npm install @novu/framework agents @cloudflare/ai-chat"
/>

<AgentIntegrationGuideStep
step={2}
title="Create your Worker"
description={
<pre className="bg-bg-weak mt-1 overflow-x-auto rounded-md p-3 font-mono text-xs leading-relaxed">
{workerCode}
</pre>
}
/>

<AgentIntegrationGuideStep
step={3}
title="Add DO bindings to wrangler.jsonc"
description={
<pre className="bg-bg-weak mt-1 overflow-x-auto rounded-md p-3 font-mono text-xs leading-relaxed">
{WRANGLER_SNIPPET}
</pre>
}
/>

<AgentIntegrationGuideStep
step={4}
title="Set the Novu secret and deploy"
description="wrangler secret put NOVU_SECRET_KEY && wrangler deploy"
/>

<AgentIntegrationGuideStep
step={5}
title="Set the Bridge URL"
description="In the Novu dashboard, paste your Worker URL (https://<worker>.workers.dev/) as the agent's Bridge URL."
/>
</div>
</div>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether sibling integration guides use a shared "copy code" component.
fd -t f 'agent-integration-guide' apps/dashboard/src/components/agents
rg -nP 'copy|Copy|clipboard' apps/dashboard/src/components/agents/agent-integration-guides

Repository: novuhq/novu

Length of output: 1639


🏁 Script executed:

#!/bin/bash
# Examine the Cloudflare guide's imports and structure
head -40 apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx

# Check how other guides with code snippets handle copying (Slack example)
grep -n "pre\|<pre\|Copy\|copy" apps/dashboard/src/components/agents/agent-integration-guides/slack-agent-integration-guide.tsx | head -20

# Check AgentIntegrationGuideStep component
cat -n apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-step.tsx

# Check the CopyButton implementation
cat -n apps/dashboard/src/components/primitives/copy-button.tsx | head -50

Repository: novuhq/novu

Length of output: 3846


🏁 Script executed:

#!/bin/bash
# Get full AgentIntegrationGuideStep file to check complete type definition
cat apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-step.tsx

# Check what other guides with code snippets do (search for <pre in all guides)
rg -A 5 '<pre' apps/dashboard/src/components/agents/agent-integration-guides/

# Check slack guide more thoroughly for code block patterns
cat apps/dashboard/src/components/agents/agent-integration-guides/slack-agent-integration-guide.tsx

Repository: novuhq/novu

Length of output: 5056


🏁 Script executed:

#!/bin/bash
# Check full CloudflareAgentHostingGuide file for imports and any type assertions
wc -l apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx
cat apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx

# Check if other guides use AgentIntegrationGuideLayout wrapper
grep -l "AgentIntegrationGuideLayout" apps/dashboard/src/components/agents/agent-integration-guides/*.tsx

Repository: novuhq/novu

Length of output: 3577


Restructure to align with established pattern: use AgentIntegrationGuideLayout wrapper and update AgentIntegrationGuideStep to accept React elements.

Unlike other integration guides (slack, teams, generic, whatsapp), this guide does not use AgentIntegrationGuideLayout, which provides the infrastructure for copy buttons. Additionally, passing JSX <pre> elements to the description prop (typed as string) violates the component contract.

Either wrap the entire guide in AgentIntegrationGuideLayout to match the established pattern, or change AgentIntegrationGuideStep's description prop to accept React.ReactNode and add <CopyButton> wrapping each <pre> block. The former aligns with how other guides structure code snippets and gives copy affordances consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/components/agents/agent-integration-guides/cloudflare-agent-hosting-guide.tsx`
around lines 39 - 91, Wrap the guide JSX inside AgentIntegrationGuideLayout
(matching other guides) and update AgentIntegrationGuideStep so its description
prop accepts React.ReactNode instead of string; then replace the raw <pre>
blocks for workerCode and WRANGLER_SNIPPET with CopyButton-wrapped pre elements
(use the existing CopyButton component) so code snippets get copy affordances
and types align (update the prop type in AgentIntegrationGuideStep and any
usages).

Comment on lines +9 to +19
"dependencies": {
"@cloudflare/ai-chat": "latest",
"@novu/framework": "latest",
"agents": "latest",
"ai": "latest",
"workers-ai-provider": "latest"
},
"devDependencies": {
"wrangler": "latest",
"typescript": "^5.5.0"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Latest stable versions of @cloudflare/ai-chat, agents, ai (Vercel), workers-ai-provider, and wrangler npm packages

💡 Result:

Latest stable versions (as of 2026-04-17): - @cloudflare/ai-chat: 0.4.2 - agents: 0.11.0 - ai (Vercel): 6.0.167 - workers-ai-provider: 3.1.11 - wrangler: 4.83.0

Citations:


Pin dependency versions instead of "latest".

Using "latest" makes this example non-reproducible and violates the coding guideline requiring adherence to minimumReleaseAge"latest" bypasses that control entirely. Additionally, in a monorepo context, @novu/framework should resolve to the local workspace using workspace:* rather than the npm tag.

Replace each "latest" with a pinned version (e.g., caret ranges like ^X.Y.Z) that you've tested against. For @novu/framework, use workspace:* to reference the local package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cloudflare-agent/package.json` around lines 9 - 19, Update
package.json dependencies to avoid "latest": replace each "latest" for
"@cloudflare/ai-chat", "agents", "ai", and "workers-ai-provider" with pinned,
tested caret ranges (e.g., "^X.Y.Z") you verified; change "@novu/framework":
"latest" to "@novu/framework": "workspace:*" so it resolves to the local
workspace; keep devDependencies pinned as well if needed and ensure versions
comply with minimumReleaseAge policy.

Comment on lines +240 to +241
"agents": ">=0.1.0",
"@cloudflare/ai-chat": ">=0.1.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Current major version of @cloudflare/ai-chat and agents npm packages compatibility

💡 Result:

The current major version of @cloudflare/ai-chat is 0.4 (latest: 0.4.2, published Apr 11, 2026). The relevant "agents" npm package (not @cloudflare/agents, which is deprecated) is at major version 0.11 (latest: 0.11.0, published Apr 14, 2026). They are fully compatible. Official installation commands in Cloudflare docs and package READMEs specify npm install @cloudflare/ai-chat agents ai, confirming interoperability. @cloudflare/ai-chat lists "agents" as a peerDependency (historically ^0.3.10 || ^0.4.0, broadened over time to support newer releases). AIChatAgent extends Agent from the "agents" package. Both packages share the same GitHub repo (cloudflare/agents) and are actively maintained together, with recent releases in April 2026.

Citations:


🏁 Script executed:

# First, let's check the actual content of packages/framework/package.json around lines 240-241
cat -n packages/framework/package.json | sed -n '235,250p'

Repository: novuhq/novu

Length of output: 553


🏁 Script executed:

# Check for pnpm-release-age or similar rules
find .cursor -name "*.mdc" -o -name "*.md" | head -20

Repository: novuhq/novu

Length of output: 693


🏁 Script executed:

# Search for minimumReleaseAge configuration
rg "minimumReleaseAge" -A 5 -B 2

Repository: novuhq/novu

Length of output: 231


🌐 Web query:

agents npm package v0.7.0 release date cloudflare

💡 Result:

The Cloudflare "agents" npm package version 0.7.0 was released in March 2026. The official Cloudflare changelog post for Agents SDK v0.7.0 is dated March 2, 2026, announcing the release with features like observability rewrite, keepAlive, and waitForMcpConnections. The npm version history groups 0.7.0 - 0.7.9 as "Mar 2026". GitHub release tracking via newreleases.io also timestamps agents@0.7.0 around March 2, 2026 (22 days ago from a later context). No exact day-of-month for the npm publish is stated beyond the changelog date, but March 2, 2026, is the authoritative announcement date from Cloudflare Developers.

Citations:


🌐 Web query:

agents npm package v0.11.0 release date cloudflare

💡 Result:

The agents npm package (published by Cloudflare Inc., described as "A home for your AI agents") version 0.11.0 was released on April 14, 2026.

Citations:


Tighten peer-dependency ranges for the 0.x Cloudflare packages.

Both agents and @cloudflare/ai-chat are still on 0.x (the first release was ~Feb 2026, but both have since shipped breaking changes in subsequent 0.x minors). An open-ended >=0.1.0 range will accept any future major, including 1.x releases with unrelated APIs, and npm will quietly pull them in — leading to compile/runtime breakage for users.

Prefer a caret-capped range pinned to the version you've integrated against, e.g. "^0.4.0" for @cloudflare/ai-chat and "^0.11.0" for agents (or whichever versions you've tested), and widen explicitly as you test newer minors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/package.json` around lines 240 - 241, The peerDependency
ranges for the Cloudflare 0.x packages are too loose; change the entries for
"agents" and "@cloudflare/ai-chat" in package.json from open-ended ">=0.1.0" to
caret-capped ranges that pin to the tested minors (e.g. "agents": "^0.11.0" and
"@cloudflare/ai-chat": "^0.4.0" or whichever exact versions you validated) so
npm won't pull incompatible 1.x or untested 0.x minors; update those two
dependency values and run npm/pnpm/yarn install and tests to confirm
compatibility.

Comment on lines +85 to +90
async function buildSignatureHeader(secret: string, body: unknown): Promise<string> {
const timestamp = Date.now();
const hash = await createHmacSubtle(secret, `${timestamp}.${JSON.stringify(body)}`);

return `${timestamp}=${timestamp},v1=${hash}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the parser to see what header format it actually accepts
fd -t f 'signature.utils' packages/framework/src
rg -nP -C3 "split|hmacHeader|t=|v1=|version=" packages/framework/src/utils/signature.utils.ts 2>/dev/null || true
# Find any other callers/formatters that build the header on the sending side
rg -nP -C3 "x-novu-signature|NOVU_SIGNATURE_HEADER" --type=ts

Repository: novuhq/novu

Length of output: 7146


🏁 Script executed:

rg -n "signatureHeader\s*=" --type=ts packages/framework/src apps/api/src -B3 -A3

Repository: novuhq/novu

Length of output: 1492


🏁 Script executed:

rg -n "buildNovuSignatureHeader|function buildNovuSignatureHeader|export.*buildNovuSignatureHeader" --type=ts -B2 -A5

Repository: novuhq/novu

Length of output: 10616


Signature format mismatch between production and test — parser has critical expiry validation bug.

The test generates ${timestamp}=${timestamp},v1=${hash}, but production code (libs/application-generic/src/utils/hmac.ts) generates t=${timestamp},v1=${hash}. More critically, the parser in packages/framework/src/utils/signature.utils.ts has a bug: it extracts the key name ("t") instead of the numeric value when parsing the correct format.

Line 37 splits timestampPart by = and assigns the first element to timestamp. With production format t=1234567890, this makes timestamp = "t", and Number("t") evaluates to NaN. Line 40's expiry check NaN < (any number) always returns false, silently bypassing timestamp validation.

The test format accidentally works because the timestamp value appears as the key, but production deployments are not validating request freshness. Fix the parser to extract the numeric value correctly (e.g., check if the key is "t" and use the second element as the timestamp) and update the test to use the correct production format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/cloudflare.test.ts` around lines 85
- 90, Update both the test and the parser: change the test helper
buildSignatureHeader to emit the production format "t=<timestamp>,v1=<hash>"
instead of "<timestamp>=<timestamp>,v1=<hash>" and modify the signature parser
function (the function that currently splits timestampPart by '=' and assigns to
timestamp) to correctly extract the numeric timestamp by checking the key (e.g.,
if the left side is "t" use the right side as the timestamp), fall back to the
existing behavior for legacy formats if needed, and ensure you convert that
right-side string to Number before performing the expiry comparison so NaN
cannot bypass validation.

Comment thread packages/framework/src/servers/cloudflare/helpers.ts
Comment on lines +68 to +95
return async (request: Request, env: any, ctx: ExecutionContext): Promise<Response> => {
const url = new URL(request.url);
const action = url.searchParams.get('action') || '';

if (request.method === 'POST' && action === 'agent-event') {
return handleAgentEvent(request, url, env, agentIdToBinding, novuHandler.client);
}

if (request.method === 'OPTIONS') {
return baseHandler(request, env, ctx);
}

if (action && action !== 'agent-event') {
return baseHandler(request, env, ctx);
}

if (options.fallthrough) {
const result = await options.fallthrough(request, env, ctx);
if (result) {
return result;
}
}

return new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider re-verifying HMAC in the Durable Object as defense in depth.

HMAC validation only happens here in the Worker router. If a consumer ever mounts the DO behind another fetch path — e.g. Cloudflare Agents SDK's /agents/:class/:name routing via routeAgentRequest used as the top-level fetch in a different entrypoint, or stub.fetch called from another internal caller — a POST ?action=agent-event with a forged body would reach _handleNovuBridgeRequest unverified (it does not call validateNovuSignature).

Either (a) always validate inside _handleNovuBridgeRequest as well, or (b) drop an internal header like x-novu-router-verified: 1 in the forwarded request and require it in the DO. The router-only check is fine for the happy path but brittle as the surface grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/router.ts` around lines 68 - 95,
The Durable Object currently trusts that HMAC validation performed in the Worker
router was sufficient, which is brittle; update the DO's entry handler
_handleNovuBridgeRequest to enforce verification as well by either calling
validateNovuSignature on incoming agent-event POSTs or by requiring an internal
header (e.g. x-novu-router-verified: 1) that the router sets before forwarding;
specifically modify the DO to reject requests missing the internal header or
failing validateNovuSignature and ensure any router code that forwards requests
for action=agent-event sets the chosen header when it has performed validation.

Comment on lines +105 to +114
try {
const bodyText = await request.text();
const body = JSON.parse(bodyText);

await validateNovuSignature(
body,
request.headers.get('x-novu-signature'),
client.secretKey,
client.strictAuthentication
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm how validateNovuSignature computes the HMAC and whether it re-stringifies.
fd -t f 'signature.utils' packages/framework/src
rg -nP -C5 "hmac|subtle|JSON\.stringify|createHmac|payload" packages/framework/src/utils/signature.utils.ts 2>/dev/null || true

# Find the server-side signer to see what it signs (raw body vs parsed)
rg -nP -C5 "x-novu-signature|signBody|hmacSign" --type=ts

Repository: novuhq/novu

Length of output: 4333


🏁 Script executed:

#!/bin/bash
# Find where the signature is actually created (signatureHeader source)
rg -n "signatureHeader\s*=" apps/api/src/app/agents/services/bridge-executor.service.ts -B5 -A2

# Find signature creation functions
rg -n "createSignature|sign.*Hmac|hmac.*sign" --type=ts packages/ apps/

# Look for the actual signing logic in utils or services
fd -t f -e ts | xargs rg -l "createHmacSubtle" | head -5
rg -n "export.*createHmacSubtle" packages/framework/src/utils/
cat packages/framework/src/utils/crypto.utils.ts

Repository: novuhq/novu

Length of output: 1794


🏁 Script executed:

#!/bin/bash
# Find buildNovuSignatureHeader implementation
rg -n "buildNovuSignatureHeader|export.*buildNovuSignatureHeader" --type=ts packages/ apps/

# Show the full implementation if found
rg -n -A20 "function buildNovuSignatureHeader|export.*function buildNovuSignatureHeader" --type=ts

Repository: novuhq/novu

Length of output: 2564


🏁 Script executed:

#!/bin/bash
# Search for tests of signature validation
rg -n "validateNovuSignature" --type=ts -A10 packages/framework/src/utils/signature.utils.test.ts

# Check if there are integration tests for the router
rg -n "cloudflare.*test\|router.*test" --type=ts packages/framework/src/servers/cloudflare/ -i

# Search for agent endpoint signature examples or tests
rg -n "buildNovuSignatureHeader\|signature" --type=ts packages/framework/src/servers/cloudflare/cloudflare.test.ts -A5

# Look at the test file
fd -t f cloudflare.test.ts
cat packages/framework/src/servers/cloudflare/cloudflare.test.ts

Repository: novuhq/novu

Length of output: 12375


🏁 Script executed:

#!/bin/bash
# Search for agent endpoint documentation or examples
rg -n "agent.*endpoint|external.*agent|sign.*body|signature.*format" --type=ts --type=md packages/framework/src/ apps/ -i

# Check if there are examples of agent implementations
fd -t f -e ts -e md agent | grep -E "(example|sample|doc)" -i
find . -name "*.md" -exec grep -l "agent.*endpoint\|signature.*header" {} \;

Repository: novuhq/novu

Length of output: 3366


🏁 Script executed:

#!/bin/bash
# Check the documentation for agents and signature requirements
cat packages/framework/docs/cloudflare-agents.md

# Also check the use-copy-prompt to see the full documentation context
rg -n -B10 -A10 "Build the signed string" apps/dashboard/src/components/workflow-editor/steps/http-request/use-copy-prompt.tsx

# Check how Slack signs requests
cat apps/api/src/app/agents/e2e/helpers/providers/slack.ts

Repository: novuhq/novu

Length of output: 8928


Sign the raw request body bytes, not a parsed-then-stringified object.

The code passes a parsed body object to validateNovuSignature, which re-stringifies it for HMAC verification. While both Novu's sender and receiver currently use JSON.stringify(payload) (documented in the dashboard copy-prompt helper), relying on JSON.stringify determinism is fragile:

  • JSON.stringify is not canonicalized — key ordering, whitespace, and number formatting can vary between implementations or contexts
  • Modern JS preserves insertion order, so it works in practice, but this isn't guaranteed across all edge cases
  • If external agent endpoints or future integrations sign raw bytes directly (standard practice), they'll fail signature verification due to formatting mismatches

Since bodyText is already available at line 106, pass it directly to validateNovuSignature (either accept string | object in the function signature, or parse only after verification). This removes the risk of canonical mismatches and aligns with HMAC best practices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/router.ts` around lines 105 - 114,
The signature verification is using the parsed body object instead of the raw
request bytes, which can cause HMAC mismatches; change the call that currently
passes body to validateNovuSignature to pass the raw bodyText (from
request.text()) and only JSON.parse after successful validateNovuSignature, or
update validateNovuSignature to accept a string payload (string | object) and
prefer the raw string for HMAC; refer to bodyText, body, validateNovuSignature,
and request.text() when making this change.

Comment thread packages/framework/src/servers/cloudflare/with-novu-agent.ts
Comment on lines +37 to +42
const [timestamp, timestampPayload] = timestampPart.split('=');
const [, signaturePayload] = signaturePart.split('=');

if (Number(timestamp) < Date.now() - SIGNATURE_TIMESTAMP_TOLERANCE) {
throw new SignatureExpiredError();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the previous validateHmac implementation in git history to confirm
# whether version validation and timestamp parsing were previously correct.
git log --all --oneline -- packages/framework/src/handler.ts | head -20
git log -p --all -S 'SignatureVersionInvalidError' -- packages/framework/src/handler.ts | head -100
# Show current usages/tests of validateNovuSignature for expected header format
rg -nP -C3 'x-novu-signature|NOVU_SIGNATURE|validateNovuSignature|hmacHeader' --type=ts
# Show the tolerance constant
rg -nP -C2 'SIGNATURE_TIMESTAMP_TOLERANCE' --type=ts

Repository: novuhq/novu

Length of output: 23495


🏁 Script executed:

cat -n packages/framework/src/utils/signature.utils.ts

Repository: novuhq/novu

Length of output: 1734


🏁 Script executed:

# Check the constants definition more carefully
cat -n packages/framework/src/constants/api.constants.ts

Repository: novuhq/novu

Length of output: 359


🏁 Script executed:

# Check if there's documentation or API implementation showing actual header format
rg -nP 'buildSignatureHeader|createHmacSubtle' --type=ts -B3 -A10

Repository: novuhq/novu

Length of output: 13261


🏁 Script executed:

# Check how Novu backend generates the signature header (should show "t=" and "v1=" format)
rg -nP 'SIGNATURE_TIMESTAMP_TOLERANCE|sign.*header|t=|v1=' --type=ts -B2 -A2 libs/application-generic enterprise/workers

Repository: novuhq/novu

Length of output: 14074


🏁 Script executed:

# Search for actual header format generation to confirm real API uses t=/v1= format
rg -nP 'novuSignatureHeader|generateNovuSignature' --type=ts -B5 -A10

Repository: novuhq/novu

Length of output: 2862


Fix critical bugs in validateNovuSignature: timestamp parsing, timeout units, and missing version validation.

Lines 37–42 contain three bugs that defeat replay-attack protection:

  1. Timestamp parsing uses key instead of value — Line 37's destructuring assigns the key ("t") to timestamp, not the numeric value. Line 40 then does Number("t")NaN, making the expiry check always false. The real API generates headers as t=<epochMs>,v1=<hash>, so line 44's HMAC computation correctly uses timestampPayload, but line 40 wrongly compares the key.

  2. Tolerance constant has wrong unitsSIGNATURE_TIMESTAMP_TOLERANCE = 5 * 60 * 5 = 1500 (milliseconds), but Date.now() returns milliseconds. This gives 1.5 seconds tolerance instead of the intended 5 minutes (300000 ms).

  3. Version validation is missing — The JSDoc specifies format <timestamp>=<timestampPayload>,<version>=<signaturePayload>, and SignatureVersionInvalidError is defined, but the code discards the version with const [, signaturePayload] and never validates version === 'v1'. The enterprise workers implementation correctly uses .startsWith('v1=') checks.

The test suite (cloudflare.test.ts:89) doesn't catch bug #1 because it builds a malformed header (${timestamp}=${timestamp},v1=...) with the same numeric value as both key and payload, masking the bug.

Suggested fix
  const [timestampPart, signaturePart] = hmacHeader.split(',');
  if (!timestampPart || !signaturePart) {
    throw new SignatureInvalidError();
  }

-  const [timestamp, timestampPayload] = timestampPart.split('=');
-  const [, signaturePayload] = signaturePart.split('=');
+  const [timestampKey, timestampPayload] = timestampPart.split('=');
+  const [version, signaturePayload] = signaturePart.split('=');

+  if (!timestampPayload || !signaturePayload) {
+    throw new SignatureInvalidError();
+  }
+
+  if (version !== 'v1') {
+    throw new SignatureVersionInvalidError();
+  }

-  if (Number(timestamp) < Date.now() - SIGNATURE_TIMESTAMP_TOLERANCE) {
+  const timestampMs = Number(timestampPayload);
+  if (!Number.isFinite(timestampMs) || timestampMs < Date.now() - SIGNATURE_TIMESTAMP_TOLERANCE) {
     throw new SignatureExpiredError();
   }

Also fix the tolerance constant: SIGNATURE_TIMESTAMP_TOLERANCE = SIGNATURE_TIMESTAMP_TOLERANCE_MINUTES * 60 * 1000; (change final multiplier from 5 to 1000).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [timestamp, timestampPayload] = timestampPart.split('=');
const [, signaturePayload] = signaturePart.split('=');
if (Number(timestamp) < Date.now() - SIGNATURE_TIMESTAMP_TOLERANCE) {
throw new SignatureExpiredError();
}
const [timestampKey, timestampPayload] = timestampPart.split('=');
const [version, signaturePayload] = signaturePart.split('=');
if (!timestampPayload || !signaturePayload) {
throw new SignatureInvalidError();
}
if (version !== 'v1') {
throw new SignatureVersionInvalidError();
}
const timestampMs = Number(timestampPayload);
if (!Number.isFinite(timestampMs) || timestampMs < Date.now() - SIGNATURE_TIMESTAMP_TOLERANCE) {
throw new SignatureExpiredError();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/utils/signature.utils.ts` around lines 37 - 42, In
validateNovuSignature fix three issues: correctly parse the timestamp/value by
changing the destructuring of timestampPart to capture the key and value (use
timestampPart.split('=') -> [key, timestampPayload] and base expiry check on
Number(timestampPayload)), correct SIGNATURE_TIMESTAMP_TOLERANCE to be
minutes→ms (multiply minutes by 60 * 1000, not 5), and validate the signature
version by extracting the version from signaturePart (e.g., split signaturePart
into [version, signaturePayload] or check that signaturePart.startsWith('v1='))
and throw SignatureVersionInvalidError if the version is not 'v1'; ensure HMAC
still uses timestampPayload for the hash computation.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 17, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@novu/framework@10775
npm i https://pkg.pr.new/novu@10775

commit: 0901d61

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
examples/cloudflare-agent/wrangler.jsonc (1)

23-32: Simplify migrations for a fresh example.

This example project is newly added, so there is no deployed history of a WineBot class to migrate from. Shipping a two-step migration (v1 creates WineBot, v2 renames to CleaningBot) looks like leftover development state and will confuse users copying this as a template — especially since the dashboard integration guide likely shows a clean single-step migration.

♻️ Proposed simplification
   "migrations": [
     {
       "tag": "v1",
-      "new_sqlite_classes": ["WineBot"]
-    },
-    {
-      "tag": "v2",
-      "renamed_classes": [{ "from": "WineBot", "to": "CleaningBot" }]
+      "new_sqlite_classes": ["CleaningBot"]
     }
   ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cloudflare-agent/wrangler.jsonc` around lines 23 - 32, The
migrations array currently creates a WineBot in "v1" then renames it to
CleaningBot in "v2", which is confusing for a fresh example; replace these
two-step entries by a single migration that directly creates the intended class
(CleaningBot) by removing the "v2" renamed_classes entry and changing the "v1"
new_sqlite_classes to ["CleaningBot"] so the migrations only include a single,
clear creation step (refer to the migrations array, tag "v1",
new_sqlite_classes, tag "v2", renamed_classes, WineBot, and CleaningBot when
making the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/cloudflare-agent/wrangler.jsonc`:
- Around line 23-32: The migrations array currently creates a WineBot in "v1"
then renames it to CleaningBot in "v2", which is confusing for a fresh example;
replace these two-step entries by a single migration that directly creates the
intended class (CleaningBot) by removing the "v2" renamed_classes entry and
changing the "v1" new_sqlite_classes to ["CleaningBot"] so the migrations only
include a single, clear creation step (refer to the migrations array, tag "v1",
new_sqlite_classes, tag "v2", renamed_classes, WineBot, and CleaningBot when
making the change).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 067972a2-4e49-4831-8331-de64f5772c4a

📥 Commits

Reviewing files that changed from the base of the PR and between 17e2602 and 293d028.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • examples/cloudflare-agent/package.json
  • examples/cloudflare-agent/src/index.ts
  • examples/cloudflare-agent/wrangler.jsonc
  • packages/framework/cloudflare/helpers/package.json
  • packages/framework/cloudflare/package.json
  • packages/framework/src/servers/cloudflare/router.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/framework/cloudflare/helpers/package.json
  • packages/framework/cloudflare/package.json
  • examples/cloudflare-agent/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/framework/src/servers/cloudflare/router.ts

Align signature parsing with production Cloudflare format and harden validation, and prevent wiping agent state.

- signature.utils: import SignatureVersionInvalidError, parse headers expecting `t=<timestamp>,v1=<sig>`, validate numeric timestamp and explicit `v1` version, compute HMAC over `<timestamp>.<JSON-payload>` and compare. Improves robustness and throws clear errors for invalid version/timestamp.
- cloudflare/helpers: change rememberLastRef to merge the new STATE_KEY into existing agent.state instead of replacing it, preventing loss of other state properties.
- cloudflare.test: update tests to use the `t=` header format, add a test for invalid signature version, and adjust the fake agent/setState test to ensure other state keys are preserved.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/framework/src/servers/cloudflare/helpers.ts (2)

45-49: Type-narrow the stored ref instead of casting.

agent.state[STATE_KEY] as NovuConversationRef | undefined blindly trusts whatever was persisted. If state was written by older code or corrupted, downstream access to ref.conversationId / ref.integrationIdentifier (used at Lines 57-58) can silently send a malformed payload. The !ref?.replyUrl guard catches the common case, but consider validating the other required fields too before posting.

-  const ref = agent.state[STATE_KEY] as NovuConversationRef | undefined;
-  if (!ref?.replyUrl) {
+  const ref = agent.state[STATE_KEY] as NovuConversationRef | undefined;
+  if (!ref?.replyUrl || !ref.conversationId || !ref.integrationIdentifier) {
     throw new Error('No conversation ref stored — call rememberLastRef(agent, ctx) first');
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/helpers.ts` around lines 45 - 49,
The stored state should be validated instead of blindly cast: in
replyToLastConversation, read agent.state[STATE_KEY] as unknown and perform a
runtime type-narrowing check that ensures the object has the required fields
(replyUrl, conversationId, integrationIdentifier) and correct types before using
them; if any required field is missing or wrong type, throw a clear Error asking
to call rememberLastRef(agent, ctx) or indicating corrupted state. Reference
STATE_KEY, replyToLastConversation, and NovuConversationRef when adding the
validation and replace the current "as NovuConversationRef | undefined" cast
with the runtime checks.

62-74: Add a request timeout to prevent Durable Object from hanging on slow endpoints.

The fetch to ref.replyUrl lacks an explicit timeout, which can cause the Durable Object to hang if the Novu endpoint is unresponsive or slow. AbortSignal.timeout() is supported in Cloudflare Workers and makes failure modes predictable, especially when called from scheduled tasks.

   const response = await fetch(ref.replyUrl, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       Authorization: `ApiKey ${secretKey}`,
     },
     body: JSON.stringify(body),
+    signal: AbortSignal.timeout(10_000),
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/framework/src/servers/cloudflare/helpers.ts` around lines 62 - 74,
The fetch to ref.replyUrl in this block should use an AbortSignal timeout to
avoid the Durable Object hanging: create a signal via AbortSignal.timeout(ms)
(e.g., 10000) and pass it into fetch options (signal) when posting the JSON body
to ref.replyUrl, and handle a potential abort/timeout by catching the thrown
error and rethrowing a descriptive Error (including that it timed out) so
replyToLastConversation's caller gets a predictable failure; ensure the signal
is used alongside the existing headers and body and that any response.ok logic
remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/framework/src/servers/cloudflare/helpers.ts`:
- Around line 45-49: The stored state should be validated instead of blindly
cast: in replyToLastConversation, read agent.state[STATE_KEY] as unknown and
perform a runtime type-narrowing check that ensures the object has the required
fields (replyUrl, conversationId, integrationIdentifier) and correct types
before using them; if any required field is missing or wrong type, throw a clear
Error asking to call rememberLastRef(agent, ctx) or indicating corrupted state.
Reference STATE_KEY, replyToLastConversation, and NovuConversationRef when
adding the validation and replace the current "as NovuConversationRef |
undefined" cast with the runtime checks.
- Around line 62-74: The fetch to ref.replyUrl in this block should use an
AbortSignal timeout to avoid the Durable Object hanging: create a signal via
AbortSignal.timeout(ms) (e.g., 10000) and pass it into fetch options (signal)
when posting the JSON body to ref.replyUrl, and handle a potential abort/timeout
by catching the thrown error and rethrowing a descriptive Error (including that
it timed out) so replyToLastConversation's caller gets a predictable failure;
ensure the signal is used alongside the existing headers and body and that any
response.ok logic remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d232307-712a-400b-abac-71448d86099d

📥 Commits

Reviewing files that changed from the base of the PR and between 293d028 and 0901d61.

📒 Files selected for processing (3)
  • packages/framework/src/servers/cloudflare/cloudflare.test.ts
  • packages/framework/src/servers/cloudflare/helpers.ts
  • packages/framework/src/servers/cloudflare/with-novu-agent.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/framework/src/servers/cloudflare/cloudflare.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/framework/src/servers/cloudflare/with-novu-agent.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant