Skip to content

Commit 0901d61

Browse files
committed
Validate v1 Cloudflare signature; preserve state
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.
1 parent 293d028 commit 0901d61

3 files changed

Lines changed: 21 additions & 9 deletions

File tree

packages/framework/src/servers/cloudflare/cloudflare.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('validateNovuSignature()', () => {
8686
const timestamp = Date.now();
8787
const hash = await createHmacSubtle(secret, `${timestamp}.${JSON.stringify(body)}`);
8888

89-
return `${timestamp}=${timestamp},v1=${hash}`;
89+
return `t=${timestamp},v1=${hash}`;
9090
}
9191

9292
it('should pass for a valid signature', async () => {
@@ -126,10 +126,18 @@ describe('validateNovuSignature()', () => {
126126
it('should throw SignatureExpiredError for old timestamps', async () => {
127127
const oldTimestamp = Date.now() - 1000 * 60 * 60;
128128
const hash = await createHmacSubtle(secretKey, `${oldTimestamp}.${JSON.stringify(payload)}`);
129-
const header = `${oldTimestamp}=${oldTimestamp},v1=${hash}`;
129+
const header = `t=${oldTimestamp},v1=${hash}`;
130130

131131
await expect(validateNovuSignature(payload, header, secretKey, true)).rejects.toThrow('Signature expired');
132132
});
133+
134+
it('should throw SignatureVersionInvalidError for wrong version', async () => {
135+
const timestamp = Date.now();
136+
const hash = await createHmacSubtle(secretKey, `${timestamp}.${JSON.stringify(payload)}`);
137+
const header = `t=${timestamp},v2=${hash}`;
138+
139+
await expect(validateNovuSignature(payload, header, secretKey, true)).rejects.toThrow('Signature version is invalid');
140+
});
133141
});
134142

135143
describe('withNovuAgent mixin', () => {
@@ -252,13 +260,13 @@ describe('helpers: rememberLastRef / replyToLastConversation', () => {
252260
it('round-trips a ref through setState / state', async () => {
253261
const { rememberLastRef, replyToLastConversation } = await import('./helpers');
254262

255-
let storedState: Record<string, unknown> = {};
263+
let storedState: Record<string, unknown> = { other: 'keep-me' };
256264

257265
const fakeAgent = {
258266
env: { NOVU_SECRET_KEY: 'test-key' },
259267
state: storedState,
260-
setState(patch: Record<string, unknown>) {
261-
Object.assign(storedState, patch);
268+
setState(newState: Record<string, unknown>) {
269+
storedState = newState;
262270
this.state = storedState;
263271
},
264272
};
@@ -273,6 +281,7 @@ describe('helpers: rememberLastRef / replyToLastConversation', () => {
273281
conversationId: 'conv-456',
274282
integrationIdentifier: 'slack-main',
275283
});
284+
expect(storedState.other).toBe('keep-me');
276285

277286
await replyToLastConversation(fakeAgent, 'ping');
278287

packages/framework/src/servers/cloudflare/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface StatefulAgent {
3535
* Call this inside `onNovuMessage` / `onNovuAction` / etc.
3636
*/
3737
export function rememberLastRef(agent: StatefulAgent, ctx: AgentContext): void {
38-
agent.setState({ [STATE_KEY]: ctx.serialize() });
38+
agent.setState({ ...agent.state, [STATE_KEY]: ctx.serialize() });
3939
}
4040

4141
/**

packages/framework/src/servers/cloudflare/with-novu-agent.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,13 @@ export function withNovuAgent<TBase extends Constructor>(Base: TBase) {
165165
};
166166

167167
const handler = handlerMap[event as AgentEventEnum];
168-
if (handler) {
169-
await handler(ctx);
168+
if (!handler) {
169+
console.warn(`[novu-agent] Unknown event: ${event}`);
170+
171+
return Response.json({ error: `Unknown event: ${event}` }, { status: 400 });
170172
}
171173

174+
await handler(ctx);
172175
await ctx.flush();
173176

174177
return Response.json({ status: 'ok' });
@@ -183,5 +186,5 @@ export function withNovuAgent<TBase extends Constructor>(Base: TBase) {
183186
}
184187
}
185188

186-
return NovuAgentMixin as unknown as TBase & NovuAgentStatics;
189+
return NovuAgentMixin as unknown as TBase & NovuAgentStatics & (new (...args: any[]) => NovuAgentInstance);
187190
}

0 commit comments

Comments
 (0)